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:
parent
d7732499b9
commit
516bab133f
5 changed files with 162 additions and 38 deletions
|
|
@ -67,6 +67,7 @@ const themeSwitcherDecorator = (storyFn) => {
|
|||
};
|
||||
|
||||
addDecorator(themeSwitcherDecorator);
|
||||
addDecorator((Story) => <Story />);
|
||||
|
||||
addParameters({
|
||||
options: {
|
||||
|
|
|
|||
|
|
@ -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'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'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'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 = {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'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'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'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 = {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue