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) => (
+
+ ),
+)
+
+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) => (
-
- ),
-)
-
-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()
+
+ const component = screen.getByRole('textbox')
+
+ expect(component).toHaveAttribute('dir', 'auto')
+ })
+
+ it('renders left-to-right by using `dir="ltr"`', () => {
+ render()
+
+ const component = screen.getByRole('textbox')
+
+ expect(component).toHaveAttribute('dir', 'ltr')
+ })
})
diff --git a/packages/react/src/TextArea/TextArea.tsx b/packages/react/src/TextArea/TextArea.tsx
index 2c9d4c6516..dfb6f87bd5 100644
--- a/packages/react/src/TextArea/TextArea.tsx
+++ b/packages/react/src/TextArea/TextArea.tsx
@@ -13,7 +13,7 @@ export type TextAreaProps = TextareaHTMLAttributes & {
}
export const TextArea = forwardRef(
- ({ className, resize, ...restProps }: TextAreaProps, ref: ForwardedRef) => (
+ ({ className, dir, resize, ...restProps }: TextAreaProps, ref: ForwardedRef) => (
),
)
diff --git a/packages/react/src/TextInput/TextInput.test.tsx b/packages/react/src/TextInput/TextInput.test.tsx
index 77a282b14e..168835466d 100644
--- a/packages/react/src/TextInput/TextInput.test.tsx
+++ b/packages/react/src/TextInput/TextInput.test.tsx
@@ -73,4 +73,20 @@ describe('Text input', () => {
expect(ref.current).toBe(component)
})
+
+ it('renders bidirectional by default using `dir="auto"`', () => {
+ render()
+
+ const component = screen.getByRole('textbox')
+
+ expect(component).toHaveAttribute('dir', 'auto')
+ })
+
+ it('renders left-to-right by using `dir="ltr"`', () => {
+ render()
+
+ const component = screen.getByRole('textbox')
+
+ expect(component).toHaveAttribute('dir', 'ltr')
+ })
})
diff --git a/packages/react/src/TextInput/TextInput.tsx b/packages/react/src/TextInput/TextInput.tsx
index 56990d874a..b5b5d9739b 100644
--- a/packages/react/src/TextInput/TextInput.tsx
+++ b/packages/react/src/TextInput/TextInput.tsx
@@ -10,8 +10,8 @@ import type { ForwardedRef, InputHTMLAttributes } from 'react'
export type TextInputProps = InputHTMLAttributes
export const TextInput = forwardRef(
- ({ className, ...restProps }: TextInputProps, ref: ForwardedRef) => (
-
+ ({ className, dir, ...restProps }: TextInputProps, ref: ForwardedRef) => (
+
),
)
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index d6b0c721f6..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'
@@ -16,7 +17,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/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}" }
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/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/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) => (
+
+ ),
+}
+
+export const WithError: Story = {
+ args: { invalid: true },
+ render: (args) => (
+
+ ),
+}
+
+export const RadioGroup: Story = {
+ args: {
+ legend: 'Waar gaat uw melding over?',
+ },
+ render: (args) => (
+
+ ),
+}
+
+export const CheckboxGroup: Story = {
+ args: {
+ legend: 'Waar gaat uw melding over?',
+ },
+ render: (args) => (
+
+ ),
+}
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/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 },
+}
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: () => (
-
),
parameters: {
docs: {