Skip to content

Commit

Permalink
feat: Add Field component (#1228)
Browse files Browse the repository at this point in the history
Co-authored-by: Vincent Smedinga <[email protected]>
  • Loading branch information
alimpens and VincentSmedinga authored May 17, 2024
1 parent 6cf2e5e commit 66832aa
Show file tree
Hide file tree
Showing 11 changed files with 179 additions and 0 deletions.
9 changes: 9 additions & 0 deletions packages/css/src/components/field/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!-- @license CC0-1.0 -->

# 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.
16 changes: 16 additions & 0 deletions packages/css/src/components/field/field.scss
Original file line number Diff line number Diff line change
@@ -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);
}
1 change: 1 addition & 0 deletions packages/css/src/components/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

/* Append here */
@import "./field/field";
@import "./select/select";
@import "./time-input/time-input";
@import "./date-input/date-input";
Expand Down
44 changes: 44 additions & 0 deletions packages/react/src/Field/Field.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<Field />)
const component = container.querySelector(':only-child')

expect(component).toBeInTheDocument()
expect(component).toBeVisible()
})

it('renders a design system BEM class name', () => {
const { container } = render(<Field />)
const component = container.querySelector(':only-child')

expect(component).toHaveClass('ams-field')
})

it('renders an additional class name', () => {
const { container } = render(<Field className="extra" />)
const component = container.querySelector(':only-child')

expect(component).toHaveClass('ams-field extra')
})

it('renders with the error class', () => {
const { container } = render(<Field invalid />)
const component = container.querySelector(':only-child')

expect(component).toHaveClass('ams-field--invalid')
})

it('supports ForwardRef in React', () => {
const ref = createRef<HTMLDivElement>()

const { container } = render(<Field ref={ref} />)
const component = container.querySelector(':only-child')

expect(ref.current).toBe(component)
})
})
23 changes: 23 additions & 0 deletions packages/react/src/Field/Field.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLAttributes<HTMLDivElement>>

export const Field = forwardRef(
({ children, className, invalid, ...restProps }: FieldProps, ref: ForwardedRef<HTMLDivElement>) => (
<div {...restProps} ref={ref} className={clsx('ams-field', invalid && 'ams-field--invalid', className)}>
{children}
</div>
),
)

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

# React Field component

[Field documentation](../../../css/src/components/field/README.md)
2 changes: 2 additions & 0 deletions packages/react/src/Field/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Field } from './Field'
export type { FieldProps } from './Field'
1 change: 1 addition & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

/* Append here */
export * from './Field'
export * from './Select'
export * from './TimeInput'
export * from './DateInput'
Expand Down
17 changes: 17 additions & 0 deletions proprietary/tokens/src/components/ams/field.tokens.json
Original file line number Diff line number Diff line change
@@ -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}"
}
}
}
}
}
17 changes: 17 additions & 0 deletions storybook/src/components/Field/Field.docs.mdx
Original file line number Diff line number Diff line change
@@ -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";

<Meta of={FieldStories} />

<Markdown>{README}</Markdown>

<Primary />

<Controls />

## With Error

A Field can indicate if the contained input has a validation error.

<Canvas of={FieldStories.WithError} />
44 changes: 44 additions & 0 deletions storybook/src/components/Field/Field.stories.tsx
Original file line number Diff line number Diff line change
@@ -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) => (
<Field invalid={args.invalid}>
<Label htmlFor="input1">Waar gaat het om?</Label>
<Paragraph id="description1" size="small">
Typ geen persoonsgegevens in deze omschrijving. We vragen dit later in dit formulier aan u.
</Paragraph>
<TextInput id="input1" aria-describedby="description1" aria-invalid={args.invalid ? true : undefined} />
</Field>
),
} satisfies Meta<typeof Field>

export default meta

type Story = StoryObj<typeof meta>

export const Default: Story = {}

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

0 comments on commit 66832aa

Please sign in to comment.