-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Components: Add a WAI-ARIA compliant custom select. #17926
Merged
Merged
Changes from 10 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
9603bb0
Components: Add a WAI-ARIA compliant custom select.
epiqueras fa15dda
Custom Select: Fix Downshift constant references.
epiqueras d08dcb3
Custom Select: Add explicit aria-label to button and export component.
epiqueras 629ef24
Custom Select: Add temporary console debugging.
epiqueras f3a5eb5
Custom Select: Collapsed switching fix.
epiqueras c4025a4
Custom Select: Firefox/IE styling fix.
epiqueras bdad6a4
Font Size Picker: Replace `SelectControl` with `CustomSelect`.
epiqueras a7206c3
Custom Select: A11y review.
epiqueras 10c7ade
Font Size Picker: A11y review.
epiqueras 3f0e9d7
Font Size Picker: a11y iteration.
epiqueras ea4fac8
Font Size Picker: Update tests.
epiqueras File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.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 = ( | ||
epiqueras marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ 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, | ||
epiqueras marked this conversation as resolved.
Show resolved
Hide resolved
|
||
'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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just checked this on Windows and the behaviour is identical to Mac: on arrow down/up the menu opens. I don't see that as a major issue though. If we did want the arrow keys to work with the menu closed we'd probably have to make sure the options always render, and hide them with CSS, but then it's not clear that we could limit that behaviour to Windows 🤷♀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the issue here is the returned
changes
. @epiqueras just return theselectedItem
because if you also return the rest of thechanges
then one of them isisOpen
that comes as true :DThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed 😄