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

feat(components): add Alert Dialog component #235

Merged
merged 1 commit into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
20
20.11.1
83 changes: 83 additions & 0 deletions packages/storybook/src/alert-dialog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Alert Dialog

An Alert Dialog component following the NL Design System guidelines. It allows users to focus on one task or piece of information by popping up and blocking the page content until the modal task is completed or until the user dismisses the action.

## When to use an Alert Dialog

- To display important information that requires user confirmation
- To present choices that cannot be undone
- To warn users about critical actions
- For short and non-frequent tasks that require immediate attention

## Guidelines

- Use dialogs sparingly because they interrupt the user's workflow
- Use a dialog for short and non-frequent tasks. Consider using the main flow for regular tasks
- Keep content concise and focused on a single task or piece of information
- Ensure the dialog title clearly describes its purpose
- Use action-oriented button labels that describe the action being taken

## Anatomy

An Alert Dialog consists of:

- A header with a title and close button
- Content area for information or form elements
- A footer with one or more action buttons
- A semi-transparent backdrop that blocks interaction with the page

## Keyboard Support

| Key | Function |
| ----------- | --------------------------------------------------------------- |
| Tab | Moves focus to the next focusable element inside the dialog |
| Shift + Tab | Moves focus to the previous focusable element inside the dialog |
| Escape | Closes the dialog |

## Accessibility

- The dialog blocks interaction with the page content when open
- Focus is trapped inside the dialog when open
- The dialog can be closed using the Escape key
- All interactive elements are keyboard accessible
- The dialog has a clear visual hierarchy
- The close button is always available in the header
- ARIA attributes are properly set for screen readers

## Best Practices

- Use clear, action-oriented labels for buttons
- Keep content concise and to the point
- Ensure the title clearly describes the dialog's content
- Use primary action buttons for the main action
- Place 'Cancel' button on the left and primary action on the right
- Ensure sufficient contrast for all text elements
- Provide clear feedback when actions are taken

## Variants

1. Default

- Standard variant with a single close button
- Suitable for informative messages
- Uses primary action button for closing

2. Custom Footer
- Variant with multiple action buttons
- Suitable for confirmations or choices
- Supports custom button configurations

## Technical Details

The Alert Dialog is built using the native HTML `<dialog>` element, which provides:

- Built-in modal behavior
- Focus management
- Keyboard interaction
- Proper backdrop handling

## References

- [HTMLDialogElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement)
- [Modal & Nonmodal Dialogs: When & When Not to Use Them](https://www.nngroup.com/articles/modal-nonmodal-dialog/)
- [ARIA Dialog Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)
224 changes: 224 additions & 0 deletions packages/storybook/src/alert-dialog.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Button, Paragraph } from '@utrecht/component-library-react/dist/css-module';
import { useRef } from 'react';
import '@tilburg/design-tokens/dist/theme.css';
import { AlertDialog } from './alert-dialog';

const meta = {
title: 'React/Alert Dialog',
component: AlertDialog,
parameters: {
layout: 'centered',
docs: {
description: {
component: `
# Alert Dialog

An Alert Dialog component following the NL Design System guidelines. It allows users to focus on one task or piece of information by popping up and blocking the page content until the modal task is completed or until the user dismisses the action.

## When to use an Alert Dialog

- To display important information that requires user confirmation
- To present choices that cannot be undone
- To warn users about critical actions
- For short and non-frequent tasks that require immediate attention

## Guidelines

- Use dialogs sparingly because they interrupt the user's workflow
- Use a dialog for short and non-frequent tasks. Consider using the main flow for regular tasks
- Keep content concise and focused on a single task or piece of information
- Ensure the dialog title clearly describes its purpose
- Use action-oriented button labels that describe the action being taken

## Accessibility

- The dialog blocks interaction with the page content when open
- Focus is trapped inside the dialog when open
- The dialog can be closed using the Escape key
- All interactive elements are keyboard accessible
- The dialog has a clear visual hierarchy
- The close button is always available in the header
- ARIA attributes are properly set for screen readers
`,
},
},
bugs: 'https://github.com/nl-design-system/tilburg/labels/component%2Falert-dialog',
design: {
type: 'figma',
url: 'https://www.figma.com/file/6RXnW5Zc1qBXlmD8YAm8lJ/Open-Tilburg?type=design&node-id=1-2&mode=design&t=YPa5Qd7RBECXALYg-0',
},
},
argTypes: {
title: {
control: 'text',
description: 'The title of the dialog',
},
children: {
control: 'text',
description: 'The content of the dialog',
},
customFooter: {
control: 'boolean',
description: 'Optional footer with custom buttons',
},
},
tags: ['autodocs'],
} satisfies Meta<typeof AlertDialog>;

export default meta;
type Story = StoryObj<typeof AlertDialog>;

export const Default: Story = {
name: 'Default',
args: {
title: 'Example Alert Dialog',
children: (
<>
<Paragraph>
<strong>Agreement</strong>
<br />A formal agreement or arrangement between two or more parties.
</Paragraph>
<Paragraph>
<strong>Policy Document</strong>
<br />
Document used to establish policies or guidelines.
</Paragraph>
</>
),
},
parameters: {
docs: {
source: {
language: 'tsx',
code: `
import { AlertDialog, Button } from '@tilburg/component-library-react';

function MyComponent() {
const dialogRef = useRef<HTMLDialogElement>(null);

const openDialog = () => {
dialogRef.current?.showModal();
};

return (
<>
<Button onClick={openDialog}>Open Dialog</Button>
<AlertDialog
ref={dialogRef}
title="Example Alert Dialog"
>
<Paragraph>
<strong>Agreement</strong><br />
A formal agreement or arrangement between two or more parties.
</Paragraph>
<Paragraph>
<strong>Policy Document</strong><br />
Document used to establish policies or guidelines.
</Paragraph>
</AlertDialog>
</>
);
}`,
},
},
},
render: (args) => {
const dialogRef = useRef<HTMLDialogElement>(null);

const openModal = () => {
dialogRef.current?.showModal();
};

return (
<>
<Button onClick={openModal}>Open Dialog</Button>
<AlertDialog {...args} ref={dialogRef} />
</>
);
},
};

export const WithCustomFooter: Story = {
name: 'With Custom Footer',
args: {
title: 'Alert Dialog with Custom Footer',
children: <Paragraph>This alert dialog has a custom footer with multiple buttons.</Paragraph>,
},
parameters: {
docs: {
source: {
language: 'tsx',
code: `
import { AlertDialog, Button } from '@tilburg/component-library-react';

function MyComponent() {
const dialogRef = useRef<HTMLDialogElement>(null);

const openDialog = () => {
dialogRef.current?.showModal();
};

const closeDialog = () => {
dialogRef.current?.close();
};

return (
<>
<Button onClick={openDialog}>Open Dialog</Button>
<AlertDialog
ref={dialogRef}
title="Alert Dialog with Custom Footer"
customFooter={
<div className="tilburg-modal__footer">
<Button appearance="subtle-button" onClick={closeDialog}>
Cancel
</Button>
<Button appearance="primary-action-button" onClick={closeDialog}>
Save
</Button>
</div>
}
>
<Paragraph>
This alert dialog has a custom footer with multiple buttons.
</Paragraph>
</AlertDialog>
</>
);
}`,
},
},
},
render: (args) => {
const dialogRef = useRef<HTMLDialogElement>(null);

const openModal = () => {
dialogRef.current?.showModal();
};

const closeModal = () => {
dialogRef.current?.close();
};

return (
<>
<Button onClick={openModal}>Open Dialog</Button>
<AlertDialog
{...args}
ref={dialogRef}
customFooter={
<div className="tilburg-modal__footer">
<Button appearance="subtle-button" onClick={closeModal}>
Cancel
</Button>
<Button appearance="primary-action-button" onClick={closeModal}>
Save
</Button>
</div>
}
/>
</>
);
},
};
67 changes: 67 additions & 0 deletions packages/storybook/src/alert-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Button, Heading } from '@utrecht/component-library-react/dist/css-module';
import clsx from 'clsx';
import React, { useState } from 'react';
import './styles/modal.css';

const CloseIcon = () => (
<svg aria-hidden="true" width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M.293.293a1 1 0 0 1 1.414 0L7 5.586 12.293.293a1 1 0 1 1 1.414 1.414L8.414 7l5.293 5.293a1 1 0 0 1-1.414 1.414L7 8.414l-5.293 5.293a1 1 0 0 1-1.414-1.414L5.586 7 .293 1.707a1 1 0 0 1 0-1.414Z"
fill="currentColor"
/>
</svg>
);

export interface AlertDialogProps {
id?: string;
title: string;
children: React.ReactNode;
customFooter?: React.ReactNode;
}

export const AlertDialog = React.forwardRef<HTMLDialogElement, AlertDialogProps>(
({ id, title, children, customFooter }, ref) => {
const [isOpen, setIsOpen] = useState(false);

const onCloseHandler = () => {
setIsOpen(false);
(ref as React.RefObject<HTMLDialogElement>)?.current?.close();
};

const onBackdropClick = (event: React.MouseEvent<HTMLDialogElement>) => {
if (event.target !== (ref as React.RefObject<HTMLDialogElement>).current) {
return;
}
setIsOpen(false);
(ref as React.RefObject<HTMLDialogElement>)?.current?.close();
};

const _CLASSES = clsx('tilburg-modal', isOpen && 'open');

return (
<dialog id={id} className={_CLASSES} ref={ref} onClick={onBackdropClick}>
<div className="tilburg-modal__header">
<Heading level={2}>{title}</Heading>
<button className="tilburg-modal__close-button" onClick={onCloseHandler}>
<CloseIcon />
Close
</button>
</div>
<div className="tilburg-modal__content">{children}</div>
{customFooter ? (
customFooter
) : (
<div className="tilburg-modal__footer">
<Button appearance="primary-action-button" onClick={onCloseHandler}>
Close
</Button>
</div>
)}
</dialog>
);
},
);

AlertDialog.displayName = 'AlertDialog';
Loading