-
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 custom select. (#17926)
* Components: Add a WAI-ARIA compliant custom select. * Custom Select: Fix Downshift constant references. * Custom Select: Add explicit aria-label to button and export component. * Custom Select: Add temporary console debugging. * Custom Select: Collapsed switching fix. * Custom Select: Firefox/IE styling fix. * Font Size Picker: Replace `SelectControl` with `CustomSelect`. * Custom Select: A11y review. * Font Size Picker: A11y review. * Font Size Picker: a11y iteration. * Font Size Picker: Update tests.
- Loading branch information
Showing
14 changed files
with
524 additions
and
161 deletions.
There are no files selected for viewing
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,149 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { useSelect } from 'downshift'; | ||
import classnames from 'classnames'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { Button, Dashicon } from '../'; | ||
|
||
const itemToString = ( item ) => item && item.name; | ||
// This is needed so that in Windows, where | ||
// the menu does not necessarily open on | ||
// key up/down, you can still switch between | ||
// options with the menu closed. | ||
const stateReducer = ( | ||
{ selectedItem }, | ||
{ type, changes, props: { items } } | ||
) => { | ||
// TODO: Remove this. | ||
// eslint-disable-next-line no-console | ||
console.debug( | ||
'Selected Item: ', | ||
selectedItem, | ||
'Type: ', | ||
type, | ||
'Changes: ', | ||
changes, | ||
'Items: ', | ||
items | ||
); | ||
switch ( type ) { | ||
case useSelect.stateChangeTypes.ToggleButtonKeyDownArrowDown: | ||
// If we already have a selected item, try to select the next one, | ||
// without circular navigation. Otherwise, select the first item. | ||
return { | ||
selectedItem: | ||
items[ | ||
selectedItem ? | ||
Math.min( items.indexOf( selectedItem ) + 1, items.length - 1 ) : | ||
0 | ||
], | ||
}; | ||
case useSelect.stateChangeTypes.ToggleButtonKeyDownArrowUp: | ||
// If we already have a selected item, try to select the previous one, | ||
// without circular navigation. Otherwise, select the last item. | ||
return { | ||
selectedItem: | ||
items[ | ||
selectedItem ? | ||
Math.max( items.indexOf( selectedItem ) - 1, 0 ) : | ||
items.length - 1 | ||
], | ||
}; | ||
default: | ||
return changes; | ||
} | ||
}; | ||
export default function CustomSelect( { | ||
className, | ||
hideLabelFromVision, | ||
label, | ||
items, | ||
onSelectedItemChange, | ||
selectedItem: _selectedItem, | ||
} ) { | ||
const { | ||
getLabelProps, | ||
getToggleButtonProps, | ||
getMenuProps, | ||
getItemProps, | ||
isOpen, | ||
highlightedIndex, | ||
selectedItem, | ||
} = useSelect( { | ||
initialSelectedItem: items[ 0 ], | ||
items, | ||
itemToString, | ||
onSelectedItemChange, | ||
selectedItem: _selectedItem, | ||
stateReducer, | ||
} ); | ||
const menuProps = getMenuProps( { | ||
className: 'components-custom-select__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-custom-select', className ) }> | ||
{ /* eslint-disable-next-line jsx-a11y/label-has-associated-control, jsx-a11y/label-has-for */ } | ||
<label | ||
{ ...getLabelProps( { | ||
className: classnames( 'components-custom-select__label', { | ||
'screen-reader-text': hideLabelFromVision, | ||
} ), | ||
} ) } | ||
> | ||
{ label } | ||
</label> | ||
<Button | ||
{ ...getToggleButtonProps( { | ||
// This is needed because some speech recognition software don't support `aria-labelledby`. | ||
'aria-label': label, | ||
'aria-labelledby': undefined, | ||
className: 'components-custom-select__button', | ||
} ) } | ||
> | ||
{ itemToString( selectedItem ) } | ||
<Dashicon | ||
icon="arrow-down-alt2" | ||
className="components-custom-select__button-icon" | ||
/> | ||
</Button> | ||
<ul { ...menuProps }> | ||
{ isOpen && | ||
items.map( ( item, index ) => ( | ||
// eslint-disable-next-line react/jsx-key | ||
<li | ||
{ ...getItemProps( { | ||
item, | ||
index, | ||
key: item.key, | ||
className: classnames( 'components-custom-select__item', { | ||
'is-highlighted': index === highlightedIndex, | ||
} ), | ||
style: item.style, | ||
} ) } | ||
> | ||
{ item === selectedItem && ( | ||
<Dashicon | ||
icon="saved" | ||
className="components-custom-select__item-icon" | ||
/> | ||
) } | ||
{ item.name } | ||
</li> | ||
) ) } | ||
</ul> | ||
</div> | ||
); | ||
} |
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,30 @@ | ||
/** | ||
* Internal dependencies | ||
*/ | ||
import CustomSelect from '../'; | ||
|
||
export default { title: 'CustomSelect', component: CustomSelect }; | ||
|
||
const items = [ | ||
{ | ||
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%' }, | ||
}, | ||
]; | ||
export const _default = () => <CustomSelect label="Font Size" items={ items } />; |
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,56 @@ | ||
.components-custom-select { | ||
color: $dark-gray-500; | ||
position: relative; | ||
} | ||
|
||
.components-custom-select__label { | ||
display: block; | ||
margin-bottom: 5px; | ||
} | ||
|
||
.components-custom-select__button { | ||
border: 1px solid $dark-gray-200; | ||
border-radius: 4px; | ||
color: $dark-gray-500; | ||
display: inline; | ||
min-height: 30px; | ||
min-width: 130px; | ||
position: relative; | ||
text-align: left; | ||
|
||
&:focus { | ||
border-color: $blue-medium-500; | ||
} | ||
|
||
&-icon { | ||
height: 100%; | ||
padding: 0 4px; | ||
position: absolute; | ||
right: 0; | ||
top: 0; | ||
} | ||
} | ||
|
||
.components-custom-select__menu { | ||
background: $white; | ||
padding: 0; | ||
position: absolute; | ||
width: 100%; | ||
z-index: z-index(".components-popover"); | ||
} | ||
|
||
.components-custom-select__item { | ||
align-items: center; | ||
display: flex; | ||
list-style-type: none; | ||
padding: 10px 5px 10px 25px; | ||
|
||
&.is-highlighted { | ||
background: $light-gray-500; | ||
} | ||
|
||
&-icon { | ||
margin-left: -20px; | ||
margin-right: 0; | ||
} | ||
} |
Oops, something went wrong.