UNPKG

@neo4j-ndl/react

Version:

React implementation of Neo4j Design System

486 lines (412 loc) 17.8 kB
# 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; ```