Skip to content

Commit

Permalink
feat!: Add Action Group component, e.g. to wrap Dialog buttons in (#1592
Browse files Browse the repository at this point in the history
)

Co-authored-by: Niels Roozemond <n.roozemond@amsterdam.nl>
  • Loading branch information
VincentSmedinga and dlnr authored Oct 1, 2024
1 parent 3734e6e commit d0ea054
Show file tree
Hide file tree
Showing 21 changed files with 308 additions and 173 deletions.
14 changes: 14 additions & 0 deletions packages/css/src/components/action-group/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!-- @license CC0-1.0 -->

# Action Group

Groups one or more related actions and manages their layout.

## How to use

- Both a [Button](?path=/docs/components-buttons-button--docs) and a [Link](?path=/docs/components-navigation-link--docs) can provide an ‘action’ in this context.
- If two or more buttons or links are in a row, put the one for the primary action first and the other buttons behind it.
- Sighted users will read the primary action first, in line with the natural reading order.
The same goes for users of screen readers, who will hear the primary action first, and users of a keyboard, who will focus the primary action first.
- Also, this approach keeps the order of buttons consistent on both narrow and wide screens: if the buttons do not fit next to each other, they get stacked vertically with the primary action on top.
- Replace the default ’group’ role with `role="toolbar"` for button toolbars.
15 changes: 15 additions & 0 deletions packages/css/src/components/action-group/action-group.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/

.ams-action-group {
align-items: baseline;
display: inline-flex;
flex-wrap: wrap;
gap: var(--ams-action-group-gap);

> * {
flex: auto;
}
}
7 changes: 3 additions & 4 deletions packages/css/src/components/button/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@ Allows the user to perform actions and operate the user interface.

## Guidelines

- A short label text that describes the function of the button.
- A clear contrasting colour scheme so that it is easy to recognize and quickly locate.
- Use the correct type of button for the corresponding application.
For example, a button within a form must always be of the type `submit`.
- Choose a short label that describes the function of the button.
- Use the correct type of button for the corresponding application, e.g. `type="submit"` for the primary form button.
- Make sure one can operate a button through a keyboard.
- Wrap 2 or more consecutive buttons and/or links in an [Action Group](?path=/docs/components-buttons-action-group--docs).
- Primary, secondary and tertiary buttons can be used side by side.
Skipping levels is allowed.

Expand Down
8 changes: 1 addition & 7 deletions packages/css/src/components/dialog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,7 @@ A Dialog allows the user to focus on one task or a piece of information by poppi
- 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.

## The order of buttons

If your Dialog needs more than one button, put the one for the primary action first and the other buttons behind it.
Sighted users will read the primary action first, in line with the natural reading order.
The same goes for users of screen readers, who will hear the primary action first, and users of a keyboard, who will focus the primary action first.
Also, this approach keeps the order of buttons consistent on both narrow and wide screens: if the buttons do not fit next to each other, they get stacked vertically with the primary action on top.
- Wrap multiple buttons in an [Action Group](https://designsystem.amsterdam/?path=/docs/components-buttons-action-group--docs).

## Keyboard support

Expand Down
19 changes: 3 additions & 16 deletions packages/css/src/components/dialog/dialog.scss
Original file line number Diff line number Diff line change
Expand Up @@ -30,27 +30,14 @@ so do not apply these styles without an `open` attribute. */
}
}

.ams-dialog__body {
overflow-y: auto;
overscroll-behavior-y: contain;
}

.ams-dialog__header {
align-items: flex-start;
display: flex;
gap: var(--ams-dialog-header-gap);
justify-content: space-between;
}

.ams-dialog__footer {
display: flex;
flex-wrap: wrap; // [1]
gap: var(--ams-dialog-footer-gap);
margin-inline-end: auto; // [1]

> * {
flex: auto; // [1]
}
.ams-dialog__body {
overflow-y: auto;
overscroll-behavior-y: contain;
}

// [1] This combination stacks the buttons vertically and stretches them, until they fit next to each other.
1 change: 1 addition & 0 deletions packages/css/src/components/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

/* Append here */
@import "./action-group/action-group";
@import "./breakout/breakout";
@import "./hint/hint";
@import "./password-input/password-input";
Expand Down
1 change: 1 addition & 0 deletions packages/css/src/components/link/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Use a link in the following cases:
- To navigate to another website (see [External links](#external-links))
- To navigate to an element on the same page
- To link to emails or phone numbers (start the link with `mailto:` or `tel:`)
- Wrap 2 or more consecutive buttons and/or links in an [Action Group](https://designsystem.amsterdam/?path=/docs/components-buttons-action-group--docs).

A link is a navigation component.
Use a button instead of a link when an action is desired.
Expand Down
41 changes: 41 additions & 0 deletions packages/react/src/ActionGroup/ActionGroup.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { render, screen } from '@testing-library/react'
import { createRef } from 'react'
import { ActionGroup } from './ActionGroup'
import '@testing-library/jest-dom'

describe('Action Group', () => {
it('renders', () => {
render(<ActionGroup />)

const component = screen.getByRole('group')

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

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

const component = screen.getByRole('group')

expect(component).toHaveClass('ams-action-group')
})

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

const component = screen.getByRole('group')

expect(component).toHaveClass('ams-action-group extra')
})

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

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

const component = screen.getByRole('group')

expect(ref.current).toBe(component)
})
})
20 changes: 20 additions & 0 deletions packages/react/src/ActionGroup/ActionGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/

import clsx from 'clsx'
import { forwardRef } from 'react'
import type { ForwardedRef, HTMLAttributes, PropsWithChildren } from 'react'

export type ActionGroupProps = PropsWithChildren<HTMLAttributes<HTMLDivElement>>

export const ActionGroup = forwardRef(
({ children, className, ...restProps }: ActionGroupProps, ref: ForwardedRef<HTMLDivElement>) => (
<div {...restProps} ref={ref} className={clsx('ams-action-group', className)} role="group">
{children}
</div>
),
)

ActionGroup.displayName = 'ActionGroup'
5 changes: 5 additions & 0 deletions packages/react/src/ActionGroup/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!-- @license CC0-1.0 -->

# React Action Group component

[Action Group documentation](../../../css/src/components/action-group/README.md)
2 changes: 2 additions & 0 deletions packages/react/src/ActionGroup/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { ActionGroup } from './ActionGroup'
export type { ActionGroupProps } from './ActionGroup'
2 changes: 1 addition & 1 deletion packages/react/src/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { IconButton } from '../IconButton'
export type DialogProps = {
/** The label for the button that dismisses the Dialog. */
closeButtonLabel?: string
/** The button(s) in the footer. Start with a primary button. */
/** Content for the footer, often one Button or an Action Group containing more of them. */
footer?: ReactNode
/** The text for the Heading. */
heading: string
Expand Down
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 './ActionGroup'
export * from './Breakout'
export * from './Hint'
export * from './PasswordInput'
Expand Down
2 changes: 1 addition & 1 deletion plop-templates/react.test.tsx.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { createRef } from 'react'
import { {{pascalCase name}} } from './{{pascalCase name}}'
import '@testing-library/jest-dom'

describe('{{sentenceCase name}}', () => {
describe('{{pascalCase name}}', () => {
it('renders', () => {
{{#if role}}
render(<{{pascalCase name}} />)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"ams": {
"action-group": {
"gap": { "value": "{ams.space.md}" }
}
}
}
3 changes: 0 additions & 3 deletions proprietary/tokens/src/components/ams/dialog.tokens.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@
"padding-inline": { "value": "{ams.space.grid.lg}" },
"header": {
"gap": { "value": "{ams.space.md}" }
},
"footer": {
"gap": { "value": "{ams.space.md}" }
}
}
}
Expand Down
27 changes: 27 additions & 0 deletions storybook/src/components/ActionGroup/ActionGroup.docs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{/* @license CC0-1.0 */}

import { Canvas, Markdown, Meta, Primary } from "@storybook/blocks";
import * as ActionGroupStories from "./ActionGroup.stories.tsx";
import README from "../../../../packages/css/src/components/action-group/README.md?raw";

<Meta of={ActionGroupStories} />

<Markdown>{README}</Markdown>

<Primary />

## Examples

### Stacked

If the Buttons don’t fit next to each other, they will automatically stack vertically and stretch to the full width.
This can occur in a narrow Dialog, with long labels, a large text size, or when zooming in.
Resize the pink rectangle to see this in action.

<Canvas of={ActionGroupStories.Stacked} />

### With Link

An action that involves navigation should be a link.

<Canvas of={ActionGroupStories.WithLink} />
43 changes: 43 additions & 0 deletions storybook/src/components/ActionGroup/ActionGroup.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/

import { Button, Link } from '@amsterdam/design-system-react'
import { ActionGroup } from '@amsterdam/design-system-react/src'
import { Meta, StoryObj } from '@storybook/react'

const meta = {
title: 'Components/Buttons/Action Group',
component: ActionGroup,
args: {
children: [<Button>Doorgaan</Button>, <Button variant="tertiary">Stoppen</Button>],
},
} satisfies Meta<typeof ActionGroup>

export default meta

type Story = StoryObj<typeof meta>

export const Default: Story = {}

export const Stacked: Story = {
args: {
children: [<Button>Adres wijzigen</Button>, <Button variant="secondary">Adres verwijderen</Button>],
className: 'ams-resize-horizontal',
style: {
inlineSize: '16rem',
},
},
}

export const WithLink: Story = {
args: {
children: [
<Button key={1}>Bewerken</Button>,
<Link download href="#" key={2} variant="standalone">
Downloaden
</Link>,
],
},
}
44 changes: 20 additions & 24 deletions storybook/src/components/Dialog/Dialog.docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,38 +14,34 @@ import README from "../../../../packages/css/src/components/dialog/README.md?raw

## Examples

### Form in a Dialog
### Open and close

Set `method="dialog"` when using a form in Dialog.
This closes the Dialog when submitting the form.
Pass the submit Button to the `footer` prop,
and link it to the form by passing its `id` to the Buttons `form` attribute.
The Dialog returns the value of the submit Button, so you can check which Button was clicked.
For more information, see [Handling the return value from the dialog (MDN)](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog#handling_the_return_value_from_the_dialog).

<Canvas of={DialogStories.FormDialog} />

### With scrollbar
To open the Dialog, use `Dialog.open(dialogId)` from the React package.
To close the Dialog, use either `Dialog.close`, or a `<form>` as in the following example.

Content taller than the dialog itself will scroll.
<Canvas of={DialogStories.OpenAndClose} />

<Canvas of={DialogStories.WithScrollbar} />
### Asking to confirm

### Trigger Button
Use a `<form>` when asking to confirm an action, e.g. through ‘OK’ and ‘Cancel’ buttons.
Add `method="dialog"` to let the browser close the Dialog automatically when the form is submitted.

Click or tap this Button to open the Dialog.
Wrap the buttons in an [Action Group](?path=/docs/components-buttons-action-group--docs) and place it in the `footer`.
This ensures correct whitespace and scrolling behaviour.
At the same time, this will position the buttons outside the `form` element.
Create an `id` for the form and add it to the submit Button’s `form` attribute to connect the two.

<Canvas of={DialogStories.TriggerButton} />
If the Action Group must be in the `form`, implement the whitespace and scrolling behaviour as well.
Add a medium bottom margin (`ams-mb--md`) to the element before it.
Make sure the content of the form scrolls if necessary, while the Action Group is visible at the bottom at all times.

#### Utility functions
The form returns the `value` of the submit Button, which allows inferring which Button the user clicked.
For more information, see [Handling the return value from the dialog (MDN)](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog#handling_the_return_value_from_the_dialog).

To open the Dialog, use `Dialog.open(id)` from the React package.
Pass the Dialog’s `id` to the function to select it.
To close the Dialog, use `Dialog.close`.
<Canvas of={DialogStories.AskingToConfirm} />

### Vertically stacked Buttons
### Tall content will scroll

If the Buttons don’t fit next to each other, they will stack vertically and stretch to the full width.
This can occur with a narrow Dialog, long Button labels, a large text size, or when zooming in.
Content that doesn’t fit entirely in the Dialog will scroll.

<Canvas of={DialogStories.VerticalButtons} />
<Canvas of={DialogStories.WithScrollbar} />
Loading

0 comments on commit d0ea054

Please sign in to comment.