Skip to content

Commit

Permalink
feat: Select component (#1151)
Browse files Browse the repository at this point in the history
Co-authored-by: Vincent Smedinga <v.smedinga@amsterdam.nl>
Co-authored-by: Aram <37216945+alimpens@users.noreply.github.com>
  • Loading branch information
3 people authored Apr 19, 2024
1 parent ccec68e commit 7679acc
Show file tree
Hide file tree
Showing 15 changed files with 548 additions and 0 deletions.
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 "./select/select";
@import "./time-input/time-input";
@import "./date-input/date-input";
@import "./document/document";
Expand Down
9 changes: 9 additions & 0 deletions packages/css/src/components/select/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!-- @license CC0-1.0 -->

# Select

A form control that allows users to select one or more options from a list.

## References

- [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select)
60 changes: 60 additions & 0 deletions packages/css/src/components/select/select.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/

@mixin reset {
appearance: none;
border: 0;
border-radius: 0; // Reset rounded borders for Safari on MacOS
}

.ams-select {
background-color: var(--ams-select-background-color);
box-shadow: var(--ams-select-box-shadow);
color: var(--ams-select-color);
font-family: var(--ams-select-font-family);
font-size: var(--ams-select-font-size);
font-weight: var(--ams-select-font-weight);
line-height: var(--ams-select-line-height);
max-inline-size: 100%;
outline-offset: var(--ams-select-outline-offset);
padding-block: var(--ams-select-padding-block);
padding-inline: var(--ams-select-padding-inline);
touch-action: manipulation;

&:not([multiple]) {
background-image: var(--ams-select-background-image);
background-position: var(--ams-select-background-position);
background-repeat: no-repeat;
background-size: 1em 1em;
}

&:hover {
box-shadow: var(--ams-select-hover-box-shadow);
}

@include reset;
}

.ams-select[aria-invalid="true"] {
box-shadow: var(--ams-select-invalid-box-shadow);

&:hover {
box-shadow: var(--ams-select-invalid-hover-box-shadow);
}
}

.ams-select:disabled {
box-shadow: var(--ams-select-disabled-box-shadow);
color: var(--ams-select-disabled-color);
cursor: not-allowed;

&:not([multiple]) {
background-image: var(--ams-select-disabled-background-image);
}
}

.ams-select__option:disabled {
color: var(--ams-select-option-disabled-color);
}
5 changes: 5 additions & 0 deletions packages/react/src/Select/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!-- @license CC0-1.0 -->

# React Select component

[Select documentation](../../../css/src/components/select/README.md)
90 changes: 90 additions & 0 deletions packages/react/src/Select/Select.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { render, screen } from '@testing-library/react'
import { createRef } from 'react'
import { Select } from './Select'
import '@testing-library/jest-dom'

describe('Select', () => {
it('renders', () => {
render(<Select />)

const component = screen.getByRole('combobox')

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

it('renders a design system BEM class name', () => {
render(<Select />)

const component = screen.getByRole('combobox')

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

it('renders an additional class name', () => {
render(<Select className="extra" />)

const component = screen.getByRole('combobox')

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

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

render(<Select ref={ref} />)

const component = screen.getByRole('combobox')

expect(ref.current).toBe(component)
})

it('renders options', () => {
render(
<Select>
<Select.Option value="a">Option A</Select.Option>
<Select.Option value="b">Option B</Select.Option>
</Select>,
)

const select = screen.getByRole('combobox')

const option = screen.getByRole('option', {
name: 'Option B',
})

expect(select).toContain(option)
})

it('can be disabled', () => {
render(<Select disabled />)

const component = screen.getByRole('combobox')

expect(component).toBeDisabled()
})

it('can be invalid', () => {
render(<Select invalid />)

const component = screen.getByRole('combobox')

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

it('is not required by default', () => {
render(<Select />)

const component = screen.getByRole('combobox')

expect(component).not.toBeRequired()
})

it('omits the required attribute when not required', () => {
render(<Select required={false} />)

const component = screen.getByRole('combobox')

expect(component).not.toHaveAttribute('required')
})
})
32 changes: 32 additions & 0 deletions packages/react/src/Select/Select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/

import clsx from 'clsx'
import { forwardRef } from 'react'
import type { ForwardedRef, PropsWithChildren, SelectHTMLAttributes } from 'react'
import { SelectOption } from './SelectOption'
import { SelectOptionGroup } from './SelectOptionGroup'

export type SelectProps = {
/** There is no native invalid attribute for select, but you can use this to get the same result as other form components */
invalid?: boolean
} & PropsWithChildren<SelectHTMLAttributes<HTMLSelectElement>>

const SelectRoot = forwardRef(
({ children, className, invalid, ...restProps }: SelectProps, ref: ForwardedRef<HTMLSelectElement>) => (
<select
{...restProps}
ref={ref}
className={clsx('ams-select', invalid && 'ams-select--invalid', className)}
aria-invalid={invalid || undefined}
>
{children}
</select>
),
)

SelectRoot.displayName = 'Select'

export const Select = Object.assign(SelectRoot, { Option: SelectOption, Group: SelectOptionGroup })
59 changes: 59 additions & 0 deletions packages/react/src/Select/SelectOption.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { render, screen } from '@testing-library/react'
import { createRef } from 'react'
import { Select } from './Select'
import '@testing-library/jest-dom'

describe('Select option', () => {
it('renders', () => {
render(<Select.Option />)

const component = screen.getByRole('option')

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

it('renders an option role element with a text label', () => {
render(<Select.Option>Option</Select.Option>)

const option = screen.getByRole('option', {
name: 'Option',
})

expect(option).toBeInTheDocument()
})

it('renders a design system BEM class name', () => {
render(<Select.Option />)

const component = screen.getByRole('option')

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

it('renders an additional class name', () => {
render(<Select.Option className="extra" />)

const component = screen.getByRole('option')

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

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

render(<Select.Option ref={ref} />)

const component = screen.getByRole('option')

expect(ref.current).toBe(component)
})

it('can be disabled', () => {
render(<Select.Option disabled />)

const component = screen.getByRole('option')

expect(component).toBeDisabled()
})
})
23 changes: 23 additions & 0 deletions packages/react/src/Select/SelectOption.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, OptionHTMLAttributes, PropsWithChildren } from 'react'

export type SelectOptionProps = OptionHTMLAttributes<HTMLOptionElement>

export const SelectOption = forwardRef(
(
{ children, className, ...restProps }: PropsWithChildren<SelectOptionProps>,
ref: ForwardedRef<HTMLOptionElement>,
) => (
<option {...restProps} ref={ref} className={clsx('ams-select__option', className)}>
{children}
</option>
),
)

SelectOption.displayName = 'Select.Option'
59 changes: 59 additions & 0 deletions packages/react/src/Select/SelectOptionGroup.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { render, screen } from '@testing-library/react'
import { createRef } from 'react'
import { Select } from './Select'
import '@testing-library/jest-dom'

describe('Select option group', () => {
it('renders', () => {
render(<Select.Group />)

const component = screen.getByRole('group')

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

it('renders an group role element with a text label', () => {
render(<Select.Group label="Options" />)

const option = screen.getByRole('group', {
name: 'Options',
})

expect(option).toBeInTheDocument()
})

it('renders a design system BEM class name', () => {
render(<Select.Group />)

const component = screen.getByRole('group')

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

it('renders an additional class name', () => {
render(<Select.Group className="extra" />)

const component = screen.getByRole('group')

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

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

render(<Select.Group ref={ref} />)

const component = screen.getByRole('group')

expect(ref.current).toBe(component)
})

it('can be disabled', () => {
render(<Select.Group disabled />)

const component = screen.getByRole('group')

expect(component).toBeDisabled()
})
})
23 changes: 23 additions & 0 deletions packages/react/src/Select/SelectOptionGroup.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, OptgroupHTMLAttributes, PropsWithChildren } from 'react'

export type SelectOptionGroupProps = OptgroupHTMLAttributes<HTMLOptGroupElement>

export const SelectOptionGroup = forwardRef(
(
{ children, className, ...restProps }: PropsWithChildren<SelectOptionGroupProps>,
ref: ForwardedRef<HTMLOptGroupElement>,
) => (
<optgroup {...restProps} ref={ref} className={clsx('ams-select__group', className)}>
{children}
</optgroup>
),
)

SelectOptionGroup.displayName = 'Select.Group'
3 changes: 3 additions & 0 deletions packages/react/src/Select/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { Select } from './Select'
export type { SelectProps } from './Select'
export type { SelectOptionProps } from './SelectOption'
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 './Select'
export * from './TimeInput'
export * from './DateInput'
export * from './Avatar'
Expand Down
Loading

0 comments on commit 7679acc

Please sign in to comment.