-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
15 changed files
with
972 additions
and
9 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
--- | ||
title: Combobox | ||
description: An input component to display a list of suggested options matching the current text input. | ||
source: https://github.com/dequelabs/cauldron/tree/develop/packages/react/src/components/Combobox/Combobox.tsx | ||
--- | ||
|
||
import { Combobox, ComboboxItem, ComboboxGroup } from '@deque/cauldron-react' | ||
|
||
```js | ||
import { | ||
Combobox, | ||
ComboboxItem, | ||
ComboboxGroup | ||
} from '@deque/cauldron-react' | ||
``` | ||
|
||
## Examples | ||
|
||
## With Items | ||
|
||
```jsx example | ||
function ComboboxWithItemPropExample() { | ||
const items = [ | ||
{ label: 'Red', value: 'Red' }, | ||
{ label: 'Orange', value: 'Orange' }, | ||
{ label: 'Yellow', value: 'Yellow' }, | ||
{ label: 'Green', value: 'Green' }, | ||
{ label: 'Blue', value: 'Blue' } | ||
] | ||
return ( | ||
<Combobox label="Favorite Color" items={items} /> | ||
) | ||
} | ||
``` | ||
|
||
## With Children | ||
|
||
```jsx example | ||
<Combobox label="Fruit" defaultValue="Peach"> | ||
<ComboboxItem value="Apple">🍎 Apple</ComboboxItem> | ||
<ComboboxItem value="Banana">🍌 Banana</ComboboxItem> | ||
<ComboboxItem value="Cucumber">🥒 Cucumber</ComboboxItem> | ||
<ComboboxItem value="Orange">🍊 Orange</ComboboxItem> | ||
<ComboboxItem value="Peach">🍑 Peach</ComboboxItem> | ||
<ComboboxItem value="Pear">🍐 Pear</ComboboxItem> | ||
</Combobox> | ||
``` | ||
|
||
## Grouping | ||
|
||
```jsx example | ||
<Combobox label="Countries"> | ||
<ComboboxGroup label="North America"> | ||
<ComboboxItem>Canada</ComboboxItem> | ||
<ComboboxItem>Mexico</ComboboxItem> | ||
<ComboboxItem>United States</ComboboxItem> | ||
</ComboboxGroup> | ||
<ComboboxGroup label="South America"> | ||
<ComboboxItem>Argentina</ComboboxItem> | ||
<ComboboxItem>Bolivia</ComboboxItem> | ||
<ComboboxItem>Brazil</ComboboxItem> | ||
<ComboboxItem>Chile</ComboboxItem> | ||
<ComboboxItem>Columbia</ComboboxItem> | ||
<ComboboxItem>Ecuador</ComboboxItem> | ||
<ComboboxItem>Falkland Islands</ComboboxItem> | ||
<ComboboxItem>French Guiana</ComboboxItem> | ||
<ComboboxItem>Guyana</ComboboxItem> | ||
<ComboboxItem>Paraguay</ComboboxItem> | ||
<ComboboxItem>Peru</ComboboxItem> | ||
<ComboboxItem>Suriname</ComboboxItem> | ||
<ComboboxItem>Uruguay</ComboboxItem> | ||
<ComboboxItem>Venezuela</ComboboxItem> | ||
</ComboboxGroup> | ||
</Combobox> | ||
``` |
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,170 @@ | ||
--- | ||
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 { 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 with components that have keyboard navigable items like [Combobox](./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. | ||
|
||
```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 | ||
|
||
```jsx example | ||
<> | ||
<div id="listbox-controlled-example">Controlled Listbox</div> | ||
<Listbox navigation="cycle" aria-labelledby="listbox-controlled-example" value="Two"> | ||
<ListboxOption>One</ListboxOption> | ||
<ListboxOption>Two</ListboxOption> | ||
<ListboxOption>Three</ListboxOption> | ||
</Listbox> | ||
</> | ||
``` | ||
|
||
### Uncontrolled | ||
|
||
```jsx example | ||
<> | ||
<div id="listbox-uncontrolled-example">Uncontrolled Listbox</div> | ||
<Listbox navigation="cycle" aria-labelledby="listbox-uncontrolled-example" value="Two"> | ||
<ListboxOption>One</ListboxOption> | ||
<ListboxOption>Two</ListboxOption> | ||
<ListboxOption>Three</ListboxOption> | ||
</Listbox> | ||
</> | ||
``` | ||
|
||
## Props | ||
|
||
### Listbox | ||
|
||
<ComponentProps className={true} refType="HTMLUListElement" props={[ | ||
{ | ||
name: 'navigation', | ||
type: ['bound', 'cycle'], | ||
defaultValue: 'bound', | ||
description: 'How keyboard navigation is handled when reaching the start/end of the list.' | ||
}, | ||
{ | ||
name: 'value', | ||
type: ['string', 'number'], | ||
description: 'Initial value to be applied to the listbox. Optionally used for "controlled" listboxes.' | ||
}, | ||
{ | ||
name: 'defaultValue', | ||
type: ['string', 'number'], | ||
description: 'Value to be applied to the listbox. Optionally used for "controlled" listboxes.' | ||
}, | ||
{ | ||
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' | ||
} | ||
]} /> |
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,132 @@ | ||
import React, { | ||
forwardRef, | ||
useState, | ||
useEffect, | ||
useRef, | ||
useCallback | ||
} from 'react'; | ||
import classnames from 'classnames'; | ||
import { useId } from 'react-id-generator'; | ||
import { ContentNode } from '../../types'; | ||
import Listbox from '../Listbox'; | ||
import ComboboxItem from './ComboboxItem'; | ||
|
||
interface ComboboxItem<Label = string, Value = string> { | ||
key?: string; | ||
label: Label; | ||
value?: Value; | ||
} | ||
|
||
type Props = { | ||
label: ContentNode; | ||
items?: ComboboxItem[]; | ||
value?: ComboboxItem | string | number; | ||
defaultValue?: ComboboxItem | string | number; | ||
} & React.HTMLAttributes<Omit<HTMLInputElement, 'value' | 'defaultValue'>>; | ||
|
||
function isComboboxItem(value: unknown): value is ComboboxItem { | ||
return value !== null && typeof value === 'object' && 'label' in value; | ||
} | ||
|
||
const Combobox = forwardRef<HTMLInputElement, Props>( | ||
( | ||
{ | ||
id: propId, | ||
className, | ||
label, | ||
children, | ||
items = [], | ||
value: propValue, | ||
defaultValue, | ||
...props | ||
}, | ||
ref | ||
): JSX.Element => { | ||
const [value, setValue] = useState<Props['value']>( | ||
defaultValue || propValue | ||
); | ||
const [open, setOpen] = useState(false); | ||
const [id] = propId ? [propId] : useId(1, 'combobox'); | ||
const inputRef = useRef<HTMLInputElement>(null); | ||
const isControlled = typeof value !== 'undefined'; | ||
|
||
useEffect(() => { | ||
setValue(propValue); | ||
}, [propValue]); | ||
|
||
useEffect(() => { | ||
const focusHandler = () => { | ||
setOpen(true); | ||
}; | ||
const blurHandler = () => setOpen(false); | ||
|
||
inputRef.current?.addEventListener('focus', focusHandler); | ||
inputRef.current?.addEventListener('blur', blurHandler); | ||
|
||
return () => { | ||
inputRef.current?.removeEventListener('focus', focusHandler); | ||
inputRef.current?.removeEventListener('blur', blurHandler); | ||
}; | ||
}, []); | ||
|
||
const comboboxItems = | ||
children || | ||
items.map((item, index) => ( | ||
<ComboboxItem key={item.key || index} id={`${id}-option-${index + 1}`}> | ||
{item.label} | ||
</ComboboxItem> | ||
)); | ||
|
||
const inputValue = isComboboxItem(value) ? value.label : value; | ||
|
||
const handleSelect = useCallback( | ||
({ value: listboxValue }: { value: string | number }) => { | ||
if (!isControlled) { | ||
setValue(listboxValue); | ||
} | ||
}, | ||
[isControlled] | ||
); | ||
|
||
return ( | ||
<div className={classnames('Combobox', className)} ref={ref}> | ||
<label | ||
className="Field__label" | ||
id={`${id}-label`} | ||
htmlFor={`${id}-input`} | ||
> | ||
{label} | ||
</label> | ||
<div className="Combobox__input"> | ||
<input | ||
type="text" | ||
id={`${id}-input`} | ||
ref={inputRef} | ||
value={inputValue} | ||
{...props} | ||
role="combobox" | ||
aria-autocomplete="list" | ||
aria-controls={`${id}-listbox`} | ||
aria-expanded={open} | ||
aria-haspopup="listbox" | ||
/> | ||
<span className="Combobox__arrow" /> | ||
</div> | ||
<Listbox | ||
className="Combobox__listbox" | ||
role="listbox" | ||
aria-labelledby={`${id}-label`} | ||
id={`${id}-listbox`} | ||
value={inputValue} | ||
onSelect={handleSelect} | ||
> | ||
{comboboxItems} | ||
</Listbox> | ||
</div> | ||
); | ||
} | ||
); | ||
|
||
Combobox.displayName = 'Combobox'; | ||
|
||
export default Combobox; |
Oops, something went wrong.