Skip to content

Commit

Permalink
feat: Add Field component
Browse files Browse the repository at this point in the history
  • Loading branch information
alimpens committed May 15, 2024
1 parent d264258 commit 5e3466e
Show file tree
Hide file tree
Showing 11 changed files with 173 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 around a single input and can indicate if that 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.
13 changes: 13 additions & 0 deletions packages/css/src/components/field/field.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/

.ams-field {
break-inside: avoid;
}

.ams-field__has-error {
border-inline-start: var(--ams-field-has-error-border-inline-start);
padding-inline-start: var(--ams-field-has-error-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 hasError />)
const component = container.querySelector(':only-child')

expect(component).toHaveClass('ams-field__has-error')
})

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 */
hasError?: boolean
} & PropsWithChildren<HTMLAttributes<HTMLDivElement>>

export const Field = forwardRef(
({ children, className, hasError, ...restProps }: FieldProps, ref: ForwardedRef<HTMLDivElement>) => (
<div {...restProps} ref={ref} className={clsx('ams-field', hasError && 'ams-field__has-error', 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
14 changes: 14 additions & 0 deletions proprietary/tokens/src/components/ams/field.tokens.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"ams": {
"field": {
"has-error": {
"border-inline-start": {
"value": "3px 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: {
hasError: false,
},
render: (args) => (
<Field hasError={args.hasError}>
<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.hasError ? true : undefined} />
</Field>
),
} satisfies Meta<typeof Field>

export default meta

type Story = StoryObj<typeof meta>

export const Default: Story = {}

export const WithError: Story = {
args: { hasError: true },
render: (args) => (
<Field hasError={args.hasError}>
<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.hasError ? true : undefined} />
</Field>
),
}

0 comments on commit 5e3466e

Please sign in to comment.