diff --git a/packages/css/src/components/field/README.md b/packages/css/src/components/field/README.md new file mode 100644 index 0000000000..581cad51f1 --- /dev/null +++ b/packages/css/src/components/field/README.md @@ -0,0 +1,9 @@ + + +# Field + +Wraps a single input and its related elements. May indicate that the input has a validation error. + +## Guidelines + +Only use Field to wrap a single input. Use [Fieldset](/docs/components-forms-fieldset--docs) to wrap multiple inputs. diff --git a/packages/css/src/components/field/field.scss b/packages/css/src/components/field/field.scss new file mode 100644 index 0000000000..681d1f88ee --- /dev/null +++ b/packages/css/src/components/field/field.scss @@ -0,0 +1,16 @@ +/** + * @license EUPL-1.2+ + * Copyright Gemeente Amsterdam + */ + +.ams-field { + break-inside: avoid; + display: flex; + flex-direction: column; + gap: var(--ams-field-gap); +} + +.ams-field--invalid { + border-inline-start: var(--ams-field-invalid-border-inline-start); + padding-inline-start: var(--ams-field-invalid-padding-inline-start); +} diff --git a/packages/css/src/components/index.scss b/packages/css/src/components/index.scss index 063a49713e..b95b00fbdd 100644 --- a/packages/css/src/components/index.scss +++ b/packages/css/src/components/index.scss @@ -4,6 +4,7 @@ */ /* Append here */ +@import "./field/field"; @import "./select/select"; @import "./time-input/time-input"; @import "./date-input/date-input"; diff --git a/packages/react/src/Field/Field.test.tsx b/packages/react/src/Field/Field.test.tsx new file mode 100644 index 0000000000..a2a5cf7577 --- /dev/null +++ b/packages/react/src/Field/Field.test.tsx @@ -0,0 +1,44 @@ +import { render } from '@testing-library/react' +import { createRef } from 'react' +import { Field } from './Field' +import '@testing-library/jest-dom' + +describe('Field', () => { + it('renders', () => { + const { container } = render() + const component = container.querySelector(':only-child') + + expect(component).toBeInTheDocument() + expect(component).toBeVisible() + }) + + it('renders a design system BEM class name', () => { + const { container } = render() + const component = container.querySelector(':only-child') + + expect(component).toHaveClass('ams-field') + }) + + it('renders an additional class name', () => { + const { container } = render() + const component = container.querySelector(':only-child') + + expect(component).toHaveClass('ams-field extra') + }) + + it('renders with the error class', () => { + const { container } = render() + const component = container.querySelector(':only-child') + + expect(component).toHaveClass('ams-field--invalid') + }) + + it('supports ForwardRef in React', () => { + const ref = createRef() + + const { container } = render() + const component = container.querySelector(':only-child') + + expect(ref.current).toBe(component) + }) +}) diff --git a/packages/react/src/Field/Field.tsx b/packages/react/src/Field/Field.tsx new file mode 100644 index 0000000000..e8404fbf10 --- /dev/null +++ b/packages/react/src/Field/Field.tsx @@ -0,0 +1,23 @@ +/** + * @license EUPL-1.2+ + * Copyright Gemeente Amsterdam + */ + +import clsx from 'clsx' +import { forwardRef } from 'react' +import type { ForwardedRef, HTMLAttributes, PropsWithChildren } from 'react' + +export type FieldProps = { + /** Whether the field has an input with a validation error */ + invalid?: boolean +} & PropsWithChildren> + +export const Field = forwardRef( + ({ children, className, invalid, ...restProps }: FieldProps, ref: ForwardedRef) => ( +
+ {children} +
+ ), +) + +Field.displayName = 'Field' diff --git a/packages/react/src/Field/README.md b/packages/react/src/Field/README.md new file mode 100644 index 0000000000..fb9b52624d --- /dev/null +++ b/packages/react/src/Field/README.md @@ -0,0 +1,5 @@ + + +# React Field component + +[Field documentation](../../../css/src/components/field/README.md) diff --git a/packages/react/src/Field/index.ts b/packages/react/src/Field/index.ts new file mode 100644 index 0000000000..93ca84d382 --- /dev/null +++ b/packages/react/src/Field/index.ts @@ -0,0 +1,2 @@ +export { Field } from './Field' +export type { FieldProps } from './Field' diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 70b5d5f6da..d6b0c721f6 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -4,6 +4,7 @@ */ /* Append here */ +export * from './Field' export * from './Select' export * from './TimeInput' export * from './DateInput' diff --git a/proprietary/tokens/src/components/ams/field.tokens.json b/proprietary/tokens/src/components/ams/field.tokens.json new file mode 100644 index 0000000000..350ba5caad --- /dev/null +++ b/proprietary/tokens/src/components/ams/field.tokens.json @@ -0,0 +1,17 @@ +{ + "ams": { + "field": { + "gap": { + "value": "{ams.space.stack.sm}" + }, + "invalid": { + "border-inline-start": { + "value": "{ams.border.width.lg} solid {ams.color.primary-red}" + }, + "padding-inline-start": { + "value": "{ams.space.inside.md}" + } + } + } + } +} diff --git a/storybook/src/components/Field/Field.docs.mdx b/storybook/src/components/Field/Field.docs.mdx new file mode 100644 index 0000000000..b465ae15f5 --- /dev/null +++ b/storybook/src/components/Field/Field.docs.mdx @@ -0,0 +1,17 @@ +import { Canvas, Controls, Markdown, Meta, Primary } from "@storybook/blocks"; +import * as FieldStories from "./Field.stories.tsx"; +import README from "../../../../packages/css/src/components/field/README.md?raw"; + + + +{README} + + + + + +## 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 new file mode 100644 index 0000000000..1eb2bb8480 --- /dev/null +++ b/storybook/src/components/Field/Field.stories.tsx @@ -0,0 +1,44 @@ +/** + * @license EUPL-1.2+ + * Copyright Gemeente Amsterdam + */ + +import { TextInput } from '@amsterdam/design-system-react' +import { Field, Label, Paragraph } from '@amsterdam/design-system-react/src' +import { Meta, StoryObj } from '@storybook/react' + +const meta = { + title: 'Components/Forms/Field', + component: Field, + args: { + invalid: false, + }, + 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. + + + + ), +}