Skip to content

Commit

Permalink
feat(react): add Combobox component (#1180)
Browse files Browse the repository at this point in the history
  • Loading branch information
scurker authored Sep 6, 2023
1 parent b09c14c commit 4d29227
Show file tree
Hide file tree
Showing 17 changed files with 2,469 additions and 12 deletions.
352 changes: 352 additions & 0 deletions docs/pages/components/Combobox.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,352 @@
---
title: Combobox
description: An input component to display a list of suggested options.
source: https://github.com/dequelabs/cauldron/tree/develop/packages/react/src/components/Combobox/Combobox.tsx
---

import { useState } from 'react'
import { Combobox, ComboboxOption, ComboboxGroup, FieldWrap } from '@deque/cauldron-react'

```js
import {
Combobox,
ComboboxOption,
ComboboxGroup
} from '@deque/cauldron-react'
```

<Note>
Use [FieldWrap](./FieldWrap) to wrap all input components. The `Combobox` component has been designed specifically to only be used when rendered as a child of this component, and may not be completely accessible if rendered outside of `FieldWrap`.
</Note>

## Examples

### With Options

Options can be rendered from an array of options with a label and an optional value by setting the `options` prop:

```jsx example
function ComboboxWithOptionsPropExample() {
const options = [
{ label: 'Red', value: 'Red' },
{ label: 'Orange', value: 'Orange' },
{ label: 'Yellow', value: 'Yellow' },
{ label: 'Green', value: 'Green' },
{ label: 'Blue', value: 'Blue' }
]
return (
<FieldWrap>
<Combobox label="Favorite Color" options={options} />
</FieldWrap>
)
}
```

### With Children

For more advanced scenarios where the rendering of the option label needs to be customized, `ComboboxOption` can be passed in to `Combobox` as `children`.

```jsx example
<FieldWrap>
<Combobox label="Fruit">
<ComboboxOption value="Apple">🍎 Apple</ComboboxOption>
<ComboboxOption value="Banana">🍌 Banana</ComboboxOption>
<ComboboxOption value="Cucumber">🥒 Cucumber</ComboboxOption>
<ComboboxOption value="Orange">🍊 Orange</ComboboxOption>
<ComboboxOption value="Peach">🍑 Peach</ComboboxOption>
<ComboboxOption value="Pear">🍐 Pear</ComboboxOption>
</Combobox>
</FieldWrap>
```

### Option Descriptions

Combobox options can optionally include a description, which can be set via a property on the array of options, or as a property on the `ComboboxOption` component.

```jsx example
<FieldWrap>
<Combobox label="Select a Drink">
<ComboboxOption
description="A monster of a barleywine, with a footprint of caramel and toffee."
value="Bigfoot's Barleywine"
>
Bigfoot's Barleywine
</ComboboxOption>
<ComboboxOption
description="A hoppy explosion of citrus and pine, a true hopocalypse."
value="Hopocalypse IPA"
>
Hopocalypse IPA
</ComboboxOption>
<ComboboxOption
description="A pot of liquid gold at the end of the rainbow."
value="Leprechaun's Gold Lager"
>
Leprechaun's Gold Lager
</ComboboxOption>
<ComboboxOption
description="Delicate, effervescent, with a hint of mythical magic in every sip."
value="Unicorn Tears Saison"
>
Unicorn Tears Saison
</ComboboxOption>
<ComboboxOption
description="Revive your taste buds with this undead burst of citrusy hops."
value="Zombie Zest Pale Ale"
>
Zombie Zest Pale Ale
</ComboboxOption>
</Combobox>
</FieldWrap>
```
### Required
```jsx example
<FieldWrap>
<Combobox label="Is this combobox required?" required>
<ComboboxOption>Yes</ComboboxOption>
<ComboboxOption>No</ComboboxOption>
<ComboboxOption>I'm not sure</ComboboxOption>
</Combobox>
</FieldWrap>
```
### Error
```jsx example
<FieldWrap>
<Combobox
label="Does this combobox have an error?"
error="You must select an option."
required
>
<ComboboxOption>Yes</ComboboxOption>
<ComboboxOption>No</ComboboxOption>
<ComboboxOption>I'm not sure</ComboboxOption>
</Combobox>
</FieldWrap>
```
### Grouping
```jsx example
<FieldWrap>
<Combobox label="Countries">
<ComboboxGroup label="North America">
<ComboboxOption>Canada</ComboboxOption>
<ComboboxOption>Mexico</ComboboxOption>
<ComboboxOption>United States</ComboboxOption>
</ComboboxGroup>
<ComboboxGroup label="South America">
<ComboboxOption>Argentina</ComboboxOption>
<ComboboxOption>Bolivia</ComboboxOption>
<ComboboxOption>Brazil</ComboboxOption>
<ComboboxOption>Chile</ComboboxOption>
<ComboboxOption>Columbia</ComboboxOption>
<ComboboxOption>Ecuador</ComboboxOption>
<ComboboxOption>Falkland Islands</ComboboxOption>
<ComboboxOption>French Guiana</ComboboxOption>
<ComboboxOption>Guyana</ComboboxOption>
<ComboboxOption>Paraguay</ComboboxOption>
<ComboboxOption>Peru</ComboboxOption>
<ComboboxOption>Suriname</ComboboxOption>
<ComboboxOption>Uruguay</ComboboxOption>
<ComboboxOption>Venezuela</ComboboxOption>
</ComboboxGroup>
</Combobox>
</FieldWrap>
```
### Uncontrolled
To set the initial value for uncontrolled Comboboxes, use `defaultValue`.
```jsx example
<FieldWrap>
<Combobox label="Fruit" defaultValue="Peach">
<ComboboxOption value="Apple">🍎 Apple</ComboboxOption>
<ComboboxOption value="Banana">🍌 Banana</ComboboxOption>
<ComboboxOption value="Cucumber">🥒 Cucumber</ComboboxOption>
<ComboboxOption value="Orange">🍊 Orange</ComboboxOption>
<ComboboxOption value="Peach">🍑 Peach</ComboboxOption>
<ComboboxOption value="Pear">🍐 Pear</ComboboxOption>
</Combobox>
</FieldWrap>
```
### Controlled
Comboboxes can also be controlled, by setting the `value` prop and updating the value from event callbacks.
<Note>
`onChange` and `onSelectionChange` have different behaviors, and special consideration needs to be made when `Combobox` is controlled. `onChange` gets called when the value of the input field changes from user input or selections while `onSelectionChange` is only called when a selection changes from the listbox.
</Note>
```jsx example
function ComboboxControlledExample() {
const [value, setValue] = useState('Peach');
const handleChange = ({ target }) => {
setValue(target.value);
};
const handleSelectionChange = ({ value }) => {
setValue(value);
};
return (
<FieldWrap>
<Combobox
label="Fruit"
value={value}
onChange={handleChange}
onSelectionChange={handleSelectionChange}
>
<ComboboxOption value="Apple">🍎 Apple</ComboboxOption>
<ComboboxOption value="Banana">🍌 Banana</ComboboxOption>
<ComboboxOption value="Cucumber">🥒 Cucumber</ComboboxOption>
<ComboboxOption value="Orange">🍊 Orange</ComboboxOption>
<ComboboxOption value="Peach">🍑 Peach</ComboboxOption>
<ComboboxOption value="Pear">🍐 Pear</ComboboxOption>
</Combobox>
</FieldWrap>
);
}
```
## Autocomplete
There are a couple of different methods to control the auto completion of the options in the listbox. By default, autocomplete is set to "manual" meaning selections will be filtered based on the value of the input, and a value will need to be manually selected via click or keypress to change the value of the input field.
### None
When autocomplete is set to "none" the listbox will provide the full list of options and will not change based on the value of the input.
```jsx example
<FieldWrap>
<Combobox label="Auto Completion (None)" autocomplete="none">
<ComboboxOption>None</ComboboxOption>
<ComboboxOption>Manual</ComboboxOption>
<ComboboxOption>Automatic</ComboboxOption>
</Combobox>
</FieldWrap>
```
### Automatic
When autocomplete is set to "automatic" the listbox will provide a filtered list of options with the first value active by default. The active value will become the value of the input when the listbox closes.
```jsx example
<FieldWrap>
<Combobox label="Auto Completion (Automatic)" autocomplete="automatic">
<ComboboxOption>None</ComboboxOption>
<ComboboxOption>Manual</ComboboxOption>
<ComboboxOption>Automatic</ComboboxOption>
</Combobox>
</FieldWrap>
```
## Props
### Combobox
<ComponentProps className={true} refType="HTMLDivElement" props={[
{
name: 'label',
type: ['string', 'number', 'ReactElement', 'ReactFragment', 'ReactPortal'],
required: true,
description: 'Label for the Combobox input field.'
},
{
name: 'options',
type: 'object',
description: 'Array of objects mapping to Combobox options. Alternatively, "ComboboxOption" can be used as children instead of this prop.'
},
{
name: 'value',
type: 'string',
description: 'Initial value to be applied to the combobox. Optionally used for "uncontrolled" comboboxes.'
},
{
name: 'defaultValue',
type: 'string',
description: 'Value to be applied to the combobox. Optionally used for "controlled" comboboxes.'
},
{
name: 'autocomplete',
type: ['none', 'manual', 'automatic'],
defaultValue: 'manual',
description: 'Sets the autocompletion type of the Combobox listbox.'
},
{
name: 'onChange',
type: 'function',
description: 'Callback function that gets invoked when the combobox input value is changed.'
},
{
name: 'onSelectionChange',
type: 'function',
description: 'Callback function that gets invoked when the selected value is changed.'
},
{
name: 'onActiveChange',
type: 'function',
description: 'Callback function that gets invoked when the current active option was changed.'
},
{
name: 'error',
type: 'React.ReactNode',
description: 'Error message for the field, when needed.'
},
{
name: 'requiredText',
type: 'string',
defaultValue: 'Required',
description: 'Custom "required" text. Useful for localization.'
},
{
name: 'renderNoResults',
type: ['() => JSX.Element', 'React.Element'],
description: 'Render prop to customize the output when there are no matching results. Useful for localization.'
},
{
name: 'portal',
type: ['React.Ref<HTMLElement>', 'HTMLElement'],
description: 'Alternative placement of combobox listbox. When not set, the listbox will be absolutely positioned inline.'
}
]} />
### ComboboxOption
<ComponentProps className={true} refType="HTMLLIElement" props={[
{
name: 'value',
type: 'string',
description: 'Value to be applied to the combobox option. When omitted, value will default to the text content of the option.'
},
{
name: 'disabled',
type: 'boolean',
description: 'When set, sets the combobox option as "aria-disabled="true" and removes the element from key navigation.'
},
{
name: 'description',
type: ['string', 'number', 'ReactElement', 'ReactFragment', 'ReactPortal'],
description: 'Additional text to display for the ComboboxOption.'
}
]} />
### ComboboxGroup
<ComponentProps className={true} refType="HTMLUListElement" props={[
{
name: 'label',
required: true,
type: ['string', 'number', 'ReactElement', 'ReactFragment', 'ReactPortal'],
description: 'Label for the group of options.'
}
]} />
## Related Components
- [Listbox](./Listbox)
- [FieldWrap](./FieldWrap)
3 changes: 2 additions & 1 deletion docs/pages/components/FieldWrap.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,5 @@ import { FieldWrap } from '@deque/cauldron-react'
- [TextField](./TextField)
- [Select](./Select)
- [RadioGroup](./RadioGroup)
- [Checkbox](./Checkbox)
- [Checkbox](./Checkbox)
- [Combobox](./Combobox)
13 changes: 6 additions & 7 deletions docs/pages/components/Listbox.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
## Examples

<Note>
Listbox follows [aria practices](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/) for _Listbox_ but is not currently intended to be used by itself. This component's intended usage is to be composed of components that have keyboard-navigable items like Combobox and [OptionsMenu](./OptionsMenu).
Listbox follows [aria practices](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/) for _Listbox_ but is not currently intended to be used by itself. This component's intended usage is to be composed with components that have keyboard-navigable items like [Combobox](./Combobox) and [OptionsMenu](./OptionsMenu).
</Note>

### Listbox Options
Expand Down Expand Up @@ -143,11 +143,6 @@ Uncontrolled listboxes will automatically set `aria-selected="true"` for the sel
type: ['string', 'number'],
description: 'Initial value to be applied to the listbox. Optionally used for "uncontrolled" listboxes.'
},
{
name: 'onSelect',
type: 'function',
description: 'Callback function that gets invoked when a selection is made from one of the ListboxOptions.'
},
{
name: 'onSelectionChange',
type: 'function',
Expand Down Expand Up @@ -214,4 +209,8 @@ Uncontrolled listboxes will automatically set `aria-selected="true"` for the sel
description: 'A component to render the ListboxGroup as.',
defaultValue: 'ul'
}
]} />
]} />

## Related Components

- [Combobox](./Combobox)
Loading

0 comments on commit 4d29227

Please sign in to comment.