Skip to content

Commit

Permalink
Add Dialog component (#681)
Browse files Browse the repository at this point in the history
* Scaffold and some basic concepts

* Long content story and some props

* Tokens filled

* Return value and docs recommendations

* Some documentation and docs css

* Button to cancel dialog

* CSS comment

* Spacing token removed

* Unit tests

* Feedback processed

* Close modal by ID HTMLDialogElement

* Hidden dialog tests

* Update packages/css/src/dialog/README.md

Co-authored-by: Aram <[email protected]>

* Update packages/css/src/dialog/README.md

Co-authored-by: Aram <[email protected]>

* Failed test in jest succesful in interaction test

* Remove test

* Replaced close onclick function

* Feedback on flex-start

* You're right

* Add two cases we currently can't test

* Close button role removed

* Removed css file for storybook

* Removed css import

* Disabled background for this story

* Dialog test fix

* Disabled storybook test

* Remove commented out code

---------

Co-authored-by: Aram <[email protected]>
Co-authored-by: Aram Limpens <[email protected]>
  • Loading branch information
3 people authored Nov 21, 2023
1 parent 415afce commit be75d9f
Show file tree
Hide file tree
Showing 12 changed files with 449 additions and 0 deletions.
23 changes: 23 additions & 0 deletions packages/css/src/dialog/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Dialog

A Dialog allows the user to focus on one task or a piece of information, by popping-up and blocking the page content until the modal task is completed, or until the user dismisses the action.

## Guidelines

- Use Dialog sparingly, because it interrupts the user's workflow.
- Use Dialog for short and non-frequent tasks. For common tasks consider using the main flow.

## Keyboard Support

| key | function |
| :---------- | :----------------------------------------------------------- |
| Tab | Moves focus to next focusable element inside the dialog. |
| Shift + Tab | Moves focus to previous focusable element inside the dialog. |
| Escape | Closes the dialog. |

## References

- [HTMLDialogElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement)
- [Return value](https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/returnValue)
- [Modal & Nonmodal Dialogs: When (& When Not) to Use Them](https://www.nngroup.com/articles/modal-nonmodal-dialog/)
- [Patterns](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)
92 changes: 92 additions & 0 deletions packages/css/src/dialog/dialog.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* @license EUPL-1.2+
* Copyright (c) 2023 Gemeente Amsterdam
*/

@import "../../utils/breakpoint";

.amsterdam-dialog {
background-color: var(--amsterdam-dialog-background-color);
border: var(--amsterdam-dialog-border);
inset: 0;
max-inline-size: var(--amsterdam-dialog-max-inline-size);
padding-block: 0;
padding-inline: 0;
position: fixed;

/* no token because dialog does not inherit from any element */
&::backdrop {
background: #0006;
}
}

.amsterdam-dialog__form {
display: grid;
gap: var(--amsterdam-dialog-form-gap);
grid-template-rows: auto 1fr auto;
max-block-size: var(--amsterdam-dialog-form-max-block-size);
padding-block: var(--amsterdam-dialog-form-padding-block);
padding-inline: var(--amsterdam-dialog-form-padding-inline);
}

.amsterdam-dialog__article {
display: grid;
gap: 1.5rem; /* Until we have a consistent way of spacing text elements */
max-block-size: 100%; /* safari */
overflow-y: auto;
overscroll-behavior-y: contain;
padding-inline-end: var(--amsterdam-dialog-article-padding-inline-end);
}

.amsterdam-dialog__header {
align-items: flex-start;
display: flex;
gap: var(--amsterdam-dialog-header-gap);
}

@mixin reset {
-webkit-text-size-adjust: 100%;
}

.amsterdam-dialog__title {
color: var(--amsterdam-dialog-title-color);
flex: auto;
font-family: var(--amsterdam-dialog-title-font-family);
font-size: var(--amsterdam-dialog-title-narrow-font-size);
font-weight: var(--amsterdam-dialog-title-font-weight);
line-height: var(--amsterdam-dialog-title-line-height);

@media screen and (min-width: $amsterdam-breakpoint-typography) {
font-size: var(--amsterdam-dialog-title-wide-font-size);
}

@include reset;
}

.amsterdam-dialog__footer {
display: flex;
flex-direction: column;
grid-gap: var(--amsterdam-dialog-footer-gap);
padding-block: var(--amsterdam-dialog-footer-padding-block);

@media screen and (min-width: $amsterdam-breakpoint-medium) {
flex-direction: row;
justify-content: end;
}
}

.amsterdam-dialog__close {
background-color: var(--amsterdam-dialog-close-background-color);
border: 0;
cursor: pointer;
padding-block: 0;
padding-inline: 0;
}

.amsterdam-dialog__close svg {
fill: var(--amsterdam-dialog-close-fill);
}

.amsterdam-dialog__close:hover svg {
fill: var(--amsterdam-dialog-close-hover-fill);
}
1 change: 1 addition & 0 deletions packages/css/src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

/* Append here */
@import "./dialog/dialog";
@import "./image/image";
@import "./pagination/pagination";
@import "./accordion/accordion";
Expand Down
94 changes: 94 additions & 0 deletions packages/react/src/Dialog/Dialog.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { render, screen } from '@testing-library/react'
import { createRef } from 'react'
import { Dialog } from './Dialog'
import '@testing-library/jest-dom'

describe('Dialog', () => {
it('renders', () => {
render(<Dialog open />)

const component = screen.getByRole('dialog')

expect(component).toBeInTheDocument()
expect(component).toBeVisible()
})

it('renders a design system BEM class name', () => {
render(<Dialog />)

const component = screen.getByRole('dialog', { hidden: true })

expect(component).toHaveClass('amsterdam-dialog')
})

it('renders an additional class name', () => {
render(<Dialog className="extra" />)

const component = screen.getByRole('dialog', { hidden: true })

expect(component).toHaveClass('extra')

expect(component).toHaveClass('amsterdam-dialog')
})

it('supports ForwardRef in React', () => {
const ref = createRef<HTMLDialogElement>()

render(<Dialog ref={ref} />)

const component = screen.getByRole('dialog', { hidden: true })

expect(ref.current).toBe(component)
})

it('is not visible when open attribute is not used', () => {
render(<Dialog />)

const component = screen.getByRole('dialog', { hidden: true })

expect(component).toBeInTheDocument()
expect(component).not.toBeVisible()
})

it('renders a title', () => {
const { getByText } = render(<Dialog title="Dialog Title" />)

expect(getByText('Dialog Title')).toBeInTheDocument()
})

it('renders children', () => {
const { getByText } = render(<Dialog>Dialog Content</Dialog>)

expect(getByText('Dialog Content')).toBeInTheDocument()
})

it('renders actions when provided', () => {
const { getByText } = render(<Dialog actions={<button>Click Me</button>} />)

expect(getByText('Click Me')).toBeInTheDocument()
})

it('does not render actions when not provided', () => {
const { queryByText } = render(<Dialog />)

expect(queryByText('Click Me')).not.toBeInTheDocument()
})

it('renders DialogClose button', () => {
render(<Dialog open />)

const closeButton = screen.getByText('Sluiten')

expect(closeButton).toBeInTheDocument()
})

it.skip('can be closed with the Close button', () => {
// We currently can't test this because dialog isn't properly supported in jsdom
// https://github.com/jsdom/jsdom/issues/3294
})

it.skip('has no accessible content when it is closed', () => {
// We currently can't test this because dialog isn't properly supported in jsdom
// https://github.com/jsdom/jsdom/issues/3294
})
})
40 changes: 40 additions & 0 deletions packages/react/src/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* @license EUPL-1.2+
* Copyright (c) 2023 Gemeente Amsterdam
*/

import { CloseIcon } from '@amsterdam/design-system-react-icons'
import clsx from 'clsx'
import { DialogHTMLAttributes, ForwardedRef, forwardRef, PropsWithChildren, ReactNode } from 'react'
import { Icon } from '../Icon'
import { VisuallyHidden } from '../VisuallyHidden'

export interface DialogProps extends PropsWithChildren<DialogHTMLAttributes<HTMLDialogElement>> {
actions?: ReactNode
}

// TODO: Replace with ActionButton
const DialogClose = forwardRef(({ ...restProps }, ref: ForwardedRef<HTMLButtonElement>) => (
<button ref={ref} className="amsterdam-dialog__close" formNoValidate {...restProps}>
<VisuallyHidden>Sluiten</VisuallyHidden>
<Icon svg={CloseIcon} size="level-5" />
</button>
))

export const Dialog = forwardRef(
({ children, className, title, actions, ...restProps }: DialogProps, ref: ForwardedRef<HTMLDialogElement>) => (
<dialog {...restProps} ref={ref} className={clsx('amsterdam-dialog', className)}>
<form method="dialog" className="amsterdam-dialog__form">
<header className="amsterdam-dialog__header">
<span className="amsterdam-dialog__title">{title}</span>
<DialogClose />
</header>
<article className="amsterdam-dialog__article">{children}</article>
{actions && <footer className="amsterdam-dialog__footer">{actions}</footer>}
</form>
</dialog>
),
)

Dialog.displayName = 'Dialog'
DialogClose.displayName = 'DialogClose'
3 changes: 3 additions & 0 deletions packages/react/src/Dialog/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# React Modal component

[Modal documentation](../../../css/src/modal/README.md)
2 changes: 2 additions & 0 deletions packages/react/src/Dialog/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Dialog } from './Dialog'
export type { DialogProps } from './Dialog'
1 change: 1 addition & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

/* Append here */
export * from './Dialog'
export * from './Image'
export * from './Pagination'
export * from './Screen'
Expand Down
47 changes: 47 additions & 0 deletions proprietary/tokens/src/components/amsterdam/dialog.tokens.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"amsterdam": {
"dialog": {
"background-color": { "value": "{amsterdam.color.primary-white}" },
"border": { "value": "0" },
"max-inline-size": { "value": "min(87.69vw, 45rem)" },
"title": {
"color": { "value": "{amsterdam.color.primary-black}" },
"font-family": { "value": "{amsterdam.typography.font-family}" },
"font-weight": { "value": "{amsterdam.typography.font-weight.bold}" },
"line-height": { "value": "{amsterdam.typography.text-level.5.line-height}" },
"narrow": {
"font-size": { "value": "{amsterdam.typography.text-level.5.narrow.font-size}" }
},
"wide": {
"font-size": { "value": "{amsterdam.typography.text-level.5.wide.font-size}" }
}
},
"backdrop": {
"background": { "value": "#0006" }
},
"close": {
"background-color": { "value": "transparent" },
"fill": { "value": "{amsterdam.color.primary-black}" },
"hover": {
"fill": { "value": "{amsterdam.color.primary-blue}" }
}
},
"form": {
"gap": { "value": "1.5rem" },
"padding-block": { "value": "clamp(1.5rem, calc(1.5rem + ((1vw - 0.5337rem) * 2.1448)), 2.5rem)" },
"padding-inline": { "value": "clamp(1.5rem, calc(1.5rem + ((1vw - 0.5337rem) * 2.1448)), 2.5rem)" },
"max-block-size": { "value": "75vh" }
},
"article": {
"padding-inline-end": { "value": "1.5rem" }
},
"header": {
"gap": { "value": "1rem" }
},
"footer": {
"gap": { "value": "1rem" },
"padding-block": { "value": "1.5rem 0" }
}
}
}
}
6 changes: 6 additions & 0 deletions storybook/storybook-react/config/preview-body.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,10 @@
margin: -16px; /* stylelint-disable-line */
padding: 16px; /* stylelint-disable-line */
}

/* Workaround to mimic dark background for dialog Canvas blocks */
.amsterdam-dialog-story {
background-color: #333 !important;
transition: background-color 0.3s;
}
</style>
19 changes: 19 additions & 0 deletions storybook/storybook-react/src/Dialog/Dialog.docs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Canvas, Controls, Markdown, Meta, Primary } from "@storybook/blocks";
import * as DialogStories from "./Dialog.stories.tsx";
import README from "../../../../packages/css/src/dialog/README.md?raw";

<Meta of={DialogStories} />

<Markdown>{README}</Markdown>

<Primary />

<Controls />

## Scroll Content

<Canvas of={DialogStories.ScrollContent} className="amsterdam-dialog-story" />

## Open Dialog Button

<Canvas of={DialogStories.ShowDialog} />
Loading

0 comments on commit be75d9f

Please sign in to comment.