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/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/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 b95b00fbdd..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"; @@ -19,7 +20,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/css/src/components/search-field/search-field.scss b/packages/css/src/components/search-field/search-field.scss index f3ffaf1450..8757fc4985 100644 --- a/packages/css/src/components/search-field/search-field.scss +++ b/packages/css/src/components/search-field/search-field.scss @@ -48,6 +48,7 @@ .ams-search-field__input::placeholder { color: var(--ams-search-field-input-placeholder-color); + opacity: 100%; // This resets the lower opacity set by Firefox } .ams-search-field__input::-webkit-search-cancel-button { diff --git a/packages/css/src/components/text-area/text-area.scss b/packages/css/src/components/text-area/text-area.scss index 96aeb5de55..3a0256e515 100644 --- a/packages/css/src/components/text-area/text-area.scss +++ b/packages/css/src/components/text-area/text-area.scss @@ -37,6 +37,7 @@ .ams-text-area::placeholder { color: var(--ams-text-area-placeholder-color); + opacity: 100%; // This resets the lower opacity set by Firefox } .ams-text-area:disabled { diff --git a/packages/css/src/components/text-input/text-input.scss b/packages/css/src/components/text-input/text-input.scss index e839902c41..4d09106fae 100644 --- a/packages/css/src/components/text-input/text-input.scss +++ b/packages/css/src/components/text-input/text-input.scss @@ -35,6 +35,7 @@ .ams-text-input::placeholder { color: var(--ams-text-input-placeholder-color); + opacity: 100%; // This resets the lower opacity set by Firefox } .ams-text-input:disabled { 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/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/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/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(