From eec669ad79353fc205e12a35aa7d0a8297c72e41 Mon Sep 17 00:00:00 2001 From: Niels Roozemond Date: Fri, 24 May 2024 11:53:26 +0200 Subject: [PATCH 1/5] feat!: Allow additional background colours for Badge and remove dark blue option (#1236) Co-authored-by: Vincent Smedinga --- packages/css/src/components/badge/badge.scss | 40 ++++++++++++++++--- packages/react/src/Badge/Badge.tsx | 8 +++- .../src/components/ams/badge.tokens.json | 32 +++++++++++++-- 3 files changed, 70 insertions(+), 10 deletions(-) diff --git a/packages/css/src/components/badge/badge.scss b/packages/css/src/components/badge/badge.scss index f25fec491b..d53d17bede 100644 --- a/packages/css/src/components/badge/badge.scss +++ b/packages/css/src/components/badge/badge.scss @@ -12,16 +12,16 @@ padding-inline: var(--ams-badge-padding-inline); } +.ams-badge--black { + background-color: var(--ams-badge-black-background-color); + color: var(--ams-badge-black-color); +} + .ams-badge--blue { background-color: var(--ams-badge-blue-background-color); color: var(--ams-badge-blue-color); } -.ams-badge--dark-blue { - background-color: var(--ams-badge-dark-blue-background-color); - color: var(--ams-badge-dark-blue-color); -} - .ams-badge--dark-green { background-color: var(--ams-badge-dark-green-background-color); color: var(--ams-badge-dark-green-color); @@ -32,6 +32,26 @@ color: var(--ams-badge-green-color); } +.ams-badge--grey-1 { + background-color: var(--ams-badge-grey-1-background-color); + color: var(--ams-badge-grey-1-color); +} + +.ams-badge--grey-2 { + background-color: var(--ams-badge-grey-2-background-color); + color: var(--ams-badge-grey-2-color); +} + +.ams-badge--grey-3 { + background-color: var(--ams-badge-grey-3-background-color); + color: var(--ams-badge-grey-3-color); +} + +.ams-badge--light-blue { + background-color: var(--ams-badge-light-blue-background-color); + color: var(--ams-badge-light-blue-color); +} + .ams-badge--magenta { background-color: var(--ams-badge-magenta-background-color); color: var(--ams-badge-magenta-color); @@ -47,6 +67,16 @@ color: var(--ams-badge-purple-color); } +.ams-badge--red { + background-color: var(--ams-badge-red-background-color); + color: var(--ams-badge-red-color); +} + +.ams-badge--white { + background-color: var(--ams-badge-white-background-color); + color: var(--ams-badge-white-color); +} + .ams-badge--yellow { background-color: var(--ams-badge-yellow-background-color); color: var(--ams-badge-yellow-color); diff --git a/packages/react/src/Badge/Badge.tsx b/packages/react/src/Badge/Badge.tsx index b17f6297ca..3ccd0cb457 100644 --- a/packages/react/src/Badge/Badge.tsx +++ b/packages/react/src/Badge/Badge.tsx @@ -8,13 +8,19 @@ import { forwardRef } from 'react' import type { ForwardedRef, HTMLAttributes } from 'react' export const badgeColors = [ + 'black', 'blue', - 'dark-blue', 'dark-green', 'green', + 'grey-1', + 'grey-2', + 'grey-3', + 'light-blue', 'magenta', 'orange', 'purple', + 'red', + 'white', 'yellow', ] as const diff --git a/proprietary/tokens/src/components/ams/badge.tokens.json b/proprietary/tokens/src/components/ams/badge.tokens.json index c3a0570ff7..7054f6036f 100644 --- a/proprietary/tokens/src/components/ams/badge.tokens.json +++ b/proprietary/tokens/src/components/ams/badge.tokens.json @@ -6,11 +6,11 @@ "font-weight": { "value": "{ams.text.font-weight.bold}" }, "line-height": { "value": "{ams.text.level.5.line-height}" }, "padding-inline": { "value": "{ams.space.inside.xs}" }, - "blue": { - "background-color": { "value": "{ams.color.blue}" }, - "color": { "value": "{ams.color.primary-black}" } + "black": { + "background-color": { "value": "{ams.color.primary-black}" }, + "color": { "value": "{ams.color.primary-white}" } }, - "dark-blue": { + "blue": { "background-color": { "value": "{ams.color.primary-blue}" }, "color": { "value": "{ams.color.primary-white}" } }, @@ -22,6 +22,22 @@ "background-color": { "value": "{ams.color.green}" }, "color": { "value": "{ams.color.primary-black}" } }, + "grey-1": { + "background-color": { "value": "{ams.color.neutral-grey1}" }, + "color": { "value": "{ams.color.primary-black}" } + }, + "grey-2": { + "background-color": { "value": "{ams.color.neutral-grey2}" }, + "color": { "value": "{ams.color.primary-black}" } + }, + "grey-3": { + "background-color": { "value": "{ams.color.neutral-grey3}" }, + "color": { "value": "{ams.color.primary-white}" } + }, + "light-blue": { + "background-color": { "value": "{ams.color.blue}" }, + "color": { "value": "{ams.color.primary-black}" } + }, "magenta": { "background-color": { "value": "{ams.color.magenta}" }, "color": { "value": "{ams.color.primary-white}" } @@ -34,6 +50,14 @@ "background-color": { "value": "{ams.color.purple}" }, "color": { "value": "{ams.color.primary-white}" } }, + "red": { + "background-color": { "value": "{ams.color.primary-red}" }, + "color": { "value": "{ams.color.primary-white}" } + }, + "white": { + "background-color": { "value": "{ams.color.primary-white}" }, + "color": { "value": "{ams.color.primary-black}" } + }, "yellow": { "background-color": { "value": "{ams.color.yellow}" }, "color": { "value": "{ams.color.primary-black}" } 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 2/5] 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: { From 7b6ba98530caaefafedada5b89a175ef0b1a8784 Mon Sep 17 00:00:00 2001 From: Niels Roozemond Date: Fri, 24 May 2024 17:37:11 +0200 Subject: [PATCH 3/5] feat: File Input (#1218) Co-authored-by: Vincent Smedinga Co-authored-by: Aram <37216945+alimpens@users.noreply.github.com> --- .../css/src/components/file-input/README.md | 9 +++ .../src/components/file-input/file-input.scss | 68 +++++++++++++++++++ packages/css/src/components/index.scss | 1 + .../react/src/FileInput/FileInput.test.tsx | 37 ++++++++++ packages/react/src/FileInput/FileInput.tsx | 18 +++++ packages/react/src/FileInput/README.md | 5 ++ packages/react/src/FileInput/index.ts | 2 + packages/react/src/index.ts | 1 + .../src/components/ams/file-input.tokens.json | 42 ++++++++++++ .../components/FileInput/FileInput.docs.mdx | 29 ++++++++ .../FileInput/FileInput.stories.tsx | 52 ++++++++++++++ 11 files changed, 264 insertions(+) create mode 100644 packages/css/src/components/file-input/README.md create mode 100644 packages/css/src/components/file-input/file-input.scss create mode 100644 packages/react/src/FileInput/FileInput.test.tsx create mode 100644 packages/react/src/FileInput/FileInput.tsx create mode 100644 packages/react/src/FileInput/README.md create mode 100644 packages/react/src/FileInput/index.ts create mode 100644 proprietary/tokens/src/components/ams/file-input.tokens.json create mode 100644 storybook/src/components/FileInput/FileInput.docs.mdx create mode 100644 storybook/src/components/FileInput/FileInput.stories.tsx diff --git a/packages/css/src/components/file-input/README.md b/packages/css/src/components/file-input/README.md new file mode 100644 index 0000000000..4ab50f20f3 --- /dev/null +++ b/packages/css/src/components/file-input/README.md @@ -0,0 +1,9 @@ + + +# File Input + +Allows the user to upload one or more files from their device. + +## Visual considerations + +The filename label and button are displayed in the language of the browser and can vary between browsers and operating systems. diff --git a/packages/css/src/components/file-input/file-input.scss b/packages/css/src/components/file-input/file-input.scss new file mode 100644 index 0000000000..0a8942825b --- /dev/null +++ b/packages/css/src/components/file-input/file-input.scss @@ -0,0 +1,68 @@ +/** + * @license EUPL-1.2+ + * Copyright Gemeente Amsterdam + */ + +@import "../../common/text-rendering"; + +@mixin reset-button { + border: 0; + border-radius: 0; // Reset rounded borders on iOS devices + box-sizing: border-box; +} + +.ams-file-input { + background-color: var(--ams-file-input-background-color); + border: var(--ams-file-input-border); + color: var(--ams-file-input-color); + cursor: var(--ams-file-input-cursor); + font-family: var(--ams-file-input-font-family); + font-size: var(--ams-file-input-font-size); + font-weight: var(--ams-file-input-font-weight); + line-height: var(--ams-file-input-line-height); + max-inline-size: calc(100% - var(--ams-file-input-padding-inline) * 2); + outline-offset: 0.25rem; // Double the default focus outline offset to compensate for the dashed border + padding-block: var(--ams-file-input-padding-block); + padding-inline: var(--ams-file-input-padding-inline); + touch-action: manipulation; + + @include text-rendering; +} + +.ams-file-input:disabled { + color: var(--ams-file-input-disabled-color); + cursor: var(--ams-file-input-disabled-cursor); +} + +.ams-file-input::file-selector-button { + appearance: none; // Reset default appearance on iOS devices + background-color: var(--ams-file-input-file-selector-button-background-color); + box-shadow: var(--ams-file-input-file-selector-button-box-shadow); + color: var(--ams-file-input-file-selector-button-color); + cursor: var(--ams-file-input-file-selector-button-cursor); + font-family: inherit; + font-size: inherit; // iOS specific fix + font-weight: inherit; + margin-inline-end: var(--ams-file-input-file-selector-button-margin-inline-end); + padding-block: var(--ams-file-input-file-selector-button-padding-block); + padding-inline: var(--ams-file-input-file-selector-button-padding-inline); + + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active) { + border: var( + --ams-file-input-file-selector-button-forced-color-mode-border + ); // add border because forced colors changes box-shadow to none + } + + @include reset-button; +} + +.ams-file-input:disabled::file-selector-button { + box-shadow: var(--ams-file-input-file-selector-button-disabled-box-shadow); + color: var(--ams-file-input-disabled-color); + cursor: var(--ams-file-input-file-selector-button-disabled-cursor); +} + +.ams-file-input:not(:disabled):hover::file-selector-button { + box-shadow: var(--ams-file-input-file-selector-button-hover-box-shadow); + color: var(--ams-file-input-file-selector-button-hover-color); +} diff --git a/packages/css/src/components/index.scss b/packages/css/src/components/index.scss index a3ac841829..5f7cf2d09a 100644 --- a/packages/css/src/components/index.scss +++ b/packages/css/src/components/index.scss @@ -4,6 +4,7 @@ */ /* Append here */ +@import "./file-input/file-input"; @import "./field/field"; @import "./select/select"; @import "./time-input/time-input"; diff --git a/packages/react/src/FileInput/FileInput.test.tsx b/packages/react/src/FileInput/FileInput.test.tsx new file mode 100644 index 0000000000..8875679be5 --- /dev/null +++ b/packages/react/src/FileInput/FileInput.test.tsx @@ -0,0 +1,37 @@ +import { render } from '@testing-library/react' +import { createRef } from 'react' +import { FileInput } from './FileInput' +import '@testing-library/jest-dom' + +describe('File input', () => { + it('renders', () => { + const { container } = render() + const component = container.querySelector('input[type="file"]') + + expect(component).toBeInTheDocument() + expect(component).toBeVisible() + }) + + it('renders a design system BEM class name', () => { + const { container } = render() + const component = container.querySelector('input[type="file"]') + + expect(component).toHaveClass('ams-file-input') + }) + + it('renders an additional class name', () => { + const { container } = render() + const component = container.querySelector('input[type="file"]') + + expect(component).toHaveClass('ams-file-input extra') + }) + + it('supports ForwardRef in React', () => { + const ref = createRef() + + const { container } = render() + const component = container.querySelector('input[type="file"]') + + expect(ref.current).toBe(component) + }) +}) diff --git a/packages/react/src/FileInput/FileInput.tsx b/packages/react/src/FileInput/FileInput.tsx new file mode 100644 index 0000000000..3df5f8cec0 --- /dev/null +++ b/packages/react/src/FileInput/FileInput.tsx @@ -0,0 +1,18 @@ +/** + * @license EUPL-1.2+ + * Copyright Gemeente Amsterdam + */ + +import clsx from 'clsx' +import { forwardRef } from 'react' +import type { ForwardedRef, InputHTMLAttributes } from 'react' + +export type FileInputProps = InputHTMLAttributes + +export const FileInput = forwardRef( + ({ className, ...restProps }: FileInputProps, ref: ForwardedRef) => ( + + ), +) + +FileInput.displayName = 'FileInput' diff --git a/packages/react/src/FileInput/README.md b/packages/react/src/FileInput/README.md new file mode 100644 index 0000000000..5fe4f7d710 --- /dev/null +++ b/packages/react/src/FileInput/README.md @@ -0,0 +1,5 @@ + + +# React File Input component + +[File Input documentation](../../../css/src/components/file-input/README.md) diff --git a/packages/react/src/FileInput/index.ts b/packages/react/src/FileInput/index.ts new file mode 100644 index 0000000000..73c0b3a40c --- /dev/null +++ b/packages/react/src/FileInput/index.ts @@ -0,0 +1,2 @@ +export { FileInput } from './FileInput' +export type { FileInputProps } from './FileInput' diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 9c8a57cd88..4334528431 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -4,6 +4,7 @@ */ /* Append here */ +export * from './FileInput' export * from './Field' export * from './Select' export * from './TimeInput' diff --git a/proprietary/tokens/src/components/ams/file-input.tokens.json b/proprietary/tokens/src/components/ams/file-input.tokens.json new file mode 100644 index 0000000000..b0f31f9437 --- /dev/null +++ b/proprietary/tokens/src/components/ams/file-input.tokens.json @@ -0,0 +1,42 @@ +{ + "ams": { + "file-input": { + "background-color": { "value": "{ams.color.primary-white}" }, + "border": { "value": "{ams.border.width.sm} dashed {ams.color.neutral-grey3}" }, + "color": { "value": "{ams.color.primary-black}" }, + "cursor": { "value": "{ams.action.activate.cursor}" }, + "font-family": { "value": "{ams.text.font-family}" }, + "font-size": { "value": "{ams.text.level.5.font-size}" }, + "font-weight": { "value": "{ams.text.font-weight.normal}" }, + "line-height": { "value": "{ams.text.level.5.line-height}" }, + "outline-offset": { "value": "{ams.focus.outline-offset}" }, + "padding-block": { "value": "{ams.space.inside.md}" }, + "padding-inline": { "value": "{ams.space.inside.md}" }, + "disabled": { + "color": { "value": "{ams.color.neutral-grey2}" }, + "cursor": { "value": "{ams.action.disabled.cursor}" } + }, + "file-selector-button": { + "background-color": { "value": "{ams.color.primary-white}" }, + "box-shadow": { "value": "inset 0 0 0 {ams.border.width.md} {ams.color.primary-blue}" }, + "color": { "value": "{ams.color.primary-blue}" }, + "cursor": { "value": "{ams.action.activate.cursor}" }, + "margin-inline-end": { "value": "{ams.space.inside.md}" }, + "padding-block": { "value": "{ams.space.inside.xs}" }, + "padding-inline": { "value": "{ams.space.inside.md}" }, + "hover": { + "box-shadow": { "value": "inset 0 0 0 {ams.border.width.lg} {ams.color.dark-blue}" }, + "color": { "value": "{ams.color.dark-blue}" } + }, + "disabled": { + "box-shadow": { "value": "inset 0 0 0 {ams.border.width.md} {ams.color.neutral-grey2}" }, + "color": { "value": "{ams.color.neutral-grey2}" }, + "cursor": { "value": "{ams.action.disabled.cursor}" } + }, + "forced-color-mode": { + "border": { "value": "{ams.border.width.md} solid" } + } + } + } + } +} diff --git a/storybook/src/components/FileInput/FileInput.docs.mdx b/storybook/src/components/FileInput/FileInput.docs.mdx new file mode 100644 index 0000000000..010919d592 --- /dev/null +++ b/storybook/src/components/FileInput/FileInput.docs.mdx @@ -0,0 +1,29 @@ +import { Canvas, Controls, Markdown, Meta, Primary } from "@storybook/blocks"; +import * as FileInputStories from "./FileInput.stories.tsx"; +import README from "../../../../packages/css/src/components/file-input/README.md?raw"; + + + +{README} + + + + + +## Multiple Files + +Allow multiple files to be selected. The label will update to show the number of files selected. + + + +## Accept + +Limit the types of files that can be selected. Some examples are `image/*`, `video/*`, or `audio/*`. To limit to a specific file type, use the MIME type, such as `application/pdf`. + +- [MDN File Input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#limiting_accepted_file_types): More examples + + + +## Disabled + + diff --git a/storybook/src/components/FileInput/FileInput.stories.tsx b/storybook/src/components/FileInput/FileInput.stories.tsx new file mode 100644 index 0000000000..19758a651d --- /dev/null +++ b/storybook/src/components/FileInput/FileInput.stories.tsx @@ -0,0 +1,52 @@ +/** + * @license EUPL-1.2+ + * Copyright Gemeente Amsterdam + */ + +import { FileInput } from '@amsterdam/design-system-react/src' +import { Meta, StoryObj } from '@storybook/react' + +const meta = { + title: 'Components/Forms/File Input', + component: FileInput, + args: { + accept: undefined, + multiple: false, + disabled: false, + }, + argTypes: { + accept: { + control: { + type: 'text', + }, + }, + multiple: { + control: { + type: 'boolean', + }, + }, + disabled: { + control: { + type: 'boolean', + }, + }, + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Default: Story = {} + +export const Multiple: Story = { + args: { multiple: true }, +} + +export const Accept: Story = { + args: { accept: 'application/pdf' }, +} + +export const Disabled: Story = { + args: { disabled: true }, +} From b588d104095365bcadff4f34f7db53e247afc873 Mon Sep 17 00:00:00 2001 From: Aram <37216945+alimpens@users.noreply.github.com> Date: Mon, 27 May 2024 11:16:56 +0200 Subject: [PATCH 4/5] feat: Set dir auto by default for text input form fields (#1238) Co-authored-by: Vincent Smedinga --- .../src/SearchField/SearchFieldInput.test.tsx | 16 ++++++++++++++++ .../react/src/SearchField/SearchFieldInput.tsx | 3 ++- packages/react/src/TextArea/TextArea.test.tsx | 16 ++++++++++++++++ packages/react/src/TextArea/TextArea.tsx | 3 ++- packages/react/src/TextInput/TextInput.test.tsx | 16 ++++++++++++++++ packages/react/src/TextInput/TextInput.tsx | 4 ++-- 6 files changed, 54 insertions(+), 4 deletions(-) diff --git a/packages/react/src/SearchField/SearchFieldInput.test.tsx b/packages/react/src/SearchField/SearchFieldInput.test.tsx index f71a1049f6..9420ca2ed9 100644 --- a/packages/react/src/SearchField/SearchFieldInput.test.tsx +++ b/packages/react/src/SearchField/SearchFieldInput.test.tsx @@ -71,4 +71,20 @@ describe('Search field input', () => { expect(ref.current).toBe(component) }) + + it('renders bidirectional by default using `dir="auto"`', () => { + render() + + const component = screen.getByRole('searchbox', { name: 'Zoeken' }) + + expect(component).toHaveAttribute('dir', 'auto') + }) + + it('renders left-to-right by using `dir="ltr"`', () => { + render() + + const component = screen.getByRole('searchbox', { name: 'Zoeken' }) + + expect(component).toHaveAttribute('dir', 'ltr') + }) }) diff --git a/packages/react/src/SearchField/SearchFieldInput.tsx b/packages/react/src/SearchField/SearchFieldInput.tsx index 510e587b94..2ef1770cf7 100644 --- a/packages/react/src/SearchField/SearchFieldInput.tsx +++ b/packages/react/src/SearchField/SearchFieldInput.tsx @@ -14,7 +14,7 @@ type SearchFieldInputProps = { } & InputHTMLAttributes export const SearchFieldInput = forwardRef( - ({ className, label = 'Zoeken', ...restProps }: SearchFieldInputProps, ref: ForwardedRef) => { + ({ className, dir, label = 'Zoeken', ...restProps }: SearchFieldInputProps, ref: ForwardedRef) => { const id = useId() return ( @@ -26,6 +26,7 @@ export const SearchFieldInput = forwardRef( {...restProps} autoComplete="off" className={clsx('ams-search-field__input', className)} + dir={dir ?? 'auto'} enterKeyHint="search" id={id} ref={ref} diff --git a/packages/react/src/TextArea/TextArea.test.tsx b/packages/react/src/TextArea/TextArea.test.tsx index 01852a11aa..3a1bf96b0e 100644 --- a/packages/react/src/TextArea/TextArea.test.tsx +++ b/packages/react/src/TextArea/TextArea.test.tsx @@ -103,4 +103,20 @@ describe('Text area', () => { expect(ref.current).toBe(component) }) + + it('renders bidirectional by default using `dir="auto"`', () => { + render(