From d7316e81cd424f79f9bd655265d1c9b41296fecf Mon Sep 17 00:00:00 2001 From: Aram <37216945+alimpens@users.noreply.github.com> Date: Fri, 24 May 2024 13:30:40 +0200 Subject: [PATCH] feat!: Add invalid prop to Field Set and update Field and Field Set docs (#1237) Co-authored-by: Vincent Smedinga --- .../css/src/components/field-set/README.md | 17 +++ .../src/components/field-set/field-set.scss | 48 +++++++ packages/css/src/components/field/README.md | 2 +- .../css/src/components/fieldset/README.md | 18 --- .../css/src/components/fieldset/fieldset.scss | 35 ----- packages/css/src/components/index.scss | 2 +- .../FieldSet.test.tsx} | 20 +-- packages/react/src/FieldSet/FieldSet.tsx | 30 ++++ packages/react/src/FieldSet/README.md | 5 + packages/react/src/FieldSet/index.ts | 2 + packages/react/src/Fieldset/Fieldset.tsx | 24 ---- packages/react/src/Fieldset/README.md | 5 - packages/react/src/Fieldset/index.ts | 2 - packages/react/src/index.ts | 2 +- ...dset.tokens.json => field-set.tokens.json} | 13 +- storybook/src/components/Field/Field.docs.mdx | 9 ++ .../src/components/Field/Field.stories.tsx | 30 ++-- .../src/components/FieldSet/FieldSet.docs.mdx | 50 +++++++ .../components/FieldSet/FieldSet.stories.tsx | 130 ++++++++++++++++++ .../src/components/Fieldset/Fieldset.docs.mdx | 19 --- .../components/Fieldset/Fieldset.stories.tsx | 34 ----- storybook/src/components/Radio/Radio.docs.mdx | 2 +- .../src/components/Radio/Radio.stories.tsx | 6 +- 23 files changed, 339 insertions(+), 166 deletions(-) create mode 100644 packages/css/src/components/field-set/README.md create mode 100644 packages/css/src/components/field-set/field-set.scss delete mode 100644 packages/css/src/components/fieldset/README.md delete mode 100644 packages/css/src/components/fieldset/fieldset.scss rename packages/react/src/{Fieldset/Fieldset.test.tsx => FieldSet/FieldSet.test.tsx} (64%) create mode 100644 packages/react/src/FieldSet/FieldSet.tsx create mode 100644 packages/react/src/FieldSet/README.md create mode 100644 packages/react/src/FieldSet/index.ts delete mode 100644 packages/react/src/Fieldset/Fieldset.tsx delete mode 100644 packages/react/src/Fieldset/README.md delete mode 100644 packages/react/src/Fieldset/index.ts rename proprietary/tokens/src/components/ams/{fieldset.tokens.json => field-set.tokens.json} (53%) create mode 100644 storybook/src/components/FieldSet/FieldSet.docs.mdx create mode 100644 storybook/src/components/FieldSet/FieldSet.stories.tsx delete mode 100644 storybook/src/components/Fieldset/Fieldset.docs.mdx delete mode 100644 storybook/src/components/Fieldset/Fieldset.stories.tsx diff --git a/packages/css/src/components/field-set/README.md b/packages/css/src/components/field-set/README.md new file mode 100644 index 0000000000..6120ee5d5a --- /dev/null +++ b/packages/css/src/components/field-set/README.md @@ -0,0 +1,17 @@ + + +# Field Set + +A component to group related form inputs. + +## Guidelines + +- Use Field Set when you need to show a relationship between multiple form inputs. For example, you may need to group a set of text inputs into a single Field Set when asking for an address. + +## Relevant WCAG Requirements + +- [WCAG 1.3.5](https://www.w3.org/WAI/WCAG22/Understanding/identify-input-purpose.html): Field Set labels the purpose of a group of inputs. + +## References + +- [Providing a description for groups of form controls using fieldset and legend elements](https://www.w3.org/WAI/WCAG22/Techniques/html/H71) diff --git a/packages/css/src/components/field-set/field-set.scss b/packages/css/src/components/field-set/field-set.scss new file mode 100644 index 0000000000..f90ffc60d0 --- /dev/null +++ b/packages/css/src/components/field-set/field-set.scss @@ -0,0 +1,48 @@ +/** + * @license EUPL-1.2+ + * Copyright Gemeente Amsterdam + */ + +@import "../../common/hyphenation"; +@import "../../common/text-rendering"; + +@mixin reset { + border: 0; + margin-inline: 0; + padding-block: 0; + padding-inline: 0; +} + +.ams-field-set { + break-inside: avoid; + + @include reset; +} + +.ams-field-set--invalid { + border-inline-start: var(--ams-field-set-invalid-border-inline-start); + padding-inline-start: var(--ams-field-set-invalid-padding-inline-start); +} + +@mixin reset-legend { + float: left; // [1] + padding-inline: 0; + width: 100%; // [1] +} + +// [1] This combination allows the fieldset border to go around the legend, instead of through it. + +.ams-field-set__legend { + color: var(--ams-field-set-legend-color); + font-family: var(--ams-field-set-legend-font-family); + font-size: var(--ams-field-set-legend-font-size); + font-weight: var(--ams-field-set-legend-font-weight); + line-height: var(--ams-field-set-legend-line-height); + margin-block-end: var( + --ams-field-set-legend-margin-block-end + ); /* Because of a bug in Chrome we can’t use display grid or flex for this gap */ + + @include hyphenation; + @include text-rendering; + @include reset-legend; +} diff --git a/packages/css/src/components/field/README.md b/packages/css/src/components/field/README.md index 581cad51f1..9f76f2e374 100644 --- a/packages/css/src/components/field/README.md +++ b/packages/css/src/components/field/README.md @@ -6,4 +6,4 @@ Wraps a single input and its related elements. May indicate that the input has a ## Guidelines -Only use Field to wrap a single input. Use [Fieldset](/docs/components-forms-fieldset--docs) to wrap multiple inputs. +Only use Field to wrap a single input. Use [Field Set](/docs/components-forms-field-set--docs) to wrap multiple inputs. diff --git a/packages/css/src/components/fieldset/README.md b/packages/css/src/components/fieldset/README.md deleted file mode 100644 index 18331daa6d..0000000000 --- a/packages/css/src/components/fieldset/README.md +++ /dev/null @@ -1,18 +0,0 @@ - - -# Fieldset - -A component to group related form inputs. - -## Guidelines - -- Use Fieldset when you need to show a relationship between multiple form inputs. For example, you may need to group a set of text inputs into a single fieldset when asking for an address. -- When grouping radio inputs, use `role="radiogroup"` on Fieldset to have this grouping explicitly announced as a radio group. Fieldset has a default role of `group`. - -## Relevant WCAG Requirements - -- [WCAG 1.3.5](https://www.w3.org/WAI/WCAG22/Understanding/identify-input-purpose.html): Fieldset labels the purpose of a group of inputs. - -## References - -- [Providing a description for groups of form controls using fieldset and legend elements](https://www.w3.org/WAI/WCAG22/Techniques/html/H71) diff --git a/packages/css/src/components/fieldset/fieldset.scss b/packages/css/src/components/fieldset/fieldset.scss deleted file mode 100644 index 5091408cc5..0000000000 --- a/packages/css/src/components/fieldset/fieldset.scss +++ /dev/null @@ -1,35 +0,0 @@ -/** - * @license EUPL-1.2+ - * Copyright Gemeente Amsterdam - */ - -@import "../../common/hyphenation"; -@import "../../common/text-rendering"; - -@mixin reset { - border: 0; - margin-inline: 0; - padding-block: 0; - padding-inline: 0; -} - -.ams-fieldset { - @include reset; -} - -@mixin reset-legend { - padding-inline: 0; -} - -.ams-fieldset__legend { - color: var(--ams-fieldset-legend-color); - font-family: var(--ams-fieldset-legend-font-family); - font-size: var(--ams-fieldset-legend-font-size); - font-weight: var(--ams-fieldset-legend-font-weight); - line-height: var(--ams-fieldset-legend-line-height); - margin-block-end: 1rem; /* Because of a bug in Chrome we can’t use display grid or flex for this gap */ - - @include hyphenation; - @include text-rendering; - @include reset-legend; -} diff --git a/packages/css/src/components/index.scss b/packages/css/src/components/index.scss index b95b00fbdd..a3ac841829 100644 --- a/packages/css/src/components/index.scss +++ b/packages/css/src/components/index.scss @@ -19,7 +19,7 @@ @import "./column/column"; @import "./margin/margin"; @import "./gap/gap"; -@import "./fieldset/fieldset"; +@import "./field-set/field-set"; @import "./link-list/link-list"; @import "./badge/badge"; @import "./table/table"; diff --git a/packages/react/src/Fieldset/Fieldset.test.tsx b/packages/react/src/FieldSet/FieldSet.test.tsx similarity index 64% rename from packages/react/src/Fieldset/Fieldset.test.tsx rename to packages/react/src/FieldSet/FieldSet.test.tsx index bb07860e57..3d59b50c52 100644 --- a/packages/react/src/Fieldset/Fieldset.test.tsx +++ b/packages/react/src/FieldSet/FieldSet.test.tsx @@ -1,11 +1,11 @@ import { render, screen } from '@testing-library/react' import { createRef } from 'react' -import { Fieldset } from './Fieldset' +import { FieldSet } from './FieldSet' import '@testing-library/jest-dom' -describe('Fieldset', () => { +describe('FieldSet', () => { it('renders', () => { - render(
) + render(
) const component = screen.getByRole('group', { name: 'Test' }) @@ -14,33 +14,33 @@ describe('Fieldset', () => { }) it('renders a design system BEM class name', () => { - render(
) + render(
) const component = screen.getByRole('group', { name: 'Test' }) - expect(component).toHaveClass('ams-fieldset') + expect(component).toHaveClass('ams-field-set') }) it('renders an additional class name', () => { - render(
) + render(
) const component = screen.getByRole('group', { name: 'Test' }) - expect(component).toHaveClass('ams-fieldset extra') + expect(component).toHaveClass('ams-field-set extra') }) it('renders the correct legend class name', () => { - const { container } = render(
) + const { container } = render(
) const component = container.querySelector('legend') - expect(component).toHaveClass('ams-fieldset__legend') + expect(component).toHaveClass('ams-field-set__legend') }) it('supports ForwardRef in React', () => { const ref = createRef() - render(
) + render(
) const component = screen.getByRole('group', { name: 'Test' }) diff --git a/packages/react/src/FieldSet/FieldSet.tsx b/packages/react/src/FieldSet/FieldSet.tsx new file mode 100644 index 0000000000..cb424ad202 --- /dev/null +++ b/packages/react/src/FieldSet/FieldSet.tsx @@ -0,0 +1,30 @@ +/** + * @license EUPL-1.2+ + * Copyright Gemeente Amsterdam + */ + +import clsx from 'clsx' +import { forwardRef } from 'react' +import type { ForwardedRef, HTMLAttributes, PropsWithChildren } from 'react' + +export type FieldSetProps = PropsWithChildren> & { + /** Whether the field set has an input with a validation error */ + invalid?: boolean + /** The text for the caption. */ + legend: string +} + +export const FieldSet = forwardRef( + ({ children, className, invalid, legend, ...restProps }: FieldSetProps, ref: ForwardedRef) => ( +
+ {legend} + {children} +
+ ), +) + +FieldSet.displayName = 'FieldSet' diff --git a/packages/react/src/FieldSet/README.md b/packages/react/src/FieldSet/README.md new file mode 100644 index 0000000000..e43373132f --- /dev/null +++ b/packages/react/src/FieldSet/README.md @@ -0,0 +1,5 @@ + + +# React Field Set component + +[Field Set documentation](../../../css/src/components/field-set/README.md) diff --git a/packages/react/src/FieldSet/index.ts b/packages/react/src/FieldSet/index.ts new file mode 100644 index 0000000000..7460a8bcd1 --- /dev/null +++ b/packages/react/src/FieldSet/index.ts @@ -0,0 +1,2 @@ +export { FieldSet } from './FieldSet' +export type { FieldSetProps } from './FieldSet' diff --git a/packages/react/src/Fieldset/Fieldset.tsx b/packages/react/src/Fieldset/Fieldset.tsx deleted file mode 100644 index fb37061dde..0000000000 --- a/packages/react/src/Fieldset/Fieldset.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/** - * @license EUPL-1.2+ - * Copyright Gemeente Amsterdam - */ - -import clsx from 'clsx' -import { forwardRef } from 'react' -import type { ForwardedRef, HTMLAttributes, PropsWithChildren } from 'react' - -export type FieldsetProps = PropsWithChildren> & { - /** The text for the caption. */ - legend: string -} - -export const Fieldset = forwardRef( - ({ children, className, legend, ...restProps }: FieldsetProps, ref: ForwardedRef) => ( -
- {legend} - {children} -
- ), -) - -Fieldset.displayName = 'Fieldset' diff --git a/packages/react/src/Fieldset/README.md b/packages/react/src/Fieldset/README.md deleted file mode 100644 index a0f4adaa77..0000000000 --- a/packages/react/src/Fieldset/README.md +++ /dev/null @@ -1,5 +0,0 @@ - - -# React Fieldset component - -[Fieldset documentation](../../../css/src/components/fieldset/README.md) diff --git a/packages/react/src/Fieldset/index.ts b/packages/react/src/Fieldset/index.ts deleted file mode 100644 index cd5d24b492..0000000000 --- a/packages/react/src/Fieldset/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { Fieldset } from './Fieldset' -export type { FieldsetProps } from './Fieldset' diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index d6b0c721f6..9c8a57cd88 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -16,7 +16,7 @@ export * from './Radio' export * from './Tabs' export * from './TextArea' export * from './Column' -export * from './Fieldset' +export * from './FieldSet' export * from './LinkList' export * from './Badge' export * from './Table' diff --git a/proprietary/tokens/src/components/ams/fieldset.tokens.json b/proprietary/tokens/src/components/ams/field-set.tokens.json similarity index 53% rename from proprietary/tokens/src/components/ams/fieldset.tokens.json rename to proprietary/tokens/src/components/ams/field-set.tokens.json index 33c360931b..cd9e5e7393 100644 --- a/proprietary/tokens/src/components/ams/fieldset.tokens.json +++ b/proprietary/tokens/src/components/ams/field-set.tokens.json @@ -1,12 +1,21 @@ { "ams": { - "fieldset": { + "field-set": { + "invalid": { + "border-inline-start": { + "value": "{ams.border.width.lg} solid {ams.color.primary-red}" + }, + "padding-inline-start": { + "value": "{ams.space.inside.md}" + } + }, "legend": { "color": { "value": "{ams.color.primary-black}" }, "font-family": { "value": "{ams.text.font-family}" }, "font-size": { "value": "{ams.text.level.4.font-size}" }, "font-weight": { "value": "{ams.text.font-weight.bold}" }, - "line-height": { "value": "{ams.text.level.4.line-height}" } + "line-height": { "value": "{ams.text.level.4.line-height}" }, + "margin-block-end": { "value": "{ams.space.inside.md}" } } } } diff --git a/storybook/src/components/Field/Field.docs.mdx b/storybook/src/components/Field/Field.docs.mdx index b465ae15f5..c2283e9caf 100644 --- a/storybook/src/components/Field/Field.docs.mdx +++ b/storybook/src/components/Field/Field.docs.mdx @@ -10,6 +10,15 @@ import README from "../../../../packages/css/src/components/field/README.md?raw" +## With Description + +A Field can have a description. +Make sure to connect this description to the input in the Field, +otherwise this won’t be read by a screen reader. +Add an `aria-describedby` attribute to the input and provide the `id` of the describing element as its value. + + + ## With Error A Field can indicate if the contained input has a validation error. diff --git a/storybook/src/components/Field/Field.stories.tsx b/storybook/src/components/Field/Field.stories.tsx index 1eb2bb8480..230d45a298 100644 --- a/storybook/src/components/Field/Field.stories.tsx +++ b/storybook/src/components/Field/Field.stories.tsx @@ -13,32 +13,42 @@ const meta = { args: { invalid: false, }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Default: Story = { render: (args) => ( + + + ), +} + +export const WithDescription: Story = { + render: (args) => ( + + Typ geen persoonsgegevens in deze omschrijving. We vragen dit later in dit formulier aan u. - + ), -} satisfies Meta - -export default meta - -type Story = StoryObj - -export const Default: Story = {} +} export const WithError: Story = { args: { invalid: true }, render: (args) => ( - + Typ geen persoonsgegevens in deze omschrijving. We vragen dit later in dit formulier aan u. - + ), } diff --git a/storybook/src/components/FieldSet/FieldSet.docs.mdx b/storybook/src/components/FieldSet/FieldSet.docs.mdx new file mode 100644 index 0000000000..afb02c04d3 --- /dev/null +++ b/storybook/src/components/FieldSet/FieldSet.docs.mdx @@ -0,0 +1,50 @@ +import { Canvas, Controls, Markdown, Meta, Primary } from "@storybook/blocks"; +import * as FieldSetStories from "./FieldSet.stories.tsx"; +import README from "../../../../packages/css/src/components/field-set/README.md?raw"; + + + +{README} + + + + + +## Examples + +## With Description + +A Field Set can have a description. +Make sure to connect this description to the Field Set or a specific input, +otherwise this won’t be read by a screen reader. +Add an `aria-describedby` attribute to the Field Set +and provide the `id` of the describing element as its value. + + + +## With Error + +A Field Set can indicate whether any of the inputs it contains has a validation error. + + + +### Radio group + +Use a Field Set to group radio buttons. +When grouping radio inputs, use `role="radiogroup"` on Field Set to have this grouping explicitly announced as a radio group (the default role is `group`). + +Using `role="radiogroup"` also allows you to use `aria-required` on Field Set, which isn’t allowed for role `group`. +Always also set `aria-required` on the individual radio buttons though, to make sure it’s read by screen readers. + + + +### Checkbox group + +Use a Field Set to group checkboxes. + +Please note: [NVDA has bug](https://github.com/nvaccess/nvda/issues/12718) which causes it to +not report a description connected to a Field Set when it contains checkboxes. + +Try to avoid using descriptions for Field Sets containing checkboxes for this reason. + + diff --git a/storybook/src/components/FieldSet/FieldSet.stories.tsx b/storybook/src/components/FieldSet/FieldSet.stories.tsx new file mode 100644 index 0000000000..18e470fa76 --- /dev/null +++ b/storybook/src/components/FieldSet/FieldSet.stories.tsx @@ -0,0 +1,130 @@ +/** + * @license EUPL-1.2+ + * Copyright Gemeente Amsterdam + */ + +import { Checkbox, Column, FieldSet, Label, Paragraph, Radio, TextInput } from '@amsterdam/design-system-react/src' +import { Meta, StoryObj } from '@storybook/react' + +const meta = { + title: 'Components/Forms/Field Set', + component: FieldSet, + args: { + invalid: false, + legend: 'Wat is uw naam?', + }, + decorators: [ + (Story) => ( +
+ + + ), + ], +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Default: Story = { + render: (args) => ( +
+ + + + + + +
+ ), +} + +export const WithDescription: Story = { + render: (args) => ( +
+ + Vul uw naam in zoals in uw paspoort staat. + + + + + + + +
+ ), +} + +export const WithError: Story = { + args: { invalid: true }, + render: (args) => ( +
+ + Vul uw naam in zoals in uw paspoort staat. + + + + + + + +
+ ), +} + +export const RadioGroup: Story = { + args: { + legend: 'Waar gaat uw melding over?', + }, + render: (args) => ( +
+ + De laatstgenoemde melding. + + + + Horecabedrijf + + + Ander soort bedrijf + + + Evenement + + + Iets anders + + +
+ ), +} + +export const CheckboxGroup: Story = { + args: { + legend: 'Waar gaat uw melding over?', + }, + render: (args) => ( +
+ + + Horecabedrijf + + + Ander soort bedrijf + + + Evenement + + + Iets anders + + +
+ ), +} diff --git a/storybook/src/components/Fieldset/Fieldset.docs.mdx b/storybook/src/components/Fieldset/Fieldset.docs.mdx deleted file mode 100644 index d38cbc9591..0000000000 --- a/storybook/src/components/Fieldset/Fieldset.docs.mdx +++ /dev/null @@ -1,19 +0,0 @@ -import { Canvas, Controls, Markdown, Meta, Primary } from "@storybook/blocks"; -import * as FieldsetStories from "./Fieldset.stories.tsx"; -import README from "../../../../packages/css/src/components/fieldset/README.md?raw"; - - - -{README} - - - - - -## Examples - -### Checkbox group - -Fieldset is used to group related form inputs, like checkboxes. - - diff --git a/storybook/src/components/Fieldset/Fieldset.stories.tsx b/storybook/src/components/Fieldset/Fieldset.stories.tsx deleted file mode 100644 index 563ed7ce5d..0000000000 --- a/storybook/src/components/Fieldset/Fieldset.stories.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @license EUPL-1.2+ - * Copyright Gemeente Amsterdam - */ - -import { Checkbox, Fieldset } from '@amsterdam/design-system-react/src' -import { Meta, StoryObj } from '@storybook/react' - -const meta = { - title: 'Components/Forms/Fieldset', - component: Fieldset, - args: { - children: 'Body van de fieldset', - legend: 'Label van de fieldset', - }, -} satisfies Meta - -export default meta - -type Story = StoryObj - -export const Default: Story = {} - -export const CheckboxGroup: Story = { - args: { - children: [ - Horecabedrijf, - Ander soort bedrijf, - Evenement, - Iets anders, - ], - legend: 'Waar gaat uw melding over?', - }, -} diff --git a/storybook/src/components/Radio/Radio.docs.mdx b/storybook/src/components/Radio/Radio.docs.mdx index 63dd7c1e33..e7e50ba2ad 100644 --- a/storybook/src/components/Radio/Radio.docs.mdx +++ b/storybook/src/components/Radio/Radio.docs.mdx @@ -10,7 +10,7 @@ import README from "../../../../packages/css/src/components/radio/README.md?raw" -Group radios together with a [Fieldset](/docs/components-forms-fieldset--docs) that describes them. +Group radios together with a [Field Set](/docs/components-forms-field-set--docs) that describes them. This is usually a question, like ‘Where do you live?’. diff --git a/storybook/src/components/Radio/Radio.stories.tsx b/storybook/src/components/Radio/Radio.stories.tsx index 05ef66884f..1f1a215b5b 100644 --- a/storybook/src/components/Radio/Radio.stories.tsx +++ b/storybook/src/components/Radio/Radio.stories.tsx @@ -3,7 +3,7 @@ * Copyright Gemeente Amsterdam */ -import { Fieldset, Radio } from '@amsterdam/design-system-react/src' +import { FieldSet, Radio } from '@amsterdam/design-system-react/src' import { useArgs } from '@storybook/preview-api' import { Meta, StoryObj } from '@storybook/react' @@ -72,7 +72,7 @@ export const RadioGroup: Story = { }, }, render: () => ( -
+
Horecabedrijf @@ -85,7 +85,7 @@ export const RadioGroup: Story = { Iets anders -
+
), parameters: { docs: {