-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add Search Field component (#892)
Co-authored-by: Vincent Smedinga <[email protected]>
- Loading branch information
1 parent
9afe482
commit dd1a437
Showing
16 changed files
with
532 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
# Search Field | ||
|
||
Met een Search Field (in het Nederlands: zoekveld) kunnen gebruikers snel relevante inhoud vinden. Een gebruiker voert een (deel van een) woord of zin in om daarmee inhoud te doorzoeken. | ||
|
||
## Richtlijnen | ||
|
||
- Je kunt een zoekactie starten door de zoekknop of de Enter-toets te gebruiken. | ||
- Als er geen zoekterm is ingevuld kan geen zoekactie gestart worden. | ||
|
||
## Autofocus | ||
|
||
Je kunt `autofocus` gebruiken om de focus gelijk in een zoekveld te plaatsen als de gebruiker op een pagina komt. | ||
Wees hier wel voorzichtig mee, dit kan negatieve gevolgen hebben: | ||
|
||
- Voor gebruikers van een schermlezer kan dit betekenen dat ze belangrijk inhoud boven het zoekveld overslaan. | ||
- Op kleinere apparaten kan het gebruik van `autofocus` er voor zorgen dat de pagina automatisch naar het zoekveld scrollt, waardoor je eerdere inhoud kan missen. | ||
- Op apparaten met touchscreen kan dit ervoor zorgen dat het toetsenbord gelijk wordt getoond, waardoor belangrijke inhoud verborgen wordt. | ||
|
||
Gebruik `autofocus` alleen als het zoekveld aan het begin van een pagina staat, en er geen andere elementen op een pagina staan waar een gebruiker misschien eerst gebruik van wil maken. | ||
|
||
Voor meer informatie: [Accessibility Tips: Be Cautious When Using Autofocus](https://www.boia.org/blog/accessibility-tips-be-cautious-when-using-autofocus) | ||
|
||
## Autocomplete en spellcheck | ||
|
||
`autocomplete` en `spellcheck` staan standaard uit. Deze functies kunnen vervelend zijn voor een gebruiker die zoekt op een deel van een woord, en `autocomplete` kan in de weg zitten van een Autosuggest component. | ||
|
||
## Relevante WCAG eisen | ||
|
||
- [WCAG 1.3.1](https://www.w3.org/TR/WCAG22/#info-and-relationships): `role="search"` wordt gebruikt voor de search landmark role. | ||
- [WCAG 1.3.5](https://www.w3.org/TR/WCAG22/#identify-input-purpose): het is zowel voor een gebruiker als programmatisch duidelijk wat het doel van een formulierveld is. | ||
- [WCAG 2.4.6](https://www.w3.org/TR/WCAG22/#headings-and-labels): er is een label dat het doel van de input beschrijft. | ||
|
||
Search Field is een interactief element, hier gelden [de algemene eisen en richtlijnen voor interactieve elementen](https://amsterdam.github.io/design-system/?path=/docs/docs-designrichtlijnen-interactieve-elementen--docs) voor. |
81 changes: 81 additions & 0 deletions
81
packages/css/src/components/search-field/search-field.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
/** | ||
* @license EUPL-1.2+ | ||
* Copyright (c) 2023 Gemeente Amsterdam | ||
*/ | ||
|
||
.amsterdam-search-field { | ||
display: flex; | ||
isolation: isolate; // create new stacking context, so the input's z-index doesn't escape the component | ||
} | ||
|
||
@mixin reset { | ||
box-sizing: border-box; | ||
margin-block: 0; | ||
-webkit-text-size-adjust: 100%; | ||
} | ||
|
||
.amsterdam-search-field__input { | ||
border: none; | ||
box-shadow: var(--amsterdam-search-field-input-box-shadow); | ||
color: var(--amsterdam-search-field-input-color); | ||
font-family: var(--amsterdam-search-field-input-font-family); | ||
font-size: var(--amsterdam-search-field-input-spacious-font-size); | ||
font-weight: var(--amsterdam-search-field-input-font-weight); | ||
line-height: var(--amsterdam-search-field-input-spacious-line-height); | ||
outline-offset: var(--amsterdam-search-field-input-outline-offset); | ||
padding-block: 0.5rem; | ||
padding-inline: 1rem; | ||
touch-action: manipulation; | ||
width: 100%; | ||
|
||
@include reset; | ||
|
||
.amsterdam-theme--compact & { | ||
font-size: var(--amsterdam-search-field-input-compact-font-size); | ||
line-height: var(--amsterdam-search-field-input-compact-line-height); | ||
} | ||
|
||
&:hover { | ||
box-shadow: var(--amsterdam-search-field-input-hover-box-shadow); | ||
} | ||
|
||
&:focus { | ||
z-index: 1; // Make sure the focus outline isn't cut off by the adjacent button | ||
} | ||
} | ||
|
||
.amsterdam-search-field__input::placeholder { | ||
color: var(--amsterdam-search-field-input-placeholder-color); | ||
} | ||
|
||
.amsterdam-search-field__input::-webkit-search-cancel-button { | ||
appearance: none; | ||
background-image: var(--amsterdam-search-field-input-cancel-button-background-image); | ||
cursor: pointer; | ||
height: var(--amsterdam-search-field-input-cancel-button-height); | ||
margin-inline-start: 0.5rem; | ||
width: var(--amsterdam-search-field-input-cancel-button-width); | ||
} | ||
|
||
.amsterdam-search-field__button { | ||
background-color: var(--amsterdam-search-field-button-background-color); | ||
border: 0; | ||
color: var(--amsterdam-search-field-button-color); | ||
cursor: pointer; | ||
outline-offset: var(--amsterdam-search-field-button-outline-offset); | ||
padding-block: 0.5rem; | ||
padding-inline: 0.5rem; | ||
touch-action: manipulation; | ||
|
||
span { | ||
display: flex; | ||
justify-content: center; | ||
|
||
// use icon tokens to set width equal to height | ||
width: calc(var(--amsterdam-icon-spacious-size-6-font-size) * var(--amsterdam-icon-spacious-size-6-line-height)); | ||
} | ||
|
||
&:hover { | ||
background-color: var(--amsterdam-search-field-button-hover-background-color); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# React Search Field component | ||
|
||
[Search Field documentation](../../../css/src/search-field/README.md) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import { render, screen } from '@testing-library/react' | ||
import { createRef } from 'react' | ||
import { SearchField } from './SearchField' | ||
import '@testing-library/jest-dom' | ||
|
||
describe('Search field', () => { | ||
it('renders the outer container', () => { | ||
render(<SearchField />) | ||
|
||
const component = screen.getByRole('search') | ||
|
||
expect(component).toBeInTheDocument() | ||
expect(component).toBeVisible() | ||
}) | ||
|
||
it('renders the input', () => { | ||
render(<SearchField.Input />) | ||
|
||
const component = screen.getByRole('searchbox') | ||
|
||
expect(component).toBeInTheDocument() | ||
expect(component).toBeVisible() | ||
}) | ||
|
||
it('renders the button', () => { | ||
render(<SearchField.Button />) | ||
|
||
const component = screen.getByRole('button') | ||
|
||
expect(component).toBeInTheDocument() | ||
expect(component).toBeVisible() | ||
}) | ||
|
||
it('renders the outer container design system BEM class name', () => { | ||
render(<SearchField />) | ||
|
||
const component = screen.getByRole('search') | ||
|
||
expect(component).toHaveClass('amsterdam-search-field') | ||
}) | ||
|
||
it('renders an additional class name', () => { | ||
render(<SearchField className="extra" />) | ||
|
||
const component = screen.getByRole('search') | ||
|
||
expect(component).toHaveClass('extra') | ||
|
||
expect(component).toHaveClass('amsterdam-search-field') | ||
}) | ||
|
||
it('supports ForwardRef in React', () => { | ||
const ref = createRef<HTMLFormElement>() | ||
|
||
render(<SearchField ref={ref} />) | ||
|
||
const component = screen.getByRole('search') | ||
|
||
expect(ref.current).toBe(component) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
/** | ||
* @license EUPL-1.2+ | ||
* Copyright (c) 2023 Gemeente Amsterdam | ||
*/ | ||
|
||
import clsx from 'clsx' | ||
import { | ||
ForwardedRef, | ||
forwardRef, | ||
ForwardRefExoticComponent, | ||
HTMLAttributes, | ||
PropsWithChildren, | ||
RefAttributes, | ||
} from 'react' | ||
import { SearchFieldButton } from './SearchFieldButton' | ||
import { SearchFieldInput } from './SearchFieldInput' | ||
|
||
export interface SearchFieldProps extends PropsWithChildren<HTMLAttributes<HTMLFormElement>> {} | ||
|
||
export interface SearchFieldComponent | ||
extends ForwardRefExoticComponent<SearchFieldProps & RefAttributes<HTMLFormElement>> { | ||
Input: typeof SearchFieldInput | ||
Button: typeof SearchFieldButton | ||
} | ||
|
||
export const SearchField = forwardRef( | ||
({ children, className, ...restProps }: SearchFieldProps, ref: ForwardedRef<HTMLFormElement>) => { | ||
return ( | ||
<form role="search" {...restProps} ref={ref} className={clsx('amsterdam-search-field', className)}> | ||
{children} | ||
</form> | ||
) | ||
}, | ||
) as SearchFieldComponent | ||
|
||
SearchField.Input = SearchFieldInput | ||
SearchField.Button = SearchFieldButton | ||
SearchField.displayName = 'SearchField' | ||
SearchField.Input.displayName = 'SearchField.Input' | ||
SearchField.Button.displayName = 'SearchField.Button' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
/** | ||
* @license EUPL-1.2+ | ||
* Copyright (c) 2023 Gemeente Amsterdam | ||
*/ | ||
|
||
import { SearchIcon } from '@amsterdam/design-system-react-icons' | ||
import clsx from 'clsx' | ||
import { ForwardedRef, forwardRef, HTMLAttributes } from 'react' | ||
import { Icon } from '../Icon' | ||
import { VisuallyHidden } from '../VisuallyHidden' | ||
|
||
interface SearchFieldButtonProps extends HTMLAttributes<HTMLButtonElement> {} | ||
|
||
// TODO: replace this with IconButton when that's done | ||
export const SearchFieldButton = forwardRef( | ||
({ className, ...restProps }: SearchFieldButtonProps, ref: ForwardedRef<HTMLButtonElement>) => ( | ||
<button {...restProps} ref={ref} className={clsx('amsterdam-search-field__button', className)}> | ||
<VisuallyHidden>Zoeken</VisuallyHidden> | ||
<Icon svg={SearchIcon} size="level-6" /> | ||
</button> | ||
), | ||
) | ||
|
||
SearchFieldButton.displayName = 'SearchFieldButton' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import { render, screen } from '@testing-library/react' | ||
import userEvent from '@testing-library/user-event' | ||
import { createRef, useState } from 'react' | ||
import { SearchFieldInput } from './SearchFieldInput' | ||
import '@testing-library/jest-dom' | ||
|
||
describe('Search field input', () => { | ||
it('renders', () => { | ||
render(<SearchFieldInput />) | ||
|
||
const component = screen.getByRole('searchbox', { name: 'Zoeken' }) | ||
|
||
expect(component).toBeInTheDocument() | ||
expect(component).toBeVisible() | ||
}) | ||
|
||
it('renders a design system BEM class name', () => { | ||
render(<SearchFieldInput />) | ||
|
||
const component = screen.getByRole('searchbox', { name: 'Zoeken' }) | ||
|
||
expect(component).toHaveClass('amsterdam-search-field__input') | ||
}) | ||
|
||
it('renders an additional class name', () => { | ||
render(<SearchFieldInput className="extra" />) | ||
|
||
const component = screen.getByRole('searchbox', { name: 'Zoeken' }) | ||
|
||
expect(component).toHaveClass('extra') | ||
|
||
expect(component).toHaveClass('amsterdam-search-field__input') | ||
}) | ||
|
||
it('supports a custom label', () => { | ||
render(<SearchFieldInput label="Test label" />) | ||
|
||
const component = screen.getByRole('searchbox', { name: 'Test label' }) | ||
|
||
expect(component).toBeInTheDocument() | ||
expect(component).toBeVisible() | ||
}) | ||
|
||
it('should be working in a controlled state', async () => { | ||
function ControlledComponent() { | ||
const [value, setValue] = useState('Hello') | ||
|
||
return <SearchFieldInput value={value} onChange={(e) => setValue(e.target.value)} /> | ||
} | ||
|
||
render(<ControlledComponent />) | ||
|
||
const componentText = screen.getByDisplayValue('Hello') | ||
expect(componentText).toBeInTheDocument() | ||
|
||
const component = screen.getByRole('searchbox', { name: 'Zoeken' }) | ||
if (component) { | ||
await userEvent.type(component, ', World!') | ||
} | ||
|
||
const newComponentText = screen.getByDisplayValue('Hello, World!') | ||
expect(newComponentText).toBeInTheDocument() | ||
}) | ||
|
||
it('supports ForwardRef in React', () => { | ||
const ref = createRef<HTMLInputElement>() | ||
|
||
render(<SearchFieldInput ref={ref} />) | ||
|
||
const component = screen.getByRole('searchbox', { name: 'Zoeken' }) | ||
|
||
expect(ref.current).toBe(component) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
/** | ||
* @license EUPL-1.2+ | ||
* Copyright (c) 2023 Gemeente Amsterdam | ||
*/ | ||
|
||
import clsx from 'clsx' | ||
import { ForwardedRef, forwardRef, InputHTMLAttributes, useId } from 'react' | ||
import { VisuallyHidden } from '../VisuallyHidden' | ||
|
||
interface SearchFieldInputProps extends InputHTMLAttributes<HTMLInputElement> { | ||
label?: string | ||
} | ||
|
||
export const SearchFieldInput = forwardRef( | ||
({ className, label = 'Zoeken', ...restProps }: SearchFieldInputProps, ref: ForwardedRef<HTMLInputElement>) => { | ||
const id = useId() | ||
|
||
return ( | ||
<> | ||
<label htmlFor={id}> | ||
<VisuallyHidden>{label}</VisuallyHidden> | ||
</label> | ||
<input | ||
{...restProps} | ||
autoComplete="off" | ||
className={clsx('amsterdam-search-field__input', className)} | ||
enterKeyHint="search" | ||
id={id} | ||
ref={ref} | ||
spellCheck="false" | ||
type="search" | ||
/> | ||
</> | ||
) | ||
}, | ||
) | ||
|
||
SearchFieldInput.displayName = 'SearchFieldInput' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export { SearchField } from './SearchField' | ||
export type { SearchFieldProps } from './SearchField' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.