Skip to content

Commit

Permalink
fix(Dropdown): improve accessibility for the dropdown component
Browse files Browse the repository at this point in the history
  • Loading branch information
karinasigartau0798 authored and cipak committed Feb 24, 2022
1 parent 5eb14eb commit 71c56e9
Show file tree
Hide file tree
Showing 10 changed files with 85 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ Array [
>
<label
className="wxc wxc-label wxc wxc-dropdown"
id="wxc-0"
>
<div
className="wxc-label__control"
Expand All @@ -267,11 +268,13 @@ Array [
onKeyDown={[Function]}
>
<div
aria-label="undefined"
aria-controls="wxc-0"
aria-haspopup="listbox"
aria-labelledby="wxc-0"
className="wxc-dropdown__selected-option "
onClick={[Function]}
onKeyDown={[Function]}
role="button"
role="combobox"
tabIndex={0}
>
<span
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ Array [
>
<label
className="wxc wxc-label wxc wxc-dropdown wxc-meeting-control__control-select wxc-dropdown--disabled"
id="wxc-0"
>
<div
className="wxc-label__control"
Expand All @@ -198,11 +199,14 @@ Array [
onKeyDown={[Function]}
>
<div
aria-label="No available cameras. Use arrow keys to navigate between camera options and hit \\"Enter\\" to select."
aria-controls="wxc-0"
aria-haspopup="listbox"
aria-label="Use arrow keys to navigate between camera options and hit \\"Enter\\" to select."
aria-labelledby="wxc-0"
className="wxc-dropdown__selected-option "
onClick={[Function]}
onKeyDown={[Function]}
role="button"
role="combobox"
tabIndex={-1}
title="Video Devices"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ Array [
>
<label
className="wxc wxc-label wxc wxc-dropdown wxc-meeting-control__control-select"
id="wxc-0"
>
<div
className="wxc-label__control"
Expand All @@ -75,11 +76,14 @@ Array [
onKeyDown={[Function]}
>
<div
aria-label="Browser Default. Use arrow keys to navigate between speaker options and hit \\"Enter\\" to select."
aria-controls="wxc-0"
aria-haspopup="listbox"
aria-label="Use arrow keys to navigate between speaker options and hit \\"Enter\\" to select."
aria-labelledby="wxc-0"
className="wxc-dropdown__selected-option "
onClick={[Function]}
onKeyDown={[Function]}
role="button"
role="combobox"
tabIndex={102}
title="The current browser does not support changing speakers"
>
Expand Down Expand Up @@ -116,6 +120,7 @@ Array [
>
<label
className="wxc wxc-label wxc wxc-dropdown wxc-meeting-control__control-select wxc-dropdown--disabled"
id="wxc-0"
>
<div
className="wxc-label__control"
Expand All @@ -126,11 +131,14 @@ Array [
onKeyDown={[Function]}
>
<div
aria-label="No available microphones. Use arrow keys to navigate between microphone options and hit \\"Enter\\" to select."
aria-controls="wxc-0"
aria-haspopup="listbox"
aria-label="Use arrow keys to navigate between microphone options and hit \\"Enter\\" to select."
aria-labelledby="wxc-0"
className="wxc-dropdown__selected-option "
onClick={[Function]}
onKeyDown={[Function]}
role="button"
role="combobox"
tabIndex={-1}
title="Microphone Devices"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Array [
>
<label
className="wxc wxc-label wxc wxc-dropdown"
id="wxc-0"
>
<div
className="wxc-label__control"
Expand All @@ -38,11 +39,13 @@ Array [
onKeyDown={[Function]}
>
<div
aria-label="undefined"
aria-controls="wxc-0"
aria-haspopup="listbox"
aria-labelledby="wxc-0"
className="wxc-dropdown__selected-option "
onClick={[Function]}
onKeyDown={[Function]}
role="button"
role="combobox"
tabIndex={0}
>
<span
Expand Down Expand Up @@ -95,6 +98,7 @@ Array [
>
<label
className="wxc wxc-label wxc wxc-dropdown"
id="wxc-0"
>
<div
className="wxc-label__control"
Expand All @@ -105,11 +109,13 @@ Array [
onKeyDown={[Function]}
>
<div
aria-label="Option 1. undefined"
aria-controls="wxc-0"
aria-haspopup="listbox"
aria-labelledby="wxc-0"
className="wxc-dropdown__selected-option "
onClick={[Function]}
onKeyDown={[Function]}
role="button"
role="combobox"
tabIndex={0}
>
<span
Expand Down
1 change: 0 additions & 1 deletion src/components/generic/OptionsList/Option.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ export default function Option({

return (
<li
aria-label={typeof option.label !== 'object' ? option.label : option.value}
aria-selected={selected === option.value}
className={cssClasses}
key={option.value}
Expand Down
17 changes: 16 additions & 1 deletion src/components/generic/OptionsList/OptionsList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import Option from './Option';
*
* @param {object} props Data passed to the component
* @param {string} props.className Custom CSS class to apply
* @param {string} [props.id] Options list id
* @param {string} [props.labelId] Label id
* @param {Function} props.onBlur Called when this component loses focus
* @param {Function} props.onSelect A function which will be triggered on option selection
* @param {object[]} props.options Array of options
Expand All @@ -20,6 +22,8 @@ import Option from './Option';
*/
export default function OptionsList({
className,
id,
labelId,
onBlur,
onSelect,
options,
Expand All @@ -38,7 +42,14 @@ export default function OptionsList({

return (
<div style={style} className={cssClasses}>
<ul role="menu" className={sc('list')} tabIndex={tabIndex} onKeyDown={onKeyDown}>
<ul
aria-labelledby={labelId}
className={sc('list')}
id={id}
onKeyDown={onKeyDown}
role="listbox"
tabIndex={tabIndex}
>
{options.map((option, index) => (
<Option
key={option.value}
Expand All @@ -56,6 +67,8 @@ export default function OptionsList({

OptionsList.propTypes = {
className: PropTypes.string,
id: PropTypes.string,
labelId: PropTypes.string,
onBlur: PropTypes.func,
onSelect: PropTypes.func.isRequired,
options: PropTypes.arrayOf(PropTypes.shape({
Expand All @@ -71,6 +84,8 @@ OptionsList.propTypes = {

OptionsList.defaultProps = {
className: '',
id: undefined,
labelId: undefined,
onBlur: undefined,
options: [],
selected: '',
Expand Down
14 changes: 12 additions & 2 deletions src/components/inputs/Dropdown/Dropdown.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {useElementPosition, useRef} from '../../hooks';
import Icon from '../../generic/Icon/Icon';
import OptionsList from '../../generic/OptionsList/OptionsList';
import Label from '../Label/Label';
import {uniqueId} from '../../../util';

/**
* Dropdown Component
Expand Down Expand Up @@ -48,6 +49,8 @@ export default function Dropdown({
const controlRef = useRef();
const selectedOptionRef = useRef();
const position = useElementPosition(controlRef);
const labelId = uniqueId();
const optionsId = uniqueId();

const collapse = () => setExpanded(undefined);
const expand = (withKey) => setExpanded({withKey});
Expand Down Expand Up @@ -126,20 +129,25 @@ export default function Dropdown({
className={cssClasses}
style={style}
error={error}
labelId={labelId}
label={controlLabel}
required={required}
>
{/* This element handles delegated keyboard events from its descendants (Esc key) */}
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div className={sc('control')} ref={controlRef} disabled={disabled} onKeyDown={handleKeyDown}>
<div
aria-controls={optionsId}
aria-expanded={expanded}
aria-haspopup="listbox"
aria-label={ariaLabel}
aria-labelledby={labelId}
className={`${sc('selected-option')} ${expanded ? sc('expanded') : ''}`}
onClick={() => toggleExpanded(false)}
role="button"
role="combobox"
tabIndex={disabled ? -1 : tabIndex}
title={tooltip}
onKeyDown={handleSelectedOptionKeyDown}
aria-label={`${label ? `${label}. ` : ''}${ariaLabel}`}
ref={selectedOptionRef}
>
<span className={sc('label')}>{options === null ? 'Loading...' : (label || value || placeholder)}</span>
Expand All @@ -155,6 +163,8 @@ export default function Dropdown({
style={layout}
tabIndex={tabIndex}
onBlur={collapse}
id={optionsId}
labelId={labelId}
/>
)}
</div>
Expand Down
6 changes: 5 additions & 1 deletion src/components/inputs/Label/Label.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Icon from '../../generic/Icon/Icon';
* @param {object} [props.style] Custom style to apply
* @param {React.ReactNode[]} props.children List of children
* @param {string} [props.label] Label text
* @param {string} [props.labelId] Label id
* @param {string} [props.error] Error text
* @param {boolean} [props.required=false] Flag indicating whether the control is required
* @returns {object} JSX of the component
Expand All @@ -20,6 +21,7 @@ export default function Label({
style,
children,
label,
labelId,
error,
required,
}) {
Expand All @@ -28,7 +30,7 @@ export default function Label({
return (
// disabling label-has-associated-control as eslint does not see role attribute as a nested control
// eslint-disable-next-line jsx-a11y/label-has-associated-control
<label className={cssClasses} style={style}>
<label className={cssClasses} style={style} id={labelId}>
{
label && (
<div className={sc('label-text')}>
Expand Down Expand Up @@ -56,6 +58,7 @@ Label.propTypes = {
style: PropTypes.shape(),
children: PropTypes.node.isRequired,
label: PropTypes.string,
labelId: PropTypes.string,
error: PropTypes.string,
required: PropTypes.bool,
};
Expand All @@ -64,6 +67,7 @@ Label.defaultProps = {
className: undefined,
style: undefined,
label: undefined,
labelId: undefined,
error: undefined,
required: false,
};
5 changes: 5 additions & 0 deletions src/storyshots.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ jest.mock('react-dom', () => {
};
});

jest.mock('./util', () => ({
...jest.requireActual('./util'),
uniqueId: () => 'wxc-0',
}));

/**
* Returns a mock DOM ref object for use of snapshot tests.
*
Expand Down
14 changes: 14 additions & 0 deletions src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,19 @@ export function mapValues(object, mapper) {
)));
}

let lastId = 0;

/**
* Generates a unique id to be used in DOM by the components
*
* @returns {string} Returns an unique id
*/
export function uniqueId() {
lastId += 1;

return `wxc-${lastId}`;
}

export default {
deepMerge,
chainWith,
Expand All @@ -138,4 +151,5 @@ export default {
range,
pad2Zeros,
mapValues,
uniqueId,
};

0 comments on commit 71c56e9

Please sign in to comment.