-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Components: Add a WAI-ARIA compliant Combobox. (#19657)
* Components: Implement a combobox control. * Components: Update Downshift. * Components: Update Downshift.
- Loading branch information
Showing
9 changed files
with
442 additions
and
32 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
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
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,146 @@ | ||
# ComboboxControl | ||
|
||
`ComboboxControl` is an enhanced version of a [`CustomSelectControl`](/packages/components/src/custom-select-control/readme.md), with the addition of being able to search for options using a search input. | ||
|
||
## Table of contents | ||
|
||
1. [Design guidelines](#design-guidelines) | ||
2. [Development guidelines](#development-guidelines) | ||
3. [Related components](#related-components) | ||
|
||
## Design guidelines | ||
|
||
These are the same as [the ones for `CustomSelectControl`s](/packages/components/src/select-control/readme.md#design-guidelines), but this component is better suited for when there are too many items to scroll through or load at once so you need to filter them based on user input. | ||
|
||
## Development guidelines | ||
|
||
### Usage | ||
|
||
```jsx | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { ComboboxControl } from "@wordpress/components"; | ||
import { useState } from "@wordpress/compose"; | ||
|
||
const options = [ | ||
{ | ||
key: "small", | ||
name: "Small", | ||
style: { fontSize: "50%" } | ||
}, | ||
{ | ||
key: "normal", | ||
name: "Normal", | ||
style: { fontSize: "100%" } | ||
}, | ||
{ | ||
key: "large", | ||
name: "Large", | ||
style: { fontSize: "200%" } | ||
}, | ||
{ | ||
key: "huge", | ||
name: "Huge", | ||
style: { fontSize: "300%" } | ||
} | ||
]; | ||
|
||
function MyComboboxControl() { | ||
const [, setFontSize] = useState(); | ||
const [filteredOptions, setFilteredOptions] = useState(options); | ||
return ( | ||
<ComboboxControl | ||
label="Font Size" | ||
options={filteredOptions} | ||
onInputValueChange={({ inputValue }) => | ||
setFilteredOptions( | ||
options.filter(option => | ||
option.name.toLowerCase().startsWith(inputValue.toLowerCase()) | ||
) | ||
) | ||
} | ||
onChange={({ selectedItem }) => setFontSize(selectedItem)} | ||
/> | ||
); | ||
} | ||
|
||
function MyControlledComboboxControl() { | ||
const [fontSize, setFontSize] = useState(options[0]); | ||
const [filteredOptions, setFilteredOptions] = useState(options); | ||
return ( | ||
<ComboboxControl | ||
label="Font Size" | ||
options={filteredOptions} | ||
onInputValueChange={({ inputValue }) => | ||
setFilteredOptions( | ||
options.filter(option => | ||
option.name.toLowerCase().startsWith(inputValue.toLowerCase()) | ||
) | ||
) | ||
} | ||
onChange={({ selectedItem }) => setFontSize(selectedItem)} | ||
value={options.find(option => option.key === fontSize.key)} | ||
/> | ||
); | ||
} | ||
``` | ||
|
||
### Props | ||
|
||
#### className | ||
|
||
A custom class name to append to the outer `<div>`. | ||
|
||
- Type: `String` | ||
- Required: No | ||
|
||
#### hideLabelFromVision | ||
|
||
Used to visually hide the label. It will always be visible to screen readers. | ||
|
||
- Type: `Boolean` | ||
- Required: No | ||
|
||
#### label | ||
|
||
The label for the control. | ||
|
||
- Type: `String` | ||
- Required: Yes | ||
|
||
#### options | ||
|
||
The options that can be chosen from. | ||
|
||
- Type: `Array<{ key: String, name: String, style: ?{}, ...rest }>` | ||
- Required: Yes | ||
|
||
#### onInputValueChange | ||
|
||
Function called with the control's search input value changes. The `inputValue` property contains the next input value. | ||
|
||
- Type: `Function` | ||
- Required: No | ||
|
||
#### onChange | ||
|
||
Function called with the control's internal state changes. The `selectedItem` property contains the next selected item. | ||
|
||
- Type: `Function` | ||
- Required: No | ||
|
||
#### value | ||
|
||
Can be used to externally control the value of the control, like in the `MyControlledComboboxControl` example above. | ||
|
||
- Type: `Object` | ||
- Required: No | ||
|
||
## Related components | ||
|
||
- Like this component, but without a search input, the `CustomSelectControl` component. | ||
|
||
- To select one option from a set, when you want to show all the available options at once, use the `Radio` component. | ||
- To select one or more items from a set, use the `CheckboxControl` component. | ||
- To toggle a single setting on or off, use the `ToggleControl` component. |
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,126 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { useCombobox } from 'downshift'; | ||
import classnames from 'classnames'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { Button, Dashicon } from '../'; | ||
|
||
const itemToString = ( item ) => item && item.name; | ||
export default function ComboboxControl( { | ||
className, | ||
hideLabelFromVision, | ||
label, | ||
options: items, | ||
onInputValueChange: onInputValueChange, | ||
onChange: onSelectedItemChange, | ||
value: _selectedItem, | ||
} ) { | ||
const { | ||
getLabelProps, | ||
getToggleButtonProps, | ||
getComboboxProps, | ||
getInputProps, | ||
getMenuProps, | ||
getItemProps, | ||
isOpen, | ||
highlightedIndex, | ||
selectedItem, | ||
} = useCombobox( { | ||
initialSelectedItem: items[ 0 ], | ||
items, | ||
itemToString, | ||
onInputValueChange, | ||
onSelectedItemChange, | ||
selectedItem: _selectedItem, | ||
} ); | ||
const menuProps = getMenuProps( { | ||
className: 'components-combobox-control__menu', | ||
} ); | ||
// We need this here, because the null active descendant is not | ||
// fully ARIA compliant. | ||
if ( | ||
menuProps[ 'aria-activedescendant' ] && | ||
menuProps[ 'aria-activedescendant' ].slice( | ||
0, | ||
'downshift-null'.length | ||
) === 'downshift-null' | ||
) { | ||
delete menuProps[ 'aria-activedescendant' ]; | ||
} | ||
return ( | ||
<div | ||
className={ classnames( 'components-combobox-control', className ) } | ||
> | ||
{ /* eslint-disable-next-line jsx-a11y/label-has-associated-control, jsx-a11y/label-has-for */ } | ||
<label | ||
{ ...getLabelProps( { | ||
className: classnames( | ||
'components-combobox-control__label', | ||
{ | ||
'screen-reader-text': hideLabelFromVision, | ||
} | ||
), | ||
} ) } | ||
> | ||
{ label } | ||
</label> | ||
<div | ||
{ ...getComboboxProps( { | ||
className: 'components-combobox-control__button', | ||
} ) } | ||
> | ||
<input | ||
{ ...getInputProps( { | ||
className: 'components-combobox-control__button-input', | ||
} ) } | ||
/> | ||
<Button | ||
{ ...getToggleButtonProps( { | ||
// This is needed because some speech recognition software don't support `aria-labelledby`. | ||
'aria-label': label, | ||
'aria-labelledby': undefined, | ||
className: 'components-combobox-control__button-button', | ||
} ) } | ||
> | ||
<Dashicon | ||
icon="arrow-down-alt2" | ||
className="components-combobox-control__button-icon" | ||
/> | ||
</Button> | ||
</div> | ||
<ul { ...menuProps }> | ||
{ isOpen && | ||
items.map( ( item, index ) => ( | ||
// eslint-disable-next-line react/jsx-key | ||
<li | ||
{ ...getItemProps( { | ||
item, | ||
index, | ||
key: item.key, | ||
className: classnames( | ||
'components-combobox-control__item', | ||
{ | ||
'is-highlighted': | ||
index === highlightedIndex, | ||
} | ||
), | ||
style: item.style, | ||
} ) } | ||
> | ||
{ item === selectedItem && ( | ||
<Dashicon | ||
icon="saved" | ||
className="components-combobox-control__item-icon" | ||
/> | ||
) } | ||
{ item.name } | ||
</li> | ||
) ) } | ||
</ul> | ||
</div> | ||
); | ||
} |
Oops, something went wrong.