# 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;
```
