Skip to content

navbar - resolve some a11y issues #188

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
668ab29
worked on the navbar to resolve some a11y issues
MelSumner Jul 1, 2019
5b434d7
alpha sort properties, unitless line-heights
MelSumner Jul 1, 2019
ceec3a2
fixing expanding the navbar
mansona Jul 1, 2019
d19eeee
fixed the spelling of anchor
MelSumner Jul 1, 2019
bf713ee
no one needs z-index 1000
MelSumner Jul 1, 2019
b1af6fd
added some todos
MelSumner Jul 1, 2019
41c5277
added TODOs for how the keyboard navigation should work
MelSumner Jul 1, 2019
3382b2c
added more TODOs
MelSumner Jul 1, 2019
61ed664
added more TODOs
MelSumner Jul 1, 2019
eae226d
added functionality to navbar
MelSumner Jul 3, 2019
acc1589
hid the toggle for desktop viewports
MelSumner Jul 3, 2019
aabd3e0
trying to reset the jsconfig.json file, not sure why it committed
MelSumner Jul 5, 2019
b2f75dd
added removeEventListener
MelSumner Jul 5, 2019
a6d9f68
added a click event listener
MelSumner Jul 5, 2019
ce3c37a
added a TODO for handleBlur
MelSumner Jul 5, 2019
1a8d0e2
a little refactor
MelSumner Jul 6, 2019
c809765
extracted keypress to its own function
MelSumner Jul 6, 2019
b4645c6
added an active class for the toggle button currently in use
MelSumner Jul 6, 2019
6b9035a
updating styles while working on the website
MelSumner Jul 6, 2019
abb938e
more style updates
MelSumner Jul 6, 2019
9894fad
updated some typography and added more styles
MelSumner Jul 6, 2019
5adca24
added section background shape
MelSumner Jul 6, 2019
02b793a
updated the background shape
MelSumner Jul 6, 2019
7073fdb
added es-media element, TODOs and some grid css
MelSumner Jul 7, 2019
ab66134
Merge branch 'website-redesign-rfc' into feature/navbar
MelSumner Jul 25, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions addon/components/es-header/es-brand.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Component from '@ember/component';
import layout from '../../templates/components/es-header/es-brand';

export default Component.extend({
layout
});
6 changes: 6 additions & 0 deletions addon/components/es-header/es-search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Component from '@ember/component';
import layout from '../../templates/components/es-header/es-search';

export default Component.extend({
layout
});
3 changes: 1 addition & 2 deletions addon/components/es-navbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ export default class EsNavbar extends Component {
}

toggleMenu() {
let menu = this.element.querySelector('ul[role="menubar"]');

let menu = this.element.querySelector('.navbar-toggler');
menu.setAttribute('aria-expanded', menu.getAttribute('aria-expanded') !== 'true');
}
}
270 changes: 66 additions & 204 deletions addon/components/es-navbar/link/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,242 +3,104 @@ import layout from './template';
import { computed } from '@ember/object';
import { equal } from '@ember/object/computed';
import { inject as service } from '@ember/service';
import { next } from '@ember/runloop';
import { schedule, next } from '@ember/runloop';

export default Component.extend({
layout,
tagName: 'li',
tabIndex: 0,

role: 'menuitem',

attributeBindings: ['role'],
classNames: ['navbar-list-item'],
classNameBindings: ['isDropdown:dropdown'],
isDropdown: equal('link.type', 'dropdown'),
isDropdownOpen: false,

keyCode: Object.freeze({
'TAB': 9,
'RETURN': 13,
'ESC': 27,
'SPACE': 32,
'PAGEUP': 33,
'PAGEDOWN': 34,
'END': 35,
'HOME': 36,
'LEFT': 37,
'UP': 38,
'RIGHT': 39,
'DOWN': 40
// because aria-expanded requires a string value instead of a boolean
isExpanded: computed('isDropdownOpen', function() {
return this.isDropdownOpen ? 'true' : 'false';
}),

navbar: service(),

didInsertElement() {
this.element.tabIndex = -1;

this.get('navbar').register(this);
this.domNode = this.element.querySelector('ul[role="menu"]');

if(this.domNode) {
this.element.querySelector('a').onmousedown = () => this.expand();
let links = Array.from(this.domNode.querySelectorAll('a'))

links.forEach((ancor) => {
ancor.addEventListener('blur', () => this.handleBlur());
});
actions: {
toggleDropdown() {
this.toggleProperty('isDropdownOpen');
if (this.isDropdownOpen) {
// if it's open, let's make sure it can do some things
schedule('afterRender', this, function() {

// move focus to the first item in the dropdown
this.processFirstElementFocus();
this.processKeyPress();
});
}
}
},

handleBlur() {
next(this, function() {
let subItems = Array.from(this.element.querySelectorAll('ul[role="menu"] li'));
let focused = subItems.find(item => document.activeElement === item.querySelector('a'));

// debugger
if(!focused) {
this.closePopupMenu();
}
})
closeDropdown() {
// set the isDropdownOpen to false, which will make the dropdown go away
this.set('isDropdownOpen', false);
},

openPopupMenu() {
// Get position and bounding rectangle of controller object's DOM node
var rect = this.element.getBoundingClientRect();

// Set CSS properties
if(this.domNode) {
this.domNode.style.display = 'block';
this.domNode.style.top = rect.height + 'px';
this.domNode.style.zIndex = 1000;
}

this.set('expanded', true);
openDropdown() { //might not need this
// open the dropdown and set the focus to the first item inside
this.set('isDropdownOpen', true);
this.processFirstElementFocus();
},

closePopupMenu(force) {
var controllerHasHover = this.hasHover;

var hasFocus = this.hasFocus;

if (!this.isMenubarItem) {
controllerHasHover = false;
}
processBlur() {
next(this, function() {
let subItems = Array.from(this.element.querySelectorAll('.navbar-dropdown-list li'));
let focused = subItems.find(item => document.activeElement === item.querySelector('a'));

if (force || (!hasFocus && !this.hasHover && !controllerHasHover)) {
if(this.domNode) {
this.domNode.style.display = 'none';
this.domNode.style.zIndex = 0;
//if the dropdown isn't focused, close it
if (!focused) {
this.closeDropdown();
}
this.set('expanded', false);
}
},

expanded: computed({
get() {
return this.element.getAttribute('aria-expanded') === 'true';
},
set(key, value) {
this.element.setAttribute('aria-expanded', value);
}
}).volatile(),
});
},

setFocusToFirstItem() {
let element = this.element.querySelector('ul[role="menu"] li a')
if (element) {
element.focus();
}
processClick() {
// TODO handle mouseclick outside the current dropdown
},

setFocusToLastItem() {
this.element.querySelector('ul[role="menu"] li a:last-of-type').focus();
processFirstElementFocus() {
// Identify the first item in the dropdown list & set focus on it
let firstFocusable = this.element.querySelector('.navbar-dropdown-list li:first-of-type a');
firstFocusable.focus();
},

setFocusToNextItem() {
let subItems = Array.from(this.element.querySelectorAll('ul[role="menu"] li'));

let focused = subItems.find(item => document.activeElement === item.querySelector('a'));
let focusedIndex = subItems.indexOf(focused);

let nextItem = subItems[(focusedIndex + 1) % subItems.length];
processKeyPress() {
// add event listeners
let dropdownList = this.element.querySelector('.navbar-dropdown-list');

if (!nextItem) {
return;
}

nextItem.querySelector('a').focus();
},

setFocusToPreviousItem() {
let subItems = Array.from(this.element.querySelectorAll('ul[role="menu"] li'));

let focused = subItems.find(item => document.activeElement === item.querySelector('a'));
let focusedIndex = subItems.indexOf(focused);

let nextIndex = focusedIndex - 1;

if (nextIndex < 0) {
nextIndex = subItems.length - 1;
}
//...for certain keypress events
dropdownList.addEventListener('keydown', event => {

let nextItem = subItems[nextIndex];
// ESC key should close the dropdown and return focus to the toggle
if (event.keyCode === 27 && this.isDropdownOpen) {
this.closeDropdown();
this.returnFocus();

if (!nextItem) {
return;
}
// if focus leaves the open dropdown via keypress, close it (without trying to otherwise control focus)
} else if (this.isDropdownOpen) {
this.processBlur();

nextItem.querySelector('a').focus();
} else {
return;
}
});
},

keyDown(event) {
let flag = false;
let clickEvent;
let mousedownEvent;

switch (event.keyCode) {
case this.keyCode.RETURN:
case this.keyCode.SPACE:
// Create simulated mouse event to mimic the behavior of ATs
// and let the event handler handleClick do the housekeeping.
mousedownEvent = new MouseEvent('mousedown', {
'view': window,
'bubbles': true,
'cancelable': true
});
clickEvent = new MouseEvent('click', {
'view': window,
'bubbles': true,
'cancelable': true
});

document.activeElement.dispatchEvent(mousedownEvent);
document.activeElement.dispatchEvent(clickEvent);

flag = true;
break;
case this.keyCode.DOWN:
if(this.get('expanded')) {
this.setFocusToNextItem();
} else {
this.openPopupMenu();
this.setFocusToFirstItem();
}
flag = true;
break;

case this.keyCode.LEFT:
this.get('navbar').setFocusToPreviousItem(this);
flag = true;
break;

case this.keyCode.RIGHT:
this.get('navbar').setFocusToNextItem(this);
flag = true;
break;

case this.keyCode.UP:
if(this.get('expanded')) {
this.setFocusToPreviousItem();
} else {
this.openPopupMenu();
this.setFocusToLastItem();
}
break;

case this.keyCode.HOME:
case this.keyCode.PAGEUP:
this.setFocusToFirstItem();
flag = true;
break;

case this.keyCode.END:
case this.keyCode.PAGEDOWN:
this.setFocusToLastItem();
flag = true;
break;

case this.keyCode.TAB:
this.closePopupMenu(true);
break;

case this.keyCode.ESC:
this.closePopupMenu(true);
break;
}

if (flag) {
event.stopPropagation();
event.preventDefault();
}
returnFocus() {
// after that rendering bit happens, we need to return the focus to the trigger
schedule('afterRender', this, function() {
let dropdownTrigger = this.element.querySelector('.navbar-list-item-dropdown-toggle');
dropdownTrigger.focus();
});
},

expand() {
next(this, () => {
if(this.get('expanded')) {
this.closePopupMenu();
} else {
this.openPopupMenu();
this.setFocusToFirstItem();
}
})
willDestroyElement() {
document.removeEventListener('keydown', this.triggerDropdown);
// document.removeEventListener('click', this.triggerDropdown);
}
});
54 changes: 25 additions & 29 deletions addon/components/es-navbar/link/template.hbs
Original file line number Diff line number Diff line change
@@ -1,40 +1,36 @@
{{#if (eq link.type "link")}}
<a
class="navbar-link__dropdown"
aria-haspopup="true"
aria-expanded="false"
class="navbar-list-item-link"
href={{link.href}}
tabindex={{if (eq index 0) 0 -1}}
>
{{link.name}}
</a>
{{/if}}
{{#if (eq link.type "dropdown")}}
<a
class="navbar-link__dropdown"
aria-haspopup="true"
aria-expanded="false"
href="javascript:void(0)"
tabindex={{if (eq index 0) 0 -1}}
<button
onclick={{action "toggleDropdown"}}
class="navbar-list-item-dropdown-toggle {{if isDropdownOpen "active"}}"
aria-expanded={{isExpanded}}
>
{{link.name}}
</a>
<ul class="dropdown" role="menu" aria-label={{link.name}}>
{{#each link.items as |item|}}
{{#if (eq item.type "link")}}
<li role="none">
<a
class="navbar-link__dropdown-item"
href={{item.href}}
tabindex="-1"
>
{{item.name}}
</a>
</li>
{{/if}}
{{#if (eq item.type "divider")}}
<hr>
{{/if}}
{{/each}}
</ul>
</button>
{{#if isDropdownOpen}}
<ul class="navbar-dropdown-list">
{{#each link.items as |item|}}
{{#if (eq item.type "link")}}
<li class="navbar-dropdown-list-item">
<a
class="navbar-dropdown-list-item-link"
href={{item.href}}
>
{{item.name}}
</a>
</li>
{{/if}}
{{#if (eq item.type "divider")}}
<li role="separator" class="separator"></li>
{{/if}}
{{/each}}
</ul>
{{/if}}
{{/if}}
Loading