Skip to content

Commit a3d6b24

Browse files
authored
Merge pull request #202 from ember-learn/navbar-only
Resolve accessibility issues with the navbar
2 parents 0a59b8d + 0a036c4 commit a3d6b24

File tree

7 files changed

+228
-369
lines changed

7 files changed

+228
-369
lines changed

addon/components/es-navbar.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ export default class EsNavbar extends Component {
2020
}
2121

2222
toggleMenu() {
23-
let menu = this.element.querySelector('ul[role="menubar"]');
24-
23+
let menu = this.element.querySelector('.navbar-toggler');
2524
menu.setAttribute('aria-expanded', menu.getAttribute('aria-expanded') !== 'true');
2625
}
2726
}

addon/components/es-navbar/link/component.js

Lines changed: 63 additions & 201 deletions
Original file line numberDiff line numberDiff line change
@@ -3,242 +3,104 @@ import layout from './template';
33
import { computed } from '@ember/object';
44
import { equal } from '@ember/object/computed';
55
import { inject as service } from '@ember/service';
6-
import { next } from '@ember/runloop';
6+
import { schedule, next } from '@ember/runloop';
77

88
export default Component.extend({
99
layout,
1010
tagName: 'li',
11-
tabIndex: 0,
12-
13-
role: 'menuitem',
14-
15-
attributeBindings: ['role'],
11+
classNames: ['navbar-list-item'],
1612
classNameBindings: ['isDropdown:dropdown'],
1713
isDropdown: equal('link.type', 'dropdown'),
14+
isDropdownOpen: false,
1815

19-
keyCode: Object.freeze({
20-
'TAB': 9,
21-
'RETURN': 13,
22-
'ESC': 27,
23-
'SPACE': 32,
24-
'PAGEUP': 33,
25-
'PAGEDOWN': 34,
26-
'END': 35,
27-
'HOME': 36,
28-
'LEFT': 37,
29-
'UP': 38,
30-
'RIGHT': 39,
31-
'DOWN': 40
16+
// because aria-expanded requires a string value instead of a boolean
17+
isExpanded: computed('isDropdownOpen', function() {
18+
return this.isDropdownOpen ? 'true' : 'false';
3219
}),
3320

3421
navbar: service(),
3522

36-
didInsertElement() {
37-
this.element.tabIndex = -1;
38-
39-
this.get('navbar').register(this);
40-
this.domNode = this.element.querySelector('ul[role="menu"]');
23+
actions: {
24+
toggleDropdown() {
25+
this.toggleProperty('isDropdownOpen');
4126

42-
if(this.domNode) {
43-
this.element.querySelector('a').onmousedown = () => this.expand();
44-
let links = Array.from(this.domNode.querySelectorAll('a'))
27+
if (this.isDropdownOpen) {
28+
// if it's open, let's make sure it can do some things
29+
schedule('afterRender', this, function() {
4530

46-
links.forEach((ancor) => {
47-
ancor.addEventListener('blur', () => this.handleBlur());
48-
});
31+
// move focus to the first item in the dropdown
32+
this.processFirstElementFocus();
33+
this.processKeyPress();
34+
});
35+
}
4936
}
5037
},
5138

52-
handleBlur() {
53-
next(this, function() {
54-
let subItems = Array.from(this.element.querySelectorAll('ul[role="menu"] li'));
55-
let focused = subItems.find(item => document.activeElement === item.querySelector('a'));
56-
57-
// debugger
58-
if(!focused) {
59-
this.closePopupMenu();
60-
}
61-
})
39+
closeDropdown() {
40+
// set the isDropdownOpen to false, which will make the dropdown go away
41+
this.set('isDropdownOpen', false);
6242
},
6343

64-
openPopupMenu() {
65-
// Get position and bounding rectangle of controller object's DOM node
66-
var rect = this.element.getBoundingClientRect();
67-
68-
// Set CSS properties
69-
if(this.domNode) {
70-
this.domNode.style.display = 'block';
71-
this.domNode.style.top = rect.height + 'px';
72-
this.domNode.style.zIndex = 1000;
73-
}
74-
75-
this.set('expanded', true);
44+
openDropdown() { //might not need this
45+
// open the dropdown and set the focus to the first item inside
46+
this.set('isDropdownOpen', true);
47+
this.processFirstElementFocus();
7648
},
7749

78-
closePopupMenu(force) {
79-
var controllerHasHover = this.hasHover;
80-
81-
var hasFocus = this.hasFocus;
82-
83-
if (!this.isMenubarItem) {
84-
controllerHasHover = false;
85-
}
50+
processBlur() {
51+
next(this, function() {
52+
let subItems = Array.from(this.element.querySelectorAll('.navbar-dropdown-list li'));
53+
let focused = subItems.find(item => document.activeElement === item.querySelector('a'));
8654

87-
if (force || (!hasFocus && !this.hasHover && !controllerHasHover)) {
88-
if(this.domNode) {
89-
this.domNode.style.display = 'none';
90-
this.domNode.style.zIndex = 0;
55+
//if the dropdown isn't focused, close it
56+
if (!focused) {
57+
this.closeDropdown();
9158
}
92-
this.set('expanded', false);
93-
}
94-
},
95-
96-
expanded: computed({
97-
get() {
98-
return this.element.getAttribute('aria-expanded') === 'true';
99-
},
100-
set(key, value) {
101-
this.element.setAttribute('aria-expanded', value);
102-
}
103-
}).volatile(),
104-
105-
setFocusToFirstItem() {
106-
let element = this.element.querySelector('ul[role="menu"] li a')
107-
if (element) {
108-
element.focus();
109-
}
59+
});
11060
},
11161

112-
setFocusToLastItem() {
113-
this.element.querySelector('ul[role="menu"] li a:last-of-type').focus();
62+
processClick() {
63+
// TODO handle mouseclick outside the current dropdown
11464
},
11565

116-
setFocusToNextItem() {
117-
let subItems = Array.from(this.element.querySelectorAll('ul[role="menu"] li'));
118-
119-
let focused = subItems.find(item => document.activeElement === item.querySelector('a'));
120-
let focusedIndex = subItems.indexOf(focused);
121-
122-
let nextItem = subItems[(focusedIndex + 1) % subItems.length];
123-
124-
if (!nextItem) {
125-
return;
126-
}
127-
128-
nextItem.querySelector('a').focus();
66+
processFirstElementFocus() {
67+
// Identify the first item in the dropdown list & set focus on it
68+
let firstFocusable = this.element.querySelector('.navbar-dropdown-list li:first-of-type a');
69+
firstFocusable.focus();
12970
},
13071

131-
setFocusToPreviousItem() {
132-
let subItems = Array.from(this.element.querySelectorAll('ul[role="menu"] li'));
133-
134-
let focused = subItems.find(item => document.activeElement === item.querySelector('a'));
135-
let focusedIndex = subItems.indexOf(focused);
72+
processKeyPress() {
73+
// add event listeners
74+
let dropdownList = this.element.querySelector('.navbar-dropdown-list');
13675

137-
let nextIndex = focusedIndex - 1;
76+
//...for certain keypress events
77+
dropdownList.addEventListener('keydown', event => {
13878

139-
if (nextIndex < 0) {
140-
nextIndex = subItems.length - 1;
141-
}
79+
// ESC key should close the dropdown and return focus to the toggle
80+
if (event.keyCode === 27 && this.isDropdownOpen) {
81+
this.closeDropdown();
82+
this.returnFocus();
14283

143-
let nextItem = subItems[nextIndex];
144-
145-
if (!nextItem) {
146-
return;
147-
}
84+
// if focus leaves the open dropdown via keypress, close it (without trying to otherwise control focus)
85+
} else if (this.isDropdownOpen) {
86+
this.processBlur();
14887

149-
nextItem.querySelector('a').focus();
88+
} else {
89+
return;
90+
}
91+
});
15092
},
15193

152-
keyDown(event) {
153-
let flag = false;
154-
let clickEvent;
155-
let mousedownEvent;
156-
157-
switch (event.keyCode) {
158-
case this.keyCode.RETURN:
159-
case this.keyCode.SPACE:
160-
// Create simulated mouse event to mimic the behavior of ATs
161-
// and let the event handler handleClick do the housekeeping.
162-
mousedownEvent = new MouseEvent('mousedown', {
163-
'view': window,
164-
'bubbles': true,
165-
'cancelable': true
166-
});
167-
clickEvent = new MouseEvent('click', {
168-
'view': window,
169-
'bubbles': true,
170-
'cancelable': true
171-
});
172-
173-
document.activeElement.dispatchEvent(mousedownEvent);
174-
document.activeElement.dispatchEvent(clickEvent);
175-
176-
flag = true;
177-
break;
178-
case this.keyCode.DOWN:
179-
if(this.get('expanded')) {
180-
this.setFocusToNextItem();
181-
} else {
182-
this.openPopupMenu();
183-
this.setFocusToFirstItem();
184-
}
185-
flag = true;
186-
break;
187-
188-
case this.keyCode.LEFT:
189-
this.get('navbar').setFocusToPreviousItem(this);
190-
flag = true;
191-
break;
192-
193-
case this.keyCode.RIGHT:
194-
this.get('navbar').setFocusToNextItem(this);
195-
flag = true;
196-
break;
197-
198-
case this.keyCode.UP:
199-
if(this.get('expanded')) {
200-
this.setFocusToPreviousItem();
201-
} else {
202-
this.openPopupMenu();
203-
this.setFocusToLastItem();
204-
}
205-
break;
206-
207-
case this.keyCode.HOME:
208-
case this.keyCode.PAGEUP:
209-
this.setFocusToFirstItem();
210-
flag = true;
211-
break;
212-
213-
case this.keyCode.END:
214-
case this.keyCode.PAGEDOWN:
215-
this.setFocusToLastItem();
216-
flag = true;
217-
break;
218-
219-
case this.keyCode.TAB:
220-
this.closePopupMenu(true);
221-
break;
222-
223-
case this.keyCode.ESC:
224-
this.closePopupMenu(true);
225-
break;
226-
}
227-
228-
if (flag) {
229-
event.stopPropagation();
230-
event.preventDefault();
231-
}
94+
returnFocus() {
95+
// after that rendering bit happens, we need to return the focus to the trigger
96+
schedule('afterRender', this, function() {
97+
let dropdownTrigger = this.element.querySelector('.navbar-list-item-dropdown-toggle');
98+
dropdownTrigger.focus();
99+
});
232100
},
233101

234-
expand() {
235-
next(this, () => {
236-
if(this.get('expanded')) {
237-
this.closePopupMenu();
238-
} else {
239-
this.openPopupMenu();
240-
this.setFocusToFirstItem();
241-
}
242-
})
102+
willDestroyElement() {
103+
document.removeEventListener('keydown', this.triggerDropdown);
104+
// document.removeEventListener('click', this.triggerDropdown);
243105
}
244106
});
Lines changed: 25 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,36 @@
11
{{#if (eq link.type "link")}}
22
<a
3-
class="navbar-link__dropdown"
4-
aria-haspopup="true"
5-
aria-expanded="false"
3+
class="navbar-list-item-link"
64
href={{link.href}}
7-
tabindex={{if (eq index 0) 0 -1}}
85
>
96
{{link.name}}
107
</a>
118
{{/if}}
129
{{#if (eq link.type "dropdown")}}
13-
<a
14-
class="navbar-link__dropdown"
15-
aria-haspopup="true"
16-
aria-expanded="false"
17-
href="javascript:void(0)"
18-
tabindex={{if (eq index 0) 0 -1}}
10+
<button
11+
onclick={{action "toggleDropdown"}}
12+
class="navbar-list-item-dropdown-toggle {{if isDropdownOpen "active"}}"
13+
aria-expanded={{isExpanded}}
1914
>
2015
{{link.name}}
21-
</a>
22-
<ul class="dropdown" role="menu" aria-label={{link.name}}>
23-
{{#each link.items as |item|}}
24-
{{#if (eq item.type "link")}}
25-
<li role="none">
26-
<a
27-
class="navbar-link__dropdown-item"
28-
href={{item.href}}
29-
tabindex="-1"
30-
>
31-
{{item.name}}
32-
</a>
33-
</li>
34-
{{/if}}
35-
{{#if (eq item.type "divider")}}
36-
<hr>
37-
{{/if}}
38-
{{/each}}
39-
</ul>
16+
</button>
17+
{{#if isDropdownOpen}}
18+
<ul class="navbar-dropdown-list">
19+
{{#each link.items as |item|}}
20+
{{#if (eq item.type "link")}}
21+
<li class="navbar-dropdown-list-item">
22+
<a
23+
class="navbar-dropdown-list-item-link"
24+
href={{item.href}}
25+
>
26+
{{item.name}}
27+
</a>
28+
</li>
29+
{{/if}}
30+
{{#if (eq item.type "divider")}}
31+
<li role="separator" class="separator"></li>
32+
{{/if}}
33+
{{/each}}
34+
</ul>
35+
{{/if}}
4036
{{/if}}

0 commit comments

Comments
 (0)