Skip to content

Commit

Permalink
feat: Allow various types of text input (#1259)
Browse files Browse the repository at this point in the history
Co-authored-by: Ruben Sibon <[email protected]>
Co-authored-by: Aram <[email protected]>
  • Loading branch information
3 people authored Jun 21, 2024
1 parent e5f8d58 commit dc1e5d5
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 9 deletions.
4 changes: 2 additions & 2 deletions packages/css/src/components/text-input/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ A form field in which a user can enter text.
- Use a Text Input when users need to enter a single line of text, such as their name or phone number.
- Do not use a Text Input when users could provide more than 1 sentence of text.
- The width of the Text Input should be appropriate for the information to be entered.
- A Text Input must have a label, and in most cases, this label should be visible.
- A Text Input must have a Label, and in most cases, this label should be visible.
- Use `spellcheck="false"` for fields that may contain sensitive information, such as passwords and personal data.
Some browser extensions for spell-checking send this information to external servers.
- Apply automatic assistance where possible.
For example, in logged-in systems, pre-filling input can prevent errors and save time.
For example, in logged-in systems, pre-filling known values can prevent errors and save time.
14 changes: 13 additions & 1 deletion packages/react/src/DateInput/DateInput.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { render } from '@testing-library/react'
import { createRef } from 'react'
import { DateInput } from './DateInput'
import { DateInput, dateInputTypes } from './DateInput'
import '@testing-library/jest-dom'

describe('Date input', () => {
Expand Down Expand Up @@ -65,4 +65,16 @@ describe('Date input', () => {
expect(component).not.toHaveAttribute('aria-invalid')
})
})

describe('Type', () => {
dateInputTypes.map((type) =>
it(`sets the ‘${type}’ type`, () => {
const { container } = render(<DateInput type={type} />)

const component = container.querySelector(':only-child')

expect(component).toHaveAttribute('type', type)
}),
)
})
})
6 changes: 5 additions & 1 deletion packages/react/src/DateInput/DateInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ import clsx from 'clsx'
import { forwardRef } from 'react'
import type { ForwardedRef, InputHTMLAttributes } from 'react'

export const dateInputTypes = ['date', 'datetime-local'] as const

type DateInputType = (typeof dateInputTypes)[number]

export type DateInputProps = {
/** Whether the value fails a validation rule. */
invalid?: boolean
/** The kind of data that the user should provide. */
type?: 'date' | 'datetime-local'
type?: DateInputType
} & Omit<InputHTMLAttributes<HTMLInputElement>, 'aria-invalid' | 'type'>

export const DateInput = forwardRef(
Expand Down
31 changes: 30 additions & 1 deletion packages/react/src/TextInput/TextInput.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { createRef, useState } from 'react'
import { TextInput } from './TextInput'
import { TextInput, textInputTypes } from './TextInput'
import { Label } from '../Label'
import '@testing-library/jest-dom'

describe('Text input', () => {
Expand Down Expand Up @@ -116,4 +117,32 @@ describe('Text input', () => {
expect(component).not.toHaveAttribute('aria-invalid')
})
})

describe('Type', () => {
textInputTypes
.filter((type) => type !== 'password')
.map((type) =>
it(`sets the ‘${type}’ type`, () => {
render(<TextInput type={type} />)

const component = screen.getByRole('textbox')

expect(component).toHaveAttribute('type', type)
}),
)

// https://github.com/testing-library/dom-testing-library/issues/567
it('sets the ‘password’ type', () => {
render(
<>
<Label htmlFor="password-field">Password</Label>
<TextInput id="password-field" type="password" />
</>,
)

const component = screen.getByLabelText(/password/i)

expect(component).toHaveAttribute('type', 'password')
})
})
})
10 changes: 8 additions & 2 deletions packages/react/src/TextInput/TextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,26 @@ import clsx from 'clsx'
import { forwardRef } from 'react'
import type { ForwardedRef, InputHTMLAttributes } from 'react'

export const textInputTypes = ['email', 'password', 'tel', 'text', 'url'] as const

type TextInputType = (typeof textInputTypes)[number]

export type TextInputProps = {
/** Whether the value fails a validation rule. */
invalid?: boolean
/** The kind of data that the user should provide. */
type?: TextInputType
} & Omit<InputHTMLAttributes<HTMLInputElement>, 'aria-invalid'>

export const TextInput = forwardRef(
({ className, dir, invalid, ...restProps }: TextInputProps, ref: ForwardedRef<HTMLInputElement>) => (
({ className, dir, invalid, type = 'text', ...restProps }: TextInputProps, ref: ForwardedRef<HTMLInputElement>) => (
<input
{...restProps}
aria-invalid={invalid || undefined}
className={clsx('ams-text-input', className)}
dir={dir ?? 'auto'}
ref={ref}
type="text"
type={type}
/>
),
)
Expand Down
118 changes: 118 additions & 0 deletions storybook/src/components/TextInput/TextInput.docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,132 @@ import README from "../../../../packages/css/src/components/text-input/README.md

## Examples

### Type: Password

Creates an input where the user can enter a password.
The characters entered are hidden, usually represented by dots or asterisks, so that the actual text is not visible.

Use the password type when the input requires sensitive information, like passwords or PINs.
It ensures that the input is not readable by others who might be looking at the screen.

Consider setting the following attributes:

1. Allow the user’s password manager to automatically fill the password through `autocomplete="current-password"`.
When asking for a new password, use `autocomplete="new-password"` instead.
2. Add a `minlength` attribute to ensure passwords meet a minimum length requirement.
Do not add a `maxlength` attribute.
3. Use the `pattern` attribute to enforce password policies, like including numbers and special characters.
Describe these policies in the [Field](?path=/docs/components-forms-field--docs)’s description as well.
4. If the password is a numeric PIN, add `inputmode="numeric"`.
Devices with virtual keyboards then switch to a numeric keypad layout which makes entering the password easier.
5. Set `autocapitalize="none"`, `autocorrect="off"` and `spellcheck="false"` to stop browsers automatically changing user input.
Passwords shouldn’t be checked for spelling or grammar.
This may also prevent posting the password to third-party plugins.

Follow the [guidelines for asking for passwords](https://design-system.service.gov.uk/patterns/passwords/) of the GOV.UK Design System.

<Canvas of={TextInputStories.Password} />

### Type: Email address

This field helps the user enter an email address.
It has built-in validation to check for a valid email format.
However, do not rely only on the browser’s validation.

The `email` input field looks like a standard text input.
On some devices, it may show an email-specific keyboard.

Consider setting the following attributes:

1. Set `autocomplete="email"` to help browsers autofill the user’s email address.
2. Set `autocapitalize="none"`, `autocorrect="off"` and `spellcheck="false"` to stop browsers automatically changing user input.
Email addresses shouldn’t be checked for spelling or grammar.
This may also prevent posting the email address to third-party plugins.

Follow the [guidelines for asking for email addresses](https://design-system.service.gov.uk/patterns/email-addresses/) of the GOV.UK Design System.

<Canvas of={TextInputStories.EmailAddress} />

### Type: Web address

This field helps the user enter a web address or URL.
It has built-in validation to check for a valid format.
However, do not rely only on the browser’s validation.

The `url` input field looks like a standard text input.
On some devices, it may show a URL-specific keyboard to aid in entering web addresses.

Consider setting the following attributes:

1. Set `autocomplete="url"` to help browsers autofill the user’s web address.
2. Set `autocapitalize="none"`, `autocorrect="off"` and `spellcheck="false"` to stop browsers automatically changing user input.
Email addresses shouldn’t be checked for spelling or grammar.
This may also prevent posting the web address to third-party plugins.

<Canvas of={TextInputStories.WebAddress} />

### Type: Phone number

This field helps the user enter a phone number.
It has built-in validation to check for a valid format.
However, do not rely only on the browser’s validation.

The `tel` input field looks like a standard text input.
On mobile devices, it may display a numeric keypad or a keyboard optimized for entering phone numbers.

Guidelines:

1. Set `autocomplete="tel"` to help browsers autofill the user’s phone number.

Follow the [guidelines for asking for telephone numbers](https://design-system.service.gov.uk/patterns/telephone-numbers/) of the GOV.UK Design System.

<Canvas of={TextInputStories.PhoneNumber} />

### Placeholder

This text appears in the field when it is empty. It can give a brief hint about the kind of data to enter.

Don’t try too hard to find a suitable text for a placeholder.
Inputs without placeholder text are just fine – the label and a description should be clear enough.
Do not use a placeholder instead of a [Label](?path=/docs/components-forms-label--docs).

Follow the [guidelines for placeholder text](https://www.nldesignsystem.nl/richtlijnen/formulieren/placeholders) by NL Design System.

<Canvas of={TextInputStories.Placeholder} />

### Invalid

Indicates that the input value does not meet the specified constraints.
An [Error Message](?path=/docs/components-forms-error-message--docs) must be displayed above the field.
To highlight the error even more, the parent [Field](?path=/docs/components-forms-field--docs) component’s `invalid` prop must also be set.

<Canvas of={TextInputStories.Invalid} />

### Disabled

A field that can not (yet) be used is indicated with a grey border.
It will not respond to interactions, e.g. with the mouse or keyboard.

Avoid disabling input fields.
They cause usability and accessibility problems.

Disabled fields are often skipped by screen readers.
This makes it hard for the user who rely on assistive technologies to understand the form’s content.
They are not included in form submissions, which can lead to incomplete or missing data.

Alternatives:

1. Use the `readonly` attribute.
This makes a field uneditable but keeps it accessible and included in form submissions.
2. Display data as plain text or within a non-input element like a `span` or `div`.
3. Use conditional logic to hide or show fields based on user interaction.
This ensures all necessary fields are accessible and editable when needed.
4. Use a label or text to explain why a field is not editable if you must disable it temporarily.
This helps the user understand the context.

<Canvas of={TextInputStories.Disabled} />

## Further reading

- Extensive [guidelines for all types of input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/text) on Mozilla Developer Network.
- An overview of [suitable metadata for various kinds of content](https://nl-design-system.github.io/utrecht/storybook/?path=/docs/react_react-textbox--docs) to be entered by Gemeente Utrecht.
34 changes: 32 additions & 2 deletions storybook/src/components/TextInput/TextInput.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ const meta = {
args: {
disabled: false,
invalid: false,
value: '',
},
argTypes: {
disabled: {
Expand All @@ -33,20 +32,51 @@ type Story = StoryObj<typeof meta>

export const Default: Story = {}

export const Password: Story = {
args: {
minLength: 8,
type: 'password',
value: 'password',
},
}

export const EmailAddress: Story = {
args: {
type: 'email',
value: '[email protected]',
},
}

export const WebAddress: Story = {
args: {
type: 'url',
value: 'https://designsystem.amsterdam/',
},
}

export const PhoneNumber: Story = {
args: {
type: 'tel',
value: '14020',
},
}

export const Placeholder: Story = {
args: {
placeholder: 'E-mail',
placeholder: 'Placeholder text',
},
}

export const Invalid: Story = {
args: {
invalid: true,
value: 'Invalid value',
},
}

export const Disabled: Story = {
args: {
disabled: true,
value: 'Disabled input',
},
}

0 comments on commit dc1e5d5

Please sign in to comment.