Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modal Component #2795

Open
wants to merge 44 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
b54a608
update component sing native dialog and polyfill
sirineJ Dec 13, 2024
b9006a2
create new ModalContext
sirineJ Dec 13, 2024
ae31cad
add tests
sirineJ Dec 13, 2024
95d8df1
add translations
sirineJ Dec 13, 2024
0755db0
add documentation
sirineJ Dec 13, 2024
e54ee8a
update NotificationModal component
sirineJ Dec 13, 2024
bd6138c
export components
sirineJ Dec 13, 2024
17a8a71
fix scroll-disabling styles
sirineJ Dec 13, 2024
c311dc9
require min typescript version of 4.1
sirineJ Dec 13, 2024
04bbd3f
fix ::backdrop inheritance issue for older browsers
sirineJ Dec 13, 2024
1a42626
add changeset
sirineJ Dec 13, 2024
6af4eaf
fix package-lock.json
sirineJ Dec 13, 2024
097326a
fix animation duration
sirineJ Dec 16, 2024
fcf6ada
fix: classes order
sirineJ Dec 16, 2024
cd8658d
Silence lint warnings in CI
connor-baer Dec 16, 2024
a8c1528
Format Modal.mdx
connor-baer Dec 16, 2024
d722e5f
fix CI
sirineJ Dec 17, 2024
3270b84
refactor styles
sirineJ Dec 17, 2024
3e6bf41
fix tests
sirineJ Dec 17, 2024
712d8ec
code review Pt 1
sirineJ Dec 17, 2024
8e0be27
optimise scrolling
sirineJ Dec 18, 2024
f4dcc9f
use useStack
sirineJ Dec 18, 2024
992224c
export useScrollLock
sirineJ Dec 18, 2024
e825622
refactor hasNativeDialogSupport
sirineJ Dec 18, 2024
8608ecf
format file
sirineJ Dec 19, 2024
cb2e3de
cde review pt1
sirineJ Dec 19, 2024
28a195b
add doc
sirineJ Dec 19, 2024
7c1c2e8
refactor styles
sirineJ Dec 19, 2024
a4829f7
improve closing modal on unmount
sirineJ Dec 19, 2024
2c19c0b
refactor tests
sirineJ Dec 19, 2024
b04366f
styles
sirineJ Dec 20, 2024
36ddb31
fix useRef
sirineJ Dec 20, 2024
739e8ae
fix scroll affordance
sirineJ Dec 20, 2024
a2d5cf6
add layout padded
sirineJ Dec 20, 2024
2e24a46
add doc to useScrollLock
sirineJ Dec 20, 2024
a56459a
fix params
sirineJ Dec 20, 2024
4440560
restore scroll on clean up
sirineJ Dec 22, 2024
466752e
fix CR workflow that creates previews based on main when using `issue…
sirineJ Dec 23, 2024
5ded272
remove unused animation styles
sirineJ Dec 23, 2024
871f8f9
add explanation for box-shadow workaround
sirineJ Dec 23, 2024
90fc87c
use label workaround
sirineJ Dec 23, 2024
6d10508
change DateInput positioning to fixed
sirineJ Dec 26, 2024
b42d2bf
fix modal width
sirineJ Dec 26, 2024
dc823fa
add post CR comment
sirineJ Dec 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cyan-knives-drop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sumup-oss/circuit-ui": minor
---

Set an explicit minimum version for TypeScript of 4.1 or higher. While this is technically a breaking change, v4.1 was released over 4 years ago, so we don't expect this to break anyone's code. Please let us know if this causes you issues.
5 changes: 5 additions & 0 deletions .changeset/eight-beers-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sumup-oss/circuit-ui": minor
---

Refactored the NotificationModal component to use the new Modal component under the hood.
5 changes: 5 additions & 0 deletions .changeset/gold-lemons-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sumup-oss/design-tokens": minor
---

Added "::backdrop" to the list of selectors to apply theme custom properties to. See https://developer.chrome.com/blog/css-backdrop-inheritance.
5 changes: 5 additions & 0 deletions .changeset/grumpy-wombats-talk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sumup-oss/circuit-ui": minor
---

Added a new hook `useScrollLock` to disable page scroll on demand.
5 changes: 5 additions & 0 deletions .changeset/rich-icons-pretend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sumup-oss/circuit-ui": minor
---

Deprecated the `hideCloseButton` prop in the Modal and NotificationModal components. It had no effect.
5 changes: 5 additions & 0 deletions .changeset/sharp-seals-leave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sumup-oss/circuit-ui": minor
---

Added default translations for the Modal and NotificationModal components. The `closeButtonLabel` prop is now optional.
5 changes: 5 additions & 0 deletions .changeset/wicked-pants-cough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sumup-oss/circuit-ui": minor
---

Refactored the Modal component to use the native `dialog` element. The Modal component can now be rendered directly in your JSX (the older `useModal` hook continues to be supported).
83 changes: 79 additions & 4 deletions .github/workflows/cr.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
name: Continuous Releases
on:
issue_comment:
types: [created]
pull_request:
types:
- labeled

jobs:
build:
if: ${{ github.event.comment.body == '/preview' && github.event.issue.pull_request }}
if: ${{ github.event.label.name == 'preview' }}
runs-on: ubuntu-latest

steps:
Expand All @@ -25,4 +26,78 @@ jobs:
run: npm run build

- name: Publish packages
run: npx pkg-pr-new publish './packages/circuit-ui' './packages/design-tokens' './packages/icons'
run: npx pkg-pr-new publish './packages/circuit-ui' './packages/design-tokens' './packages/icons' --json output.json --comment=off

- name: Post or update comment
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
const output = JSON.parse(fs.readFileSync('output.json', 'utf8'));
console.log(output);

const packages = output.packages
.map((p) => `- ${p.name}: ${p.url}`)
.join('\n');

const sha = context.payload.pull_request.head.sha

const commitUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${sha}`;

const body = `## 🚀 Your packages were published

### Published Packages:

${packages}

[View Commit](${commitUrl})`;

const botCommentIdentifier = '## 🚀 Your packages were published ';

async function findBotComment(issueNumber) {
if (!issueNumber) return null;
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
});
return comments.data.find((comment) =>
comment.body.includes(botCommentIdentifier)
);
}

async function createOrUpdateComment(issueNumber) {
if (!issueNumber) {
console.log('No issue number provided. Cannot post or update comment.');
return;
}

const existingComment = await findBotComment(issueNumber);
if (existingComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existingComment.id,
body: body,
});
} else {
await github.rest.issues.createComment({
issue_number: issueNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: body,
});
}
}
if (context.issue.number) {
await createOrUpdateComment(context.issue.number);
}

- name: Delete label
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER:
${{ github.event.pull_request.number }}
run: |
gh pr edit $PR_NUMBER --remove-label "preview"
3 changes: 2 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"test:ci": "vitest run --coverage",
"lint": "biome check --diagnostic-level=error && foundry run eslint . --ext .js,.jsx,.ts,.tsx",
"lint:fix": "biome check --write --diagnostic-level=error && foundry run eslint . --ext .js,.jsx,.ts,.tsx --fix",
"lint:ci": "biome ci && foundry run eslint . --ext .js,.jsx,.ts,.tsx --quiet ",
"lint:ci": "biome ci --diagnostic-level=error && foundry run eslint . --ext .js,.jsx,.ts,.tsx --quiet ",
"lint:css": "foundry run stylelint '**/*.css'",
"lint:css:fix": "foundry run stylelint '**/*.css' --fix",
"dev": "npm run docs:start",
Expand Down
1 change: 1 addition & 0 deletions packages/circuit-ui/components/DateInput/DateInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ export const DateInput = forwardRef<HTMLInputElement, DateInputProps>(
const { floatingStyles, update } = useFloating({
open,
placement,
strategy: 'fixed',
middleware: [
offset(4),
flip({ padding, fallbackAxisSideDirection: 'start' }),
Expand Down
92 changes: 75 additions & 17 deletions packages/circuit-ui/components/Modal/Modal.mdx
connor-baer marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -5,58 +5,116 @@ import * as Stories from './Modal.stories';

# Modal

<Status variant="under-review" />
<Status variant="stable" />

The modal component displays self-contained tasks in a focused window that overlays the page content.
The modal component displays self-contained tasks in an overlay view, requiring the user to interact with it before returning to the underlying content.

<Story of={Stories.Base} />
<Props />

## When to use it

Generally, use the modal component sparingly. Consider displaying more complex tasks and large amounts of information on a separate page instead.
Generally, use the modal dialog component sparingly. Consider displaying more complex tasks and large amounts of information on a separate page instead.

## Variants
A modal dialog is a disruptive pattern that interrupts the user's current task. It should only be used when the task requires immediate attention or when the user needs to make a decision before continuing:

<Story of={Stories.Variants} />
- Confirming a critical action, such as deleting an item or abandoning a task (also see the [NotificationModal](Notification/NotificationModal/Docs) component).
- Forms or inputs to collect small amounts of data without navigating away from the current page (e.g., login, feedback forms).
- Focused tasks like editing an item, uploading files, or reviewing details.

### Contextual
Copy link
Member

@connor-baer connor-baer Dec 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happened to the contextual variant?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even though it's the default variant, we need to document the contextual variant to help users choose between the variants.

## How to use it

Use this variant when the modal content requires the context of the page underneath to be understood. On small viewports, the modal component opens up from the bottom as a bottom sheet overlay on top of the page content, dimming the uncovered area while giving a visual context of the page underneath. The height of the bottom sheet can be manually adjusted depending on the use case and the amount of content needed to be displayed.
### Inline (recommended)

### Immersive
Place your dialog content directly in the `Modal` component:

Use this variant to focus the user's attention on the modal content. On small viewports, the modal component opens up from the bottom as a fullscreen overlay on top of the page content and covers it entirely in favor of an immersive experience.
```tsx
import { Modal, Body, Button, Heading } from '@sumup-oss/circuit-ui';
import { useState } from 'react';

function Component() {
const [open, setOpen] = useState(false);

return (
<>
<Button onClick={() => setOpen(true)}>Open dialog</Button>
<Modal
open={open}
aria-labelledby="dialog-title"
onClose={() => setOpen(false)}
>
{() => (
<>
<Heading as="h2" id="dialog-title">
Modal title
</Heading>
<Body>Modal content</Body>
<Button>Close dialog</Button>
</>
)}
</Modal>
</>
);
}
```

## Usage in code
### With the `useModal` hook

First, wrap your application in the `ModalProvider` which keeps track of the open modals, prevents scrolling of the page when a modal is open, and ensures the accessibility of the modal.
First, wrap your application in the `ModalProvider`:

```tsx
// _app.tsx for Next.js or App.js for CRA
import { ModalProvider } from '@sumup-oss/circuit-ui';

export default function App() {
export function App() {
return <ModalProvider>{/* Your content here... */}</ModalProvider>;
}
```

Then, use the `useModal` hook to open a modal from a component:
Then, use the `useModal` hook to open a dialog from a component:

```tsx
import { useModal, Button, Body } from '@sumup-oss/circuit-ui';
import { useModal, Heading, Button, Body } from '@sumup-oss/circuit-ui';

export function SayHello({ name }) {
const { setModal } = useModal();

const handleClick = () => {
setModal({
children: <Body>Hello {name}</Body>,
children: (
<>
<Heading as="h2" id="dialog-title">
Modal title
</Heading>
<Body>Modal content</Body>
<Button>Close dialog</Button>
</>
),
'aria-labelledby': 'dialog-title',
variant: 'immersive',
closeButtonLabel: 'Close modal',
});
};

return <Button onClick={handleClick}>Say hello</Button>;
}
```

## Immersive

Use the `immersive` variant to focus the user's attention on the dialog content. On small viewports, the dialog component opens up from the bottom as a fullscreen overlay on top of the page content and covers it entirely in favor of an immersive experience.

<Story of={Stories.Immersive} />

## Related components

For cases displaying simple text content but requiring immediate or critical action(s) from the user, consider using the [NotificationModal](Notification/NotificationModal/Docs) component.

## Accessibility

This component is built using the native `dialog` HTML element and follows the [WAI-ARIA Modal Authoring Practices](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/). It is fully accessible and supports keyboard navigation and screen readers. In browsers where `dialog` is not supported, it uses [dialog-polyfill](https://github.com/GoogleChrome/dialog-polyfill).

It is important to ensure that the dialog is appropriately labeled and that the user can easily navigate and interact with it using a keyboard or screen reader.
If your dialog content has a title, make sure to provide an `aria-labelledby` attribute with the ID of the title element to the `Modal` component. Otherwise, provide an `aria-label` attribute with a descriptive label.

If your dialog displays a complex flow with multiple screens (for example: a complex form with multiple steps), make sure to programmatically set focus to the title upon landing on every step to convey to the user their evolution within your flow.

If your content contains interactive elements, the component will focus the first interactive element when the dialog opens by default. However, if you wish to focus a different element, you can provide the `initialFocusRef` prop with the ref of the element you want to focus. It is generally recommended to focus the least destructive element, such as a close or a cancel button. If the element you want to focus is not interactive, don't forget to give it a tabindex with a negative value to enable its focus.
Loading
Loading