Skip to content

Commit a0626b0

Browse files
committed
fix(a11y) Make Dropdown more accessible
- make it focusable - allow navigation using arrow keys - dismiss on escape
1 parent 24a3e39 commit a0626b0

File tree

3 files changed

+89
-19
lines changed

3 files changed

+89
-19
lines changed

src/components/Dropdown/Dropdown.jsx

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,43 +7,105 @@ export default class Dropdown extends React.Component {
77
active: false
88
};
99

10+
componentDidMount() {
11+
document.addEventListener('keyup', this._closeDropdownOnEsc.bind(this), true);
12+
document.addEventListener('focus', this._closeDropdownIfFocusLost.bind(this), true);
13+
document.addEventListener('click', this._closeDropdownIfFocusLost.bind(this), true);
14+
}
15+
16+
_closeDropdownOnEsc(e) {
17+
if (e.key === "Escape" && this.state.active) {
18+
this.setState({ active: false}, () => {
19+
this.dropdownButton.focus();
20+
});
21+
}
22+
}
23+
24+
_closeDropdownIfFocusLost(e) {
25+
if (this.state.active && !this.dropdown.contains(e.target)) {
26+
this.setState({ active: false });
27+
}
28+
}
29+
1030
render() {
1131
let { className = '', items = [] } = this.props;
1232
let activeMod = this.state.active ? "dropdown__list--active" : "";
1333

1434
return (
15-
<div
16-
tabIndex="0"
35+
<nav
1736
className={ `dropdown ${className}` }
37+
ref={ el => this.dropdown = el }
1838
onMouseOver={ this._toggle.bind(this, true) }
19-
onMouseLeave={ this._toggle.bind(this, false) }>
20-
<img
21-
className="dropdown__language"
22-
alt="select language"
23-
src={ LanguageIcon } />
24-
{/* Commented out until media breakpoints are in place
25-
<span>{ items[0].title }</span> */}
26-
<i aria-hidden="true" className="dropdown__arrow" />
27-
39+
onMouseLeave={ this._toggle.bind(this, false) }
40+
>
41+
<button
42+
ref={ el => this.dropdownButton = el }
43+
aria-haspopup="true"
44+
aria-expanded={ String(this.state.active) }
45+
aria-label="Select language"
46+
onClick={ this._handleClick.bind(this) }
47+
>
48+
<img
49+
className="dropdown__language"
50+
alt="select language"
51+
src={ LanguageIcon } />
52+
{/* Commented out until media breakpoints are in place
53+
<span>{ items[0].title }</span> */}
54+
<i aria-hidden="true" className="dropdown__arrow" />
55+
</button>
2856
<div className={ `dropdown__list ${activeMod}` }>
2957
<ul>
3058
{
31-
items.map(item => {
59+
items.map((item, i) => {
3260
return (
3361
<li key={ item.title }>
34-
<a href={ item.url }>
35-
<span>{ item.title }</span>
62+
<a
63+
onKeyDown={this._handleArrowKeys.bind(this, i, items.length - 1)}
64+
ref={ node => this.links ? this.links.push(node) : this.links = [node] }
65+
href={ item.url }>
66+
<span lang={ item.lang }>{ item.title }</span>
3667
</a>
3768
</li>
3869
);
3970
})
4071
}
4172
</ul>
4273
</div>
43-
</div>
74+
</nav>
4475
);
4576
}
4677

78+
_handleArrowKeys(currentIndex, lastIndex, e) {
79+
if (["ArrowDown", "ArrowUp"].includes(e.key)) {
80+
e.preventDefault();
81+
}
82+
83+
let newIndex = currentIndex;
84+
if (e.key === "ArrowDown") {
85+
newIndex++;
86+
if (newIndex > lastIndex) {
87+
newIndex = 0;
88+
}
89+
}
90+
91+
if (e.key === "ArrowUp") {
92+
newIndex--;
93+
if (newIndex < 0) {
94+
newIndex = lastIndex;
95+
}
96+
}
97+
98+
this.links[newIndex].focus();
99+
}
100+
101+
_handleClick(e) {
102+
this.setState({active: !this.state.active}, () => {
103+
if (this.state.active) {
104+
this.links[0].focus();
105+
}
106+
});
107+
}
108+
47109
/**
48110
* Toggle visibility of dropdown items
49111
*

src/components/Dropdown/Dropdown.scss

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,16 @@
22

33
.dropdown {
44
position: relative;
5-
color: #fff;
6-
cursor: pointer;
5+
6+
button {
7+
cursor: pointer;
8+
color: #fff;
9+
border: none;
10+
background-color: transparent;
11+
margin: 0;
12+
padding: 0;
13+
font-size: inherit;
14+
}
715
}
816

917
.dropdown__language {

src/components/Navigation/Navigation.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ export default class Navigation extends React.Component {
4545
<Dropdown
4646
className="navigation__languages"
4747
items={[
48-
{ title: 'English', url: 'https://webpack.js.org/' },
49-
{ title: '中文', url: 'https://doc.webpack-china.org/' }
48+
{ lang: 'en', title: 'English', url: 'https://webpack.js.org/' },
49+
{ lang: 'zh', title: '中文', url: 'https://doc.webpack-china.org/' }
5050
]} />
5151
)
5252
}

0 commit comments

Comments
 (0)