Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(react): add Combobox component #1180

Merged
merged 64 commits into from
Sep 6, 2023
Merged
Show file tree
Hide file tree
Changes from 60 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
2e87a50
feat(react): add Combobox component
scurker Aug 23, 2023
3dd2885
click stuff
scurker Aug 23, 2023
ff327e0
max-width
scurker Aug 23, 2023
b02c867
minheight
scurker Aug 23, 2023
39a1d89
pointer events
scurker Aug 23, 2023
b84d526
more things
scurker Aug 23, 2023
0cf06a2
small tweaks
scurker Aug 24, 2023
8cbe0e4
portals
scurker Aug 24, 2023
47d250e
Merge branch 'develop' into combobox
scurker Aug 24, 2023
c0e7e3c
wip filtering
scurker Aug 24, 2023
6e1e32c
a few docs things
scurker Aug 24, 2023
27fd5e2
updated styles
scurker Aug 25, 2023
4ac33ca
minor fixes
scurker Aug 25, 2023
41cbbda
tweak
scurker Aug 25, 2023
5faf5f1
fix type
scurker Aug 25, 2023
322d6bd
fix font colors
scurker Aug 25, 2023
5d73578
fix font weight
scurker Aug 25, 2023
ba35bfe
handle scroll behavior for keydown
scurker Aug 25, 2023
20261ec
fix specificity
scurker Aug 25, 2023
22e8d1d
tweaks
scurker Aug 25, 2023
4f37006
make lint happy
scurker Aug 25, 2023
ddc746a
wrap
scurker Aug 25, 2023
3576651
fix controlled comboboxes
scurker Aug 25, 2023
e61e328
stub tests
scurker Aug 25, 2023
9d861f6
it's broken but I have somewhere to be :shrug:
scurker Aug 25, 2023
aa7b467
comment
scurker Aug 25, 2023
c8fbacc
update intersection hook
scurker Aug 26, 2023
35db158
handle null
scurker Aug 28, 2023
4b5c03f
convert comboboxitems to comboboxoptions
scurker Aug 28, 2023
07c84a8
more items
scurker Aug 28, 2023
6085fd1
css var renames
scurker Aug 28, 2023
065b217
few more options things
scurker Aug 28, 2023
466dc68
more tests
scurker Aug 28, 2023
30ba6af
update description
scurker Aug 28, 2023
53a8147
more testing
scurker Aug 28, 2023
84a932d
Merge branch 'develop' into combobox
scurker Aug 28, 2023
4e8f16e
more test coverage
scurker Aug 28, 2023
e187688
more tests
scurker Aug 28, 2023
9348b0d
additional mdx docs
scurker Aug 28, 2023
ef2631b
border color on hover
scurker Aug 28, 2023
3f97f54
combobox match and things
scurker Aug 29, 2023
24bdf93
lint
scurker Aug 29, 2023
7c0b8f4
matching tests
scurker Aug 29, 2023
33d89bf
fix styles
scurker Aug 29, 2023
b3506f1
fix focus ring
scurker Aug 29, 2023
a7ed25e
fix text matching
scurker Aug 29, 2023
1cc50ad
fix context things
scurker Aug 29, 2023
849f71b
fix scroll behavior and display
scurker Aug 29, 2023
9826c94
no ?
scurker Aug 29, 2023
d58feb2
set value correctly from events
scurker Aug 30, 2023
8124d40
additional test cases
scurker Aug 30, 2023
d5356db
automatic
scurker Aug 30, 2023
4cd1d7a
fix long text from bleeding into trigger
scurker Aug 30, 2023
4f32181
small tweak
scurker Aug 30, 2023
1ef9e57
fix boundary click
scurker Aug 30, 2023
458b8b0
no layout effect
scurker Aug 30, 2023
93c00ed
fix autocomplete none/automatic behaviors
scurker Aug 31, 2023
344b758
fix autocomplete things
scurker Aug 31, 2023
0ebd6bf
home/end prevent default
scurker Aug 31, 2023
a16a9c3
cleanup dispatch event function
scurker Aug 31, 2023
5b30496
document `onChange`
scurker Sep 5, 2023
efcc713
words
scurker Sep 5, 2023
c3bec2a
,
scurker Sep 5, 2023
0229e13
clean up logic
scurker Sep 5, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
347 changes: 347 additions & 0 deletions docs/pages/components/Combobox.mdx
scurker marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,347 @@
---
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>
)
}
```
scurker marked this conversation as resolved.
Show resolved Hide resolved

### 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 need 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.
scurker marked this conversation as resolved.
Show resolved Hide resolved
</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: 'onSelectionChange',
type: 'function',
description: 'Callback function that gets invoked when the selected value is changed.'
},
scurker marked this conversation as resolved.
Show resolved Hide resolved
{
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)
2 changes: 1 addition & 1 deletion packages/react/__tests__/setupTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ if (
Object.defineProperty(window.HTMLElement.prototype, 'innerText', {
get() {
return this.textContent;
},
}
});
}
Loading