Skip to content

Commit

Permalink
feat!: Add invalid prop to Field Set and update Field and Field Set d…
Browse files Browse the repository at this point in the history
…ocs (#1237)

Co-authored-by: Vincent Smedinga <[email protected]>
  • Loading branch information
alimpens and VincentSmedinga authored May 24, 2024
1 parent eec669a commit d7316e8
Show file tree
Hide file tree
Showing 23 changed files with 339 additions and 166 deletions.
17 changes: 17 additions & 0 deletions packages/css/src/components/field-set/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!-- @license CC0-1.0 -->

# 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)
48 changes: 48 additions & 0 deletions packages/css/src/components/field-set/field-set.scss
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 1 addition & 1 deletion packages/css/src/components/field/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
18 changes: 0 additions & 18 deletions packages/css/src/components/fieldset/README.md

This file was deleted.

35 changes: 0 additions & 35 deletions packages/css/src/components/fieldset/fieldset.scss

This file was deleted.

2 changes: 1 addition & 1 deletion packages/css/src/components/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -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(<Fieldset legend="Test" />)
render(<FieldSet legend="Test" />)

const component = screen.getByRole('group', { name: 'Test' })

Expand All @@ -14,33 +14,33 @@ describe('Fieldset', () => {
})

it('renders a design system BEM class name', () => {
render(<Fieldset legend="Test" />)
render(<FieldSet legend="Test" />)

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(<Fieldset legend="Test" className="extra" />)
render(<FieldSet legend="Test" className="extra" />)

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(<Fieldset legend="Test" />)
const { container } = render(<FieldSet legend="Test" />)

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<HTMLFieldSetElement>()

render(<Fieldset legend="Test" ref={ref} />)
render(<FieldSet legend="Test" ref={ref} />)

const component = screen.getByRole('group', { name: 'Test' })

Expand Down
30 changes: 30 additions & 0 deletions packages/react/src/FieldSet/FieldSet.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLAttributes<HTMLFieldSetElement>> & {
/** 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<HTMLFieldSetElement>) => (
<fieldset
{...restProps}
ref={ref}
className={clsx('ams-field-set', invalid && 'ams-field-set--invalid', className)}
>
<legend className="ams-field-set__legend">{legend}</legend>
{children}
</fieldset>
),
)

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

# React Field Set component

[Field Set documentation](../../../css/src/components/field-set/README.md)
2 changes: 2 additions & 0 deletions packages/react/src/FieldSet/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { FieldSet } from './FieldSet'
export type { FieldSetProps } from './FieldSet'
24 changes: 0 additions & 24 deletions packages/react/src/Fieldset/Fieldset.tsx

This file was deleted.

5 changes: 0 additions & 5 deletions packages/react/src/Fieldset/README.md

This file was deleted.

2 changes: 0 additions & 2 deletions packages/react/src/Fieldset/index.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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}" }
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions storybook/src/components/Field/Field.docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ import README from "../../../../packages/css/src/components/field/README.md?raw"

<Controls />

## 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.

<Canvas of={FieldStories.WithDescription} />

## With Error

A Field can indicate if the contained input has a validation error.
Expand Down
30 changes: 20 additions & 10 deletions storybook/src/components/Field/Field.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,42 @@ const meta = {
args: {
invalid: false,
},
} satisfies Meta<typeof Field>

export default meta

type Story = StoryObj<typeof meta>

export const Default: Story = {
render: (args) => (
<Field invalid={args.invalid}>
<Label htmlFor="input1">Waar gaat het om?</Label>
<TextInput id="input1" aria-invalid={args.invalid ? true : undefined} />
</Field>
),
}

export const WithDescription: Story = {
render: (args) => (
<Field invalid={args.invalid}>
<Label htmlFor="input2">Waar gaat het om?</Label>
<Paragraph id="description1" size="small">
Typ geen persoonsgegevens in deze omschrijving. We vragen dit later in dit formulier aan u.
</Paragraph>
<TextInput id="input1" aria-describedby="description1" aria-invalid={args.invalid ? true : undefined} />
<TextInput id="input2" aria-describedby="description1" aria-invalid={args.invalid ? true : undefined} />
</Field>
),
} satisfies Meta<typeof Field>

export default meta

type Story = StoryObj<typeof meta>

export const Default: Story = {}
}

export const WithError: Story = {
args: { invalid: true },
render: (args) => (
<Field invalid={args.invalid}>
<Label htmlFor="input2">Waar gaat het om?</Label>
<Label htmlFor="input3">Waar gaat het om?</Label>
<Paragraph id="description2" size="small">
Typ geen persoonsgegevens in deze omschrijving. We vragen dit later in dit formulier aan u.
</Paragraph>
<TextInput id="input2" aria-describedby="description2" aria-invalid={args.invalid ? true : undefined} />
<TextInput id="input3" aria-describedby="description2" aria-invalid={args.invalid ? true : undefined} />
</Field>
),
}
Loading

0 comments on commit d7316e8

Please sign in to comment.