@neo4j-ndl/react
Version:
React implementation of Neo4j Design System
486 lines (412 loc) • 17.8 kB
Markdown
# Dialog
Import: `import { Dialog } from '@neo4j-ndl/react'`
## Props
### Dialog
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `children` | `ReactNode` | ✅ | | Content to render inside the Modal. |
| `container` | `Element` | | | The container where the modal will be rendered. |
| `hasDisabledCloseButton` | `boolean` | | `false` | Whether to disable the close button in the top-right corner. |
| `isOpen` | `boolean` | ✅ | | Controls whether the dialog is open or closed. |
| `onClose` | `((e: MouseEvent<HTMLElement, MouseEvent> \| KeyboardEvent<HTMLElement>, reason?: DialogCloseReason) => void)` | | | Callback function called when the dialog is closed. |
| `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. |
| `rootProps` | `HtmlAttributes<"div">` | | | Forwards the props to the modal container's wrapper - use with caution |
| `size` | `'large' \| 'medium' \| 'small' \| 'unset'` | | | Sets the size(max-width) of the Modal, use 'unset' for custom sizing. |
| `variant` | `'danger' \| 'info' \| 'warning'` | | | Type of dialog, which determines the icon and styling. |
### Dialog.Actions
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `as` | `ElementType<any, keyof IntrinsicElements>` | | | An override of the default HTML tag of the root of the component. Can also be another React component. |
| `children` | `ReactNode` | | | The content to display within the dialog component. |
| `ref` | `any` | | | A ref to apply to the root element. |
### Dialog.Content
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `as` | `ElementType<any, keyof IntrinsicElements>` | | | An override of the default HTML tag of the root of the component. Can also be another React component. |
| `children` | `ReactNode` | | | The content to display within the dialog component. |
| `ref` | `any` | | | A ref to apply to the root element. |
### Dialog.Description
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `as` | `ElementType<any, keyof IntrinsicElements>` | | | An override of the default HTML tag of the root of the component. Can also be another React component. |
| `children` | `ReactNode` | | | The content to display within the dialog component. |
| `ref` | `any` | | | A ref to apply to the root element. |
### Dialog.Header
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `as` | `ElementType<any, keyof IntrinsicElements>` | | | An override of the default HTML tag of the root of the component. Can also be another React component. |
| `children` | `ReactNode` | | | The content to display within the dialog component. |
| `ref` | `any` | | | A ref to apply to the root element. |
### Dialog.Image
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `alt` | `string` | ✅ | | Alternative text for the image |
| `ref` | `Ref<HTMLImageElement>` | | | A ref to apply to the root element. |
| `src` | `string` | ✅ | | Source URL of the image |
### Dialog.Subtitle
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `as` | `ElementType<any, keyof IntrinsicElements>` | | | An override of the default HTML tag of the root of the component. Can also be another React component. |
| `children` | `ReactNode` | | | The content to display within the dialog component. |
| `ref` | `any` | | | A ref to apply to the root element. |
## Accessibility
## Keyboard interactions
Implements the keyboard interactions defined in the [WAI-ARIA dialog (modal) pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/).
| Key | Description |
| ------------- | --------------------------------------------------------------- |
| `Tab` | Moves focus to the next focusable element within the dialog |
| `Shift + Tab` | Moves focus to the previous focusable element within the dialog |
| `Escape` | Closes the dialog |
Escape always closes the dialog — this behavior cannot be disabled. The `onClose` callback receives a `reason` parameter (`escapeKeyDown` or `closeButtonClick`) so you can distinguish how the dialog was closed.
## WAI-ARIA roles and attributes
The Dialog component follows the [WAI-ARIA dialog (modal) pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/).
- The dialog container has `role="dialog"` and `aria-modal="true"`. When `variant` is `warning` or `danger`, the role is set to `alertdialog` per the [WAI-ARIA alertdialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/)
- Focus is trapped within the dialog while open and returned to the trigger on close
- Initial focus moves to the first focusable element inside the dialog
- The dialog is labeled via `aria-labelledby` (auto-wired from `Dialog.Header`) or `aria-label`
- The dialog is described via `aria-describedby` (auto-wired from `Dialog.Description`)
- The close button has `aria-label="Close"`
- When a `variant` is set (`info`, `warning`, or `danger`), the icon is announced to screen readers via `aria-label`
- Document scroll is locked while the dialog is open
- Clicking the backdrop does not close the dialog, keeping the user's context preserved
## Implementation guidelines
- Always provide either a `Dialog.Header` or an `aria-label` via `htmlAttributes` so the dialog has an accessible name. A console warning is emitted if neither is provided
- If using a button as the trigger, set `aria-haspopup="dialog"` and `aria-controls` on the trigger button
- If `hasDisabledCloseButton` is set to `true`, the built-in close button is removed. You must provide an alternative visible way to close the dialog (e.g. action buttons). `Escape` still works but it is not discoverable by all users and is unavailable on touch devices.
- `aria-describedby` is auto-wired from `Dialog.Description`. For `alertdialog` variants (`warning`/`danger`), screen readers will announce this description automatically when the dialog opens
- For `warning` and `danger` variants the dialog text must clearly communicate the consequences of the action. The variant icon and `alertdialog` role signal urgency but screen reader users rely on the text content alone to understand what is at stake. For example: "Are you sure you want to delete this project? This action cannot be undone."
- The dialog does not support setting a custom initial focus element — focus always moves to the first focusable element (the close button). Be aware of how you structure your content and how you want focus to be ordered.
### Related WCAG criteria
- [2.4.3 Focus Order](https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html) (A): Focus is automatically trapped within the dialog and returned to the trigger on close
- [2.4.11 Focus Not Obscured](https://www.w3.org/WAI/WCAG22/Understanding/focus-not-obscured-minimum.html) (AA): The modal overlay and scroll lock prevent interaction with obscured content behind the dialog
- [4.1.2 Name, Role, Value](https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html) (A): `role="dialog"`/`role="alertdialog"`, `aria-modal`, accessible name (`aria-labelledby`), and accessible description (`aria-describedby`) are set automatically by the component
## Examples
### Default
```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
import { Dialog, FilledButton, OutlinedButton } from '@neo4j-ndl/react';
import { useRef, useState } from 'react';
const Component = (props: { isOpen: boolean }) => {
const [isOpen, setIsOpen] = useState<boolean>(props.isOpen);
const handleClick = () => setIsOpen((prev) => !prev);
const handleClose = () => setIsOpen(false);
const ref = useRef<HTMLDivElement | null>(null);
return (
<div
ref={ref}
className="n-flex n-flex-col n-items-center n-justify-center n-relative n-h-[500px] n-w-full"
>
<div className="n-flex n-justify-center">
<FilledButton
onClick={handleClick}
htmlAttributes={{ 'aria-haspopup': 'dialog' }}
>
Open Dialog
</FilledButton>
</div>
<Dialog
onClose={handleClose}
isOpen={isOpen}
container={ref?.current || undefined}
htmlAttributes={{ id: 'default-dialog' }}
>
<Dialog.Header>Header</Dialog.Header>
<Dialog.Description>This is a description</Dialog.Description>
<Dialog.Content>This is where you put your content</Dialog.Content>
<Dialog.Actions>
<OutlinedButton onClick={handleClose} variant="neutral" size="medium">
Secondary
</OutlinedButton>
<FilledButton onClick={handleClose} size="medium">
Primary
</FilledButton>
</Dialog.Actions>
</Dialog>
</div>
);
};
export default Component;
```
### Danger
```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
import { Dialog, FilledButton, OutlinedButton } from '@neo4j-ndl/react';
import { useState } from 'react';
const Component = (props: { isOpen: boolean }) => {
const [isOpen, setIsOpen] = useState<boolean>(props.isOpen);
const handleClick = () => setIsOpen((prev) => !prev);
const handleClose = () => setIsOpen(false);
return (
<>
<div className="n-flex n-justify-center">
<FilledButton
onClick={handleClick}
htmlAttributes={{ 'aria-haspopup': 'dialog' }}
>
Open Dialog
</FilledButton>
</div>
<Dialog
onClose={handleClose}
isOpen={isOpen}
htmlAttributes={{
id: 'danger-dialog',
}}
variant="danger"
>
<Dialog.Header>Header</Dialog.Header>
<Dialog.Subtitle>This is a subtitle</Dialog.Subtitle>
<Dialog.Description>This is a description</Dialog.Description>
<Dialog.Actions>
<OutlinedButton onClick={handleClose} variant="neutral" size="medium">
Secondary
</OutlinedButton>
<FilledButton onClick={handleClose} size="medium">
Primary
</FilledButton>
</Dialog.Actions>
</Dialog>
</>
);
};
export default Component;
```
### Image
```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
import { Dialog, FilledButton, OutlinedButton } from '@neo4j-ndl/react';
import { useState } from 'react';
import DialogExample from './assets/DialogExample.png';
const Component = (props: { isOpen: boolean }) => {
const [isOpen, setIsOpen] = useState<boolean>(props.isOpen);
const handleClick = () => setIsOpen((prev) => !prev);
const handleClose = () => setIsOpen(false);
return (
<>
<div className="n-flex n-justify-center">
<FilledButton
onClick={handleClick}
htmlAttributes={{ 'aria-haspopup': 'dialog' }}
>
Open Dialog
</FilledButton>
</div>
<Dialog
htmlAttributes={{
id: 'image-dialog',
}}
onClose={handleClose}
isOpen={isOpen}
>
<Dialog.Header>Header</Dialog.Header>
<Dialog.Image src={DialogExample} alt="Example image" />
<Dialog.Description>This is a description.</Dialog.Description>
<Dialog.Actions>
<OutlinedButton onClick={handleClose} variant="neutral" size="medium">
Secondary
</OutlinedButton>
<FilledButton onClick={handleClose} size="medium">
Primary
</FilledButton>
</Dialog.Actions>
</Dialog>
</>
);
};
export default Component;
```
### Info
```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
import { Dialog, FilledButton, OutlinedButton } from '@neo4j-ndl/react';
import { useState } from 'react';
const Component = (props: { isOpen: boolean }) => {
const [isOpen, setIsOpen] = useState<boolean>(props.isOpen);
const handleClick = () => setIsOpen((prev) => !prev);
const handleClose = () => setIsOpen(false);
return (
<>
<div className="n-flex n-justify-center">
<FilledButton
onClick={handleClick}
htmlAttributes={{ 'aria-haspopup': 'dialog' }}
>
Open Dialog
</FilledButton>
</div>
<Dialog
onClose={handleClose}
isOpen={isOpen}
htmlAttributes={{
id: 'info-dialog',
}}
variant="info"
>
<Dialog.Header>Header</Dialog.Header>
<Dialog.Subtitle>This is a subtitle</Dialog.Subtitle>
<Dialog.Description>This is a description</Dialog.Description>
<Dialog.Actions>
<OutlinedButton onClick={handleClose} variant="neutral" size="medium">
Secondary
</OutlinedButton>
<FilledButton onClick={handleClose} size="medium">
Primary
</FilledButton>
</Dialog.Actions>
</Dialog>
</>
);
};
export default Component;
```
### Top Aligned
```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
import { Dialog, FilledButton, OutlinedButton } from '@neo4j-ndl/react';
import { useState } from 'react';
const Component = (props: { isOpen: boolean }) => {
const [isOpen, setIsOpen] = useState<boolean>(props.isOpen);
const handleClick = () => setIsOpen((prev) => !prev);
const handleClose = () => setIsOpen(false);
return (
<>
<div className="n-flex n-justify-center">
<FilledButton
onClick={handleClick}
htmlAttributes={{ 'aria-haspopup': 'dialog' }}
>
Open Dialog
</FilledButton>
</div>
<Dialog
htmlAttributes={{
id: 'top-aligned-dialog',
}}
className="n-align-top n-mt-token-16"
onClose={handleClose}
isOpen={isOpen}
>
<Dialog.Header>Header</Dialog.Header>
<Dialog.Content>This is where you put your content</Dialog.Content>
<Dialog.Actions>
<OutlinedButton onClick={handleClose} variant="neutral" size="medium">
Secondary
</OutlinedButton>
<FilledButton onClick={handleClose} size="medium">
Primary
</FilledButton>
</Dialog.Actions>
</Dialog>
</>
);
};
export default Component;
```
### Variants
```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
import { Dialog, FilledButton, OutlinedButton } from '@neo4j-ndl/react';
import { useState } from 'react';
const Component = (props: { isOpen: boolean }) => {
const [variant, setVariant] = useState<
React.ComponentProps<typeof Dialog>['variant'] | undefined
>(undefined);
const [isOpen, setIsOpen] = useState<boolean>(props.isOpen);
const handleClick = (
variant: React.ComponentProps<typeof Dialog>['variant'],
) => {
setVariant(variant);
setIsOpen((prev) => !prev);
};
const handleClose = () => setIsOpen(false);
return (
<>
<div className="n-flex n-justify-center n-gap-token-16">
<FilledButton
onClick={() => handleClick('info')}
htmlAttributes={{ 'aria-haspopup': 'dialog' }}
>
Info Dialog
</FilledButton>
<FilledButton
onClick={() => handleClick('warning')}
htmlAttributes={{ 'aria-haspopup': 'dialog' }}
>
Warning Dialog
</FilledButton>
<FilledButton
onClick={() => handleClick('danger')}
htmlAttributes={{ 'aria-haspopup': 'dialog' }}
>
Danger Dialog
</FilledButton>
</div>
<Dialog
htmlAttributes={{
id: `${variant}-dialog`,
}}
onClose={handleClose}
isOpen={isOpen}
variant={variant}
>
<Dialog.Header>Header</Dialog.Header>
<Dialog.Subtitle>This is a subtitle</Dialog.Subtitle>
<Dialog.Description>This is a description</Dialog.Description>
<Dialog.Actions>
<OutlinedButton onClick={handleClose} variant="neutral" size="medium">
Secondary
</OutlinedButton>
<FilledButton onClick={handleClose} size="medium">
Primary
</FilledButton>
</Dialog.Actions>
</Dialog>
</>
);
};
export default Component;
```
### Warning
```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
import { Dialog, FilledButton, OutlinedButton } from '@neo4j-ndl/react';
import { useState } from 'react';
const Component = (props: { isOpen: boolean }) => {
const [isOpen, setIsOpen] = useState<boolean>(props.isOpen);
const handleClick = () => setIsOpen((prev) => !prev);
const handleClose = () => setIsOpen(false);
return (
<>
<div className="n-flex n-justify-center">
<FilledButton
onClick={handleClick}
htmlAttributes={{ 'aria-haspopup': 'dialog' }}
>
Open Dialog
</FilledButton>
</div>
<Dialog
onClose={handleClose}
isOpen={isOpen}
htmlAttributes={{
id: 'warning-dialog',
}}
variant="warning"
>
<Dialog.Header>Header</Dialog.Header>
<Dialog.Subtitle>This is a subtitle</Dialog.Subtitle>
<Dialog.Description>This is a description</Dialog.Description>
<Dialog.Actions>
<OutlinedButton onClick={handleClose} variant="neutral" size="medium">
Secondary
</OutlinedButton>
<FilledButton onClick={handleClose} size="medium">
Primary
</FilledButton>
</Dialog.Actions>
</Dialog>
</>
);
};
export default Component;
```