Storybook dropdown should be accessible 11724 (#12295)

* allow keyboard access to dropdown stories

* add accessibility notes to dropdown stories

* manage focus in html example

* Update reference to aria attributes in notes

Co-authored-by: Vaidehi Joshi <vaidehi.sj@gmail.com>

* add focus style to link for keyboard users

Co-authored-by: Vaidehi Joshi <vaidehi.sj@gmail.com>
This commit is contained in:
Suzanne Aitchison 2021-01-19 08:51:37 +00:00 committed by GitHub
parent d7732499b9
commit 516bab133f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 162 additions and 38 deletions

View file

@ -67,6 +67,7 @@ const themeSwitcherDecorator = (storyFn) => {
};
addDecorator(themeSwitcherDecorator);
addDecorator((Story) => <Story />);
addParameters({
options: {

View file

@ -1,4 +1,5 @@
import { h } from 'preact';
import { useState, useRef, useEffect } from 'preact/hooks';
import { withKnobs, text } from '@storybook/addon-knobs';
import './dropdown-css-helper.scss';
import notes from './dropdowns.md';
@ -10,16 +11,75 @@ export default {
parameters: { notes },
};
const DropdownWithActivator = ({ additionalDropdownClasses }) => {
const activatorRef = useRef(null);
const dropdownInteractiveItemRef = useRef(null);
const [isOpen, setIsOpen] = useState(false);
const handleActivatorClick = () => {
setIsOpen(!isOpen);
};
const handleKeyUp = (event) => {
if (event.key === 'Escape' && isOpen) {
setIsOpen(false);
}
};
const handleClickOutside = (event) => {
if (
dropdownInteractiveItemRef.current?.contains(event.target) ||
activatorRef.current?.contains(event.target)
) {
return;
}
setIsOpen(false);
};
useEffect(() => {
if (isOpen) {
dropdownInteractiveItemRef.current.focus();
document.addEventListener('mousedown', handleClickOutside);
} else {
document.removeEventListener('mousedown', handleClickOutside);
activatorRef.current.focus();
}
}, [isOpen]);
return (
// keyUp listener allows dropdown to be closed by keyboard, and is not interactive iteself
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div className="dropdown-trigger-container" onKeyUp={handleKeyUp}>
<button
className="crayons-btn dropdown-trigger"
aria-haspopup="true"
aria-expanded={isOpen}
aria-controls="dropdown-content"
ref={activatorRef}
onClick={handleActivatorClick}
>
Click to trigger dropdown
</button>
<Dropdown
className={`${additionalDropdownClasses} ${isOpen ? 'active' : ''}`}
id="dropdown-content"
>
<p>
Hey, I&apos;m a dropdown content! Lorem ipsum dolor sit amet,
consectetur adipisicing elit.
</p>
<a href="/" ref={dropdownInteractiveItemRef}>
Sequi ea voluptates
</a>
</Dropdown>
</div>
);
};
export const Default = () => (
<div className="dropdown-trigger-container">
<a href="/" className="crayons-btn dropdown-trigger">
Hover to trigger dropdown
</a>
<Dropdown className={text('className', 'mb-2')}>
Hey, I&apos;m a dropdown content! Lorem ipsum dolor sit amet, consectetur
adipisicing elit. Sequi ea voluptates quaerat eos consequuntur temporibus.
</Dropdown>
</div>
<DropdownWithActivator
additionalDropdownClasses={text('className', 'mb-2')}
/>
);
Default.story = {
@ -27,15 +87,7 @@ Default.story = {
};
export const AdditonalCssClasses = () => (
<div className="dropdown-trigger-container">
<a href="/" className="crayons-btn dropdown-trigger">
Hover to trigger dropdown
</a>
<Dropdown className={text('className', 'p-6')}>
Hey, I&apos;m a dropdown content! Lorem ipsum dolor sit amet, consectetur
adipisicing elit. Sequi ea voluptates quaerat eos consequuntur temporibus.
</Dropdown>
</div>
<DropdownWithActivator additionalDropdownClasses={text('className', 'p-6')} />
);
AdditonalCssClasses.story = {

View file

@ -1,7 +1,15 @@
.dropdown-trigger-container {
position: relative;
.dropdown-trigger:hover + div {
.dropdown-trigger + .active {
display: block;
}
a:focus {
outline: -webkit-focus-ring-color auto 1px;
}
a:focus:not(:focus-visible) {
outline: none;
}
}

View file

@ -1,4 +1,5 @@
import { h } from 'preact';
import { useState, useRef, useEffect } from 'preact/hooks';
import '../../storybook-utilities/designSystem.scss';
import notes from './dropdowns.md';
@ -7,32 +8,81 @@ export default {
parameters: { notes },
};
export const Default = () => (
<div className="dropdown-trigger-container">
<a href="/" className="crayons-btn dropdown-trigger">
Hover to trigger dropdown
</a>
<div className="crayons-dropdown">
Hey, I&apos;m a dropdown content! Lorem ipsum dolor sit amet, consectetur
adipisicing elit. Sequi ea voluptates quaerat eos consequuntur temporibus.
const DropdownWithActivator = ({ additionalDropdownClasses = '' }) => {
const activatorRef = useRef(null);
const dropdownInteractiveItemRef = useRef(null);
const [isOpen, setIsOpen] = useState(false);
const handleActivatorClick = () => {
setIsOpen(!isOpen);
};
const handleKeyUp = (event) => {
if (event.key === 'Escape' && isOpen) {
setIsOpen(false);
}
};
const handleClickOutside = (event) => {
if (
dropdownInteractiveItemRef.current?.contains(event.target) ||
activatorRef.current?.contains(event.target)
) {
return;
}
setIsOpen(false);
};
useEffect(() => {
if (isOpen) {
dropdownInteractiveItemRef.current.focus();
document.addEventListener('mousedown', handleClickOutside);
} else {
document.removeEventListener('mousedown', handleClickOutside);
activatorRef.current.focus();
}
}, [isOpen]);
return (
// keyUp listener allows dropdown to be closed by keyboard, and is not interactive iteself
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div className="dropdown-trigger-container" onKeyUp={handleKeyUp}>
<button
className="crayons-btn dropdown-trigger"
aria-haspopup="true"
aria-expanded={isOpen}
aria-controls="dropdown-content"
ref={activatorRef}
onClick={handleActivatorClick}
>
Click to trigger dropdown
</button>
<div
className={`crayons-dropdown ${additionalDropdownClasses} ${
isOpen ? 'active' : ''
}`}
id="dropdown-content"
>
<p>
Hey, I&apos;m a dropdown content! Lorem ipsum dolor sit amet,
consectetur adipisicing elit.
</p>
<a href="/" ref={dropdownInteractiveItemRef}>
Sequi ea voluptates
</a>
</div>
</div>
</div>
);
);
};
export const Default = () => <DropdownWithActivator />;
Default.story = {
name: 'default',
};
export const AdditonalCssClasses = () => (
<div className="dropdown-trigger-container">
<a href="/" className="crayons-btn dropdown-trigger">
Hover to trigger dropdown
</a>
<div className="crayons-dropdown p-6">
Hey, I&apos;m a dropdown content! Lorem ipsum dolor sit amet, consectetur
adipisicing elit. Sequi ea voluptates quaerat eos consequuntur temporibus.
</div>
</div>
<DropdownWithActivator additionalDropdownClasses="p-6" />
);
AdditonalCssClasses.story = {

View file

@ -9,3 +9,16 @@ dependent on width:
- < 250px: 16px 251 - 320px: 24px
FYI: Dropdowns use “Box” component as background, with Level 3 elevation.
### Dropdown accessibility
When a dropdown of options is opened, focus should be sent to the first
interactive item inside the dropdown content. When the dropdown is closed, focus
should return to the activator button. If there are no interactive items,
consider using an `aria-live` region to ensure the new content is announced to
screen-reader users when the dropdown appears.
A user should be able to open and close the dropdown by keyboard (e.g. using
Enter and Escape), and appropriate `aria` attributes (e.g. `aria-haspopup`) should be used
to indicate the relationship between the activator button and the dropdown
content.