diff --git a/packages/css/src/components/error-message/README.md b/packages/css/src/components/error-message/README.md new file mode 100644 index 0000000000..779ef3cd4e --- /dev/null +++ b/packages/css/src/components/error-message/README.md @@ -0,0 +1,11 @@ + + +# Error Message + +Show an error message when there is a form field validation error. +In the error message explain what went wrong and how to fix it. + +For guidance and examples on using error messages in a form, +refer to the [Field](/docs/components-forms-field--docs) and [Field Set](/docs/components-forms-field-set--docs) documentation. + +Read the documentation by [NL Design System](https://www.nldesignsystem.nl/richtlijnen/formulieren/foutmeldingen) and [Gov.uk](https://design-system.service.gov.uk/components/error-message/) for more information on the contents of error messages and when to show them. diff --git a/packages/css/src/components/error-message/error-message.scss b/packages/css/src/components/error-message/error-message.scss new file mode 100644 index 0000000000..4dd6e3fd19 --- /dev/null +++ b/packages/css/src/components/error-message/error-message.scss @@ -0,0 +1,22 @@ +/** + * @license EUPL-1.2+ + * Copyright Gemeente Amsterdam + */ + +@import "../../common/text-rendering"; + +@mixin reset { + box-sizing: border-box; + margin-block: 0; +} + +.ams-error-message { + color: var(--ams-error-message-color); + font-family: var(--ams-error-message-font-family); + font-size: var(--ams-error-message-font-size); + font-weight: var(--ams-error-message-font-weight); + line-height: var(--ams-error-message-line-height); + + @include text-rendering; + @include reset; +} diff --git a/packages/css/src/components/index.scss b/packages/css/src/components/index.scss index 5f7cf2d09a..92badfec90 100644 --- a/packages/css/src/components/index.scss +++ b/packages/css/src/components/index.scss @@ -4,6 +4,7 @@ */ /* Append here */ +@import "./error-message/error-message"; @import "./file-input/file-input"; @import "./field/field"; @import "./select/select"; diff --git a/packages/react/src/ErrorMessage/ErrorMessage.test.tsx b/packages/react/src/ErrorMessage/ErrorMessage.test.tsx new file mode 100644 index 0000000000..4026873e78 --- /dev/null +++ b/packages/react/src/ErrorMessage/ErrorMessage.test.tsx @@ -0,0 +1,51 @@ +import { render, screen } from '@testing-library/react' +import { createRef } from 'react' +import { ErrorMessage } from './ErrorMessage' +import '@testing-library/jest-dom' + +describe('Error message', () => { + it('renders', () => { + render() + const component = screen.getByRole('paragraph') + + expect(component).toBeInTheDocument() + expect(component).toBeVisible() + }) + + it('renders a design system BEM class name', () => { + render() + const component = screen.getByRole('paragraph') + + expect(component).toHaveClass('ams-error-message') + }) + + it('renders an additional class name', () => { + render() + const component = screen.getByRole('paragraph') + + expect(component).toHaveClass('ams-error-message extra') + }) + + it('renders a Dutch prefix by default', () => { + render() + const component = screen.getByText('Invoerfout', { exact: false }) + + expect(component).toBeInTheDocument() + }) + + it('renders a custom prefix', () => { + render() + const component = screen.getByText('Error', { exact: false }) + + expect(component).toBeInTheDocument() + }) + + it('supports ForwardRef in React', () => { + const ref = createRef() + + render() + const component = screen.getByRole('paragraph') + + expect(ref.current).toBe(component) + }) +}) diff --git a/packages/react/src/ErrorMessage/ErrorMessage.tsx b/packages/react/src/ErrorMessage/ErrorMessage.tsx new file mode 100644 index 0000000000..ce9b2e1c0a --- /dev/null +++ b/packages/react/src/ErrorMessage/ErrorMessage.tsx @@ -0,0 +1,31 @@ +/** + * @license EUPL-1.2+ + * Copyright Gemeente Amsterdam + */ + +import clsx from 'clsx' +import { forwardRef } from 'react' +import type { ForwardedRef, HTMLAttributes, PropsWithChildren } from 'react' +import { VisuallyHidden } from '../VisuallyHidden' + +export type ErrorMessageProps = { + /** An accessible phrase that screen readers announce before the error message. Should translate to something like ‘input error’. */ + prefix?: string +} & PropsWithChildren> + +export const ErrorMessage = forwardRef( + ( + { children, className, prefix = 'Invoerfout', ...restProps }: ErrorMessageProps, + ref: ForwardedRef, + ) => ( +

+ + {prefix} + {': '} + + {children} +

+ ), +) + +ErrorMessage.displayName = 'ErrorMessage' diff --git a/packages/react/src/ErrorMessage/README.md b/packages/react/src/ErrorMessage/README.md new file mode 100644 index 0000000000..b7a1e98d1a --- /dev/null +++ b/packages/react/src/ErrorMessage/README.md @@ -0,0 +1,5 @@ + + +# React Error Message component + +[Error Message documentation](../../../css/src/components/error-message/README.md) diff --git a/packages/react/src/ErrorMessage/index.ts b/packages/react/src/ErrorMessage/index.ts new file mode 100644 index 0000000000..50e3b5c5c3 --- /dev/null +++ b/packages/react/src/ErrorMessage/index.ts @@ -0,0 +1,2 @@ +export { ErrorMessage } from './ErrorMessage' +export type { ErrorMessageProps } from './ErrorMessage' diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 4334528431..c7bd65c925 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -4,6 +4,7 @@ */ /* Append here */ +export * from './ErrorMessage' export * from './FileInput' export * from './Field' export * from './Select' diff --git a/proprietary/tokens/src/components/ams/error-message.tokens.json b/proprietary/tokens/src/components/ams/error-message.tokens.json new file mode 100644 index 0000000000..a742de7279 --- /dev/null +++ b/proprietary/tokens/src/components/ams/error-message.tokens.json @@ -0,0 +1,11 @@ +{ + "ams": { + "error-message": { + "color": { "value": "{ams.color.primary-red}" }, + "font-family": { "value": "{ams.text.font-family}" }, + "font-size": { "value": "{ams.text.level.6.font-size}" }, + "font-weight": { "value": "{ams.text.font-weight.normal}" }, + "line-height": { "value": "{ams.text.level.6.line-height}" } + } + } +} diff --git a/storybook/src/components/ErrorMessage/ErrorMessage.docs.mdx b/storybook/src/components/ErrorMessage/ErrorMessage.docs.mdx new file mode 100644 index 0000000000..74fae9a038 --- /dev/null +++ b/storybook/src/components/ErrorMessage/ErrorMessage.docs.mdx @@ -0,0 +1,19 @@ +import { Canvas, Controls, Markdown, Meta, Primary } from "@storybook/blocks"; +import * as ErrorMessageStories from "./ErrorMessage.stories.tsx"; +import README from "../../../../packages/css/src/components/error-message/README.md?raw"; + + + +{README} + + + + + +## With a custom prefix + +Error messages are automatically prefixed with a visually hidden text, the Dutch word "Invoerfout". +This makes the error message more clear for screen reader users. +If you want to change this prefix, to support another language for example, you can use the `prefix` prop. + + diff --git a/storybook/src/components/ErrorMessage/ErrorMessage.stories.tsx b/storybook/src/components/ErrorMessage/ErrorMessage.stories.tsx new file mode 100644 index 0000000000..8e6c3e4312 --- /dev/null +++ b/storybook/src/components/ErrorMessage/ErrorMessage.stories.tsx @@ -0,0 +1,33 @@ +/** + * @license EUPL-1.2+ + * Copyright Gemeente Amsterdam + */ + +import { ErrorMessage } from '@amsterdam/design-system-react/src' +import { Meta, StoryObj } from '@storybook/react' + +const meta = { + title: 'Components/Forms/Error Message', + component: ErrorMessage, + args: { + children: 'Vul een geldig e-mailadres in, bijvoorbeeld naam@voorbeeld.nl.', + }, + argTypes: { + children: { + table: { disable: false }, + }, + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Default: Story = {} + +export const WithCustomPrefix: Story = { + args: { + children: 'Enter an email address in the correct format, like name@example.com', + prefix: 'Error', + }, +} diff --git a/storybook/src/components/Field/Field.docs.mdx b/storybook/src/components/Field/Field.docs.mdx index c2283e9caf..4a0eb86187 100644 --- a/storybook/src/components/Field/Field.docs.mdx +++ b/storybook/src/components/Field/Field.docs.mdx @@ -14,7 +14,7 @@ import README from "../../../../packages/css/src/components/field/README.md?raw" 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. +otherwise it 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. @@ -22,5 +22,9 @@ Add an `aria-describedby` attribute to the input and provide the `id` of the des ## With Error A Field can indicate if the contained input has a validation error. +Use Error Message to describe the error. +Make sure to connect the error message to the input in the Field, +otherwise it won’t be read by a screen reader. +Add an `aria-describedby` attribute to the input and provide the `id` of Error Message as its value. diff --git a/storybook/src/components/Field/Field.stories.tsx b/storybook/src/components/Field/Field.stories.tsx index c1559c47a3..e542c88ea8 100644 --- a/storybook/src/components/Field/Field.stories.tsx +++ b/storybook/src/components/Field/Field.stories.tsx @@ -3,8 +3,8 @@ * Copyright Gemeente Amsterdam */ -import { TextInput } from '@amsterdam/design-system-react' -import { Field, Label, Paragraph } from '@amsterdam/design-system-react/src' +import { ErrorMessage, Label, TextInput } from '@amsterdam/design-system-react' +import { Field, Paragraph } from '@amsterdam/design-system-react/src' import { Meta, StoryObj } from '@storybook/react' const meta = { @@ -48,7 +48,8 @@ export const WithError: Story = { Typ geen persoonsgegevens in deze omschrijving. We vragen dit later in dit formulier aan u. - + Geef aan waar het om gaat. + ), } diff --git a/storybook/src/components/FieldSet/FieldSet.docs.mdx b/storybook/src/components/FieldSet/FieldSet.docs.mdx index 4214143dec..506cab411d 100644 --- a/storybook/src/components/FieldSet/FieldSet.docs.mdx +++ b/storybook/src/components/FieldSet/FieldSet.docs.mdx @@ -25,6 +25,10 @@ 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. +Use Error Message to describe the error. +Make sure to connect the error message to the correct input in the Field Set, +otherwise it won’t be read by a screen reader. +Add an `aria-describedby` attribute to the input and provide the `id` of Error Message as its value. @@ -32,12 +36,19 @@ A Field Set can indicate whether any of the inputs it contains has a validation 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. +### Radio group with error + +A Field Set with a radio button group can also have a validation error. +In this case, connect the error message to the Field Set instead of an input. +Add an `aria-describedby` attribute to the Field Set and provide the `id` of Error Message as its value. + + + ### Checkbox group Use a Field Set to group checkboxes. @@ -48,3 +59,9 @@ 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. + +### Checkbox group with error + +Because of [the NVDA bug mentioned earlier](https://github.com/nvaccess/nvda/issues/12718), +we currently do not have a reliable way to report error messages for checkbox groups with a validation error. +We are working on adding this as soon as possible. diff --git a/storybook/src/components/FieldSet/FieldSet.stories.tsx b/storybook/src/components/FieldSet/FieldSet.stories.tsx index 501d5c56bd..1aed9b47b9 100644 --- a/storybook/src/components/FieldSet/FieldSet.stories.tsx +++ b/storybook/src/components/FieldSet/FieldSet.stories.tsx @@ -3,7 +3,16 @@ * Copyright Gemeente Amsterdam */ -import { Checkbox, Column, FieldSet, Label, Paragraph, Radio, TextInput } from '@amsterdam/design-system-react/src' +import { + Checkbox, + Column, + ErrorMessage, + FieldSet, + Label, + Paragraph, + Radio, + TextInput, +} from '@amsterdam/design-system-react/src' import { Meta, StoryObj } from '@storybook/react' const meta = { @@ -64,9 +73,11 @@ export const WithError: Story = { - + {args.invalid && Vul uw voornaam in.} + - + {args.invalid && Vul uw achternaam in.} + ), @@ -105,6 +116,45 @@ export const RadioGroup: Story = { ), } +export const RadioGroupWithError: Story = { + args: { + legend: 'Waar gaat uw melding over?', + invalid: true, + }, + render: (args) => ( +
+ + De laatstgenoemde melding. + + {args.invalid && ( + + Geef aan waar uw laatstgenoemde melding over gaat. + + )} + + + Horecabedrijf + + + Ander soort bedrijf + + + Evenement + + + Iets anders + + +
+ ), +} + export const CheckboxGroup: Story = { args: { legend: 'Waar gaat uw melding over?', @@ -128,3 +178,29 @@ export const CheckboxGroup: Story = { ), } + +// export const CheckboxGroupWithError: Story = { +// args: { +// invalid: true, +// legend: 'Waar gaat uw melding over?', +// }, +// render: (args) => ( +//
+// {args.invalid && Geef aan waar uw melding over gaat.} +// +// +// Horecabedrijf +// +// +// Ander soort bedrijf +// +// +// Evenement +// +// +// Iets anders +// +// +//
+// ), +// }