Skip to content

Commit

Permalink
Components: Add a WAI-ARIA compliant custom select. (#17926)
Browse files Browse the repository at this point in the history
* 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
epiqueras authored Nov 28, 2019
1 parent 8f739f2 commit 139f525
Show file tree
Hide file tree
Showing 14 changed files with 524 additions and 161 deletions.
28 changes: 26 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"classnames": "^2.2.5",
"clipboard": "^2.0.1",
"dom-scroll-into-view": "^1.2.1",
"downshift": "^3.3.4",
"lodash": "^4.17.15",
"memize": "^1.0.5",
"moment": "^2.22.1",
Expand Down
149 changes: 149 additions & 0 deletions packages/components/src/custom-select/index.js
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>
);
}
30 changes: 30 additions & 0 deletions packages/components/src/custom-select/stories/index.js
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 } />;
56 changes: 56 additions & 0 deletions packages/components/src/custom-select/style.scss
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;
}
}
Loading

0 comments on commit 139f525

Please sign in to comment.