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 Listbox component #1167

Merged
merged 45 commits into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
3368530
feat(Listbox): add Listbox component
scurker Aug 17, 2023
954ea0a
small fixes
scurker Aug 17, 2023
7fb53b7
Merge remote-tracking branch 'origin' into listbox
scurker Aug 17, 2023
0a12828
update typescript
scurker Aug 17, 2023
9a940af
update typescript in packages
scurker Aug 17, 2023
2b1aea3
rogue types
scurker Aug 17, 2023
3e41b88
rollup
scurker Aug 17, 2023
88248d8
fix other types
scurker Aug 17, 2023
11cb629
tweaks
scurker Aug 17, 2023
6daca4c
more type things
scurker Aug 17, 2023
73d7065
listbox tests
scurker Aug 17, 2023
2a251e9
babel things
scurker Aug 18, 2023
4c596ab
clean cache
scurker Aug 18, 2023
c4bff1d
unmuck files
scurker Aug 18, 2023
c5d21c6
more unmucking
scurker Aug 18, 2023
fc3507f
passing tests
scurker Aug 18, 2023
1d7ee51
more tests
scurker Aug 18, 2023
5bae546
check for property descriptor
scurker Aug 18, 2023
9783423
Merge remote-tracking branch 'origin' into listbox
scurker Aug 18, 2023
9fc30c9
cleanup prettier
scurker Aug 18, 2023
6ad21ba
more accordion
scurker Aug 18, 2023
926d5a4
docs things
scurker Aug 18, 2023
14ea82a
key testing
scurker Aug 18, 2023
df7bf04
more testing
scurker Aug 18, 2023
3ae35e3
more testing
scurker Aug 21, 2023
9a73058
fix selections
scurker Aug 21, 2023
7f72c4d
Update docs/pages/components/Listbox.mdx
scurker Aug 21, 2023
4eea77c
Update docs/pages/components/Listbox.mdx
scurker Aug 21, 2023
8daba62
Update docs/pages/components/Listbox.mdx
scurker Aug 21, 2023
01c1d1e
test aria-selected
scurker Aug 21, 2023
57d5dfe
Merge branch 'listbox' of github.com:dequelabs/cauldron into listbox
scurker Aug 21, 2023
b9aa465
fix focus on selected item
scurker Aug 21, 2023
3f97729
Merge branch 'develop' into listbox
scurker Aug 22, 2023
fbd29c0
handle active change
scurker Aug 22, 2023
023407b
Merge branch 'develop' into listbox
scurker Aug 23, 2023
d10680d
Merge branch 'develop' into listbox
scurker Aug 23, 2023
f5c9c3d
fix incorrect description
scurker Aug 23, 2023
c63c812
missing '
scurker Aug 23, 2023
28a5e5b
check for null
scurker Aug 23, 2023
dd5dfa9
add missing prop doc
scurker Aug 23, 2023
d90340e
words
scurker Aug 23, 2023
6d3281f
.
scurker Aug 23, 2023
cc096a7
changes
scurker Aug 23, 2023
c0d8529
uncontrolled uselayouteffect
scurker Aug 23, 2023
3ce77e2
remove onSelect for single onSelectionChange function
scurker Aug 24, 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
217 changes: 217 additions & 0 deletions docs/pages/components/Listbox.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
---
title: Listbox
description: An unstyled component to provide a keyboard-navigable list of options.
source: https://github.com/dequelabs/cauldron/tree/develop/packages/react/src/components/Listbox/Listbox.tsx
---

import { useState } from 'react';
import { Listbox, ListboxOption, ListboxGroup } from '@deque/cauldron-react';

```js
import {
Listbox,
ListboxOption,
ListboxGroup
} from '@deque/cauldron-react';
```

## 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).
</Note>

### Listbox Options

A listbox can contain a list of options with optional values.

```jsx example
<>
<div id="listbox-options-example">Numbers</div>
<Listbox aria-labelledby="listbox-options-example">
<ListboxOption>One</ListboxOption>
<ListboxOption value="dos">Two</ListboxOption>
<ListboxOption>Three</ListboxOption>
</Listbox>
</>
```

### Disabled Options

A listbox option can be optionally disabled.

```jsx example
<>
<div id="listbox-disabled-options-example">Numbers, but two is disabled</div>
<Listbox aria-labelledby="listbox-disabled-options-example">
<ListboxOption>One</ListboxOption>
<ListboxOption disabled>Two</ListboxOption>
<ListboxOption>Three</ListboxOption>
</Listbox>
</>
```

### Grouped Options

Listbox options can also be grouped into categories. As a best practice, when using grouped options the `as` property should be set to a generic container for the wrapping `Listbox` component for proper HTML semantics.

```jsx example
<>
<div id="listbox-grouped-example">Colors and Numbers</div>
<Listbox as="div" aria-labelledby="listbox-grouped-example">
<ListboxGroup label="Colors">
<ListboxOption>Red</ListboxOption>
<ListboxOption>Green</ListboxOption>
<ListboxOption>Blue</ListboxOption>
</ListboxGroup>
<ListboxGroup label="Numbers">
<ListboxOption>One</ListboxOption>
<ListboxOption>Two</ListboxOption>
<ListboxOption>Three</ListboxOption>
</ListboxGroup>
</Listbox>
</>
```

### Keyboard Navigation

By default, keyboard navigation will stop at the first or last option of the list. To wrap around focus to the beginning or the end of the list, `navigation="cycle"` can be set to enable this behavior.

```jsx example
<>
<div id="listbox-cycle-example">Numbers that Cycle</div>
<Listbox navigation="cycle" aria-labelledby="listbox-cycle-example">
<ListboxOption>One</ListboxOption>
<ListboxOption>Two</ListboxOption>
<ListboxOption>Three</ListboxOption>
</Listbox>
</>
```

### Controlled

Controlled listboxes require updating the value to reflect the newly selected state. Useful if some form of validation needs to be performed before setting the selected value.

```jsx example
function ControlledListboxExample() {
const [value, setValue] = useState('Two');
const handleSelect = ({ value }) => setValue(value);
return (
<>
<div id="listbox-controlled-example">Controlled Listbox</div>
<Listbox
aria-labelledby="listbox-controlled-example"
value={value}
onSelect={handleSelect}
>
<ListboxOption>One</ListboxOption>
<ListboxOption>Two</ListboxOption>
<ListboxOption>Three</ListboxOption>
</Listbox>
</>
);
}
```

### Uncontrolled

Uncontrolled listboxes will automatically set `aria-selected="true"` for the selected option upon selection. The initial value can be set via `defaultValue`.

```jsx example
<>
<div id="listbox-uncontrolled-example">Uncontrolled Listbox</div>
<Listbox aria-labelledby="listbox-uncontrolled-example" defaultValue="Two">
<ListboxOption>One</ListboxOption>
<ListboxOption>Two</ListboxOption>
<ListboxOption>Three</ListboxOption>
</Listbox>
</>
```

## Props

### Listbox

<ComponentProps className={true} refType="HTMLUListElement" props={[
{
name: 'value',
type: ['string', 'number'],
description: 'Value to be applied to the listbox. Optionally used for "controlled" listboxes.'
},
{
name: 'defaultValue',
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',
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: 'navigation',
type: ['bound', 'cycle'],
defaultValue: 'bound',
description: 'How keyboard navigation is handled when reaching the start/end of the list.'
},
{
name: 'as',
type: ['React.ElementType', 'string'],
description: 'A component to render the Listbox as.',
defaultValue: 'ul'
}
]} />

### ListboxOption

<ComponentProps className={true} refType="HTMLLIElement" props={[
{
name: 'value',
type: ['string', 'number'],
description: 'Value to be applied to the listbox option. When omitted, value will default to the text content of the option.'
},
{
name: 'disabled',
type: 'boolean',
description: 'When set, sets the listbox option as "aria-disabled="true" and removes the element from key navigation.'
},
{
name: 'activeClass',
type: 'string',
defaultValue: 'ListboxOption--active',
description: 'When the listbox option becomes active, this class will be applied to the element.'
},
{
name: 'as',
type: ['React.ElementType', 'string'],
description: 'A component to render the ListboxOption as.',
defaultValue: 'li'
}
]} />

### ListboxGroup

<ComponentProps className={true} refType="HTMLUListElement" props={[
{
name: 'label',
required: true,
type: ['string', 'number', 'ReactElement', 'ReactFragment', 'ReactPortal'],
description: 'Label for the group of items.'
},
{
name: 'as',
type: ['React.ElementType', 'string'],
description: 'A component to render the ListboxGroup as.',
defaultValue: 'ul'
}
]} />
12 changes: 12 additions & 0 deletions packages/react/__tests__/setupTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,15 @@ import Adapter from 'enzyme-adapter-react-16';
import 'jest-axe/extend-expect';

configure({ adapter: new Adapter() });

if (
!Object.getOwnPropertyDescriptor(window.HTMLElement.prototype, 'innerText')
) {
// JSDOM doesn't fully support innerText, but we can fall back to
// using textContent for now until this gets patched
Object.defineProperty(window.HTMLElement.prototype, 'innerText', {
get() {
return this.textContent;
},
});
}
Loading
Loading