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

Add Dialog component #681

Merged
merged 34 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
c297408
Scaffold and some basic concepts
dlnr Oct 23, 2023
24738c6
Long content story and some props
dlnr Oct 25, 2023
809e4d1
Tokens filled
dlnr Oct 26, 2023
48e3895
Return value and docs recommendations
dlnr Oct 26, 2023
7b81e91
Merged with main
dlnr Nov 7, 2023
654259a
Some documentation and docs css
dlnr Nov 8, 2023
473fd73
Button to cancel dialog
dlnr Nov 8, 2023
e4951a6
Merge branch 'main' of https://github.com/Amsterdam/design-system int…
dlnr Nov 8, 2023
0d9f119
CSS comment
dlnr Nov 8, 2023
f9205e9
Spacing token removed
dlnr Nov 9, 2023
dcb86fa
Unit tests
dlnr Nov 9, 2023
9a0bb0d
Merge branch 'main' of https://github.com/Amsterdam/design-system int…
dlnr Nov 9, 2023
ddbd78f
Merge branch 'main' of https://github.com/Amsterdam/design-system int…
dlnr Nov 14, 2023
1bbdafb
Feedback processed
dlnr Nov 14, 2023
f652248
Close modal by ID HTMLDialogElement
dlnr Nov 14, 2023
192a5dd
Hidden dialog tests
dlnr Nov 14, 2023
d9554cc
Update packages/css/src/dialog/README.md
dlnr Nov 16, 2023
9508c43
Update packages/css/src/dialog/README.md
dlnr Nov 16, 2023
352fc83
Failed test in jest succesful in interaction test
dlnr Nov 16, 2023
e9459eb
Remove test
dlnr Nov 16, 2023
1463d62
Replaced close onclick function
dlnr Nov 16, 2023
edd5748
Feedback on flex-start
dlnr Nov 16, 2023
0baac51
You're right
dlnr Nov 16, 2023
e920bff
Merge branch 'main' of https://github.com/Amsterdam/design-system int…
dlnr Nov 16, 2023
746b2c8
Add two cases we currently can't test
alimpens Nov 16, 2023
cbfcc9f
Merge branch 'feature/DES-253-modal-dialog' of https://github.com/Ams…
alimpens Nov 16, 2023
3fdb41f
Close button role removed
dlnr Nov 16, 2023
fc5f089
Removed css file for storybook
dlnr Nov 17, 2023
f46d959
Removed css import
dlnr Nov 17, 2023
ea0ffb2
Disabled background for this story
dlnr Nov 17, 2023
2e20d04
Dialog test fix
dlnr Nov 17, 2023
4f49e14
Disabled storybook test
dlnr Nov 21, 2023
7ae480b
Remove commented out code
dlnr Nov 21, 2023
c0130e7
Merge branch 'develop' into feature/DES-253-modal-dialog
alimpens Nov 21, 2023
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
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()
})

alimpens marked this conversation as resolved.
Show resolved Hide resolved
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
})
})
alimpens marked this conversation as resolved.
Show resolved Hide resolved
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