diff --git a/css/nav.css b/css/nav.css index ac57ffd..6b8c43a 100644 --- a/css/nav.css +++ b/css/nav.css @@ -42,6 +42,14 @@ margin-right: 62px; } +.Nav-titleLink, +.Nav-titleLink:active, +.Nav-titleLink:focus, +.Nav-titleLink:hover, +.Nav-titleLink:visited { + color: #fff; +} + .Nav-close { background-color: transparent; border: none; @@ -52,9 +60,17 @@ position: absolute; right: 15px; top: 15px; + transition: filter var(--Nav-transitionDuration) ease-out; width: 32px; } +.Nav-close:active, +.Nav-close:focus, +.Nav-close:hover { + filter: drop-shadow(0 0 2px #fff); + outline: 0; +} + .Nav-closeBar { background-color: #fff; border-radius: 2px; diff --git a/js/menu.js b/js/menu.js index 7648b3b..e4f60d4 100644 --- a/js/menu.js +++ b/js/menu.js @@ -12,12 +12,14 @@ export default class Menu { } init() { + if (!this.state) return false; this.container.setAttribute('aria-haspopup', true); this.container.setAttribute('aria-expanded', false); this.attachEvents(); setTimeout(() => { this.showMenu(); }, 1000); + return true; } attachEvents() { diff --git a/js/navigation.js b/js/navigation.js index 48f9735..5f7f47b 100644 --- a/js/navigation.js +++ b/js/navigation.js @@ -12,19 +12,19 @@ export default class Navigation { } init() { + if (!this.state) return false; this.container.setAttribute('aria-hidden', true); - this.container.querySelector('.Nav-list').setAttribute('role', 'menubar'); - this.container.querySelectorAll('.Nav-listItem').forEach((item) => { - // Override implied listitem role - item.setAttribute('role', 'none'); - }); - this.container.querySelectorAll('.Nav-link').forEach((link) => { - link.setAttribute('role', 'menuitem'); - }); - this.attachEvents(); + this.attachClickEvents(); + this.attachKeyEvents(); + return true; } - attachEvents() { + attachClickEvents() { + this.container.addEventListener('click', (e) => { + if (this.container === e.target) { + this.closeNav(); + } + }); this.menu.container.addEventListener('click', () => { this.openNav(); }); @@ -33,11 +33,34 @@ export default class Navigation { }); } + attachKeyEvents() { + const first = this.container.querySelector('.Nav-link'); + const last = this.container.querySelector('.Nav-close'); + this.container.addEventListener('keydown', (e) => { + if (e.keyCode === 27) { + this.closeNav(); + } + }); + first.addEventListener('keydown', (e) => { + if (e.shiftKey && e.keyCode === 9) { + e.preventDefault(); + last.focus(); + } + }); + last.addEventListener('keydown', (e) => { + if (!e.shiftKey && e.keyCode === 9) { + e.preventDefault(); + first.focus(); + } + }); + } + openNav() { this.state = 'visible'; this.menu.hideMenu(); this.container.classList.add(STATES.open); this.container.setAttribute('aria-hidden', false); + this.container.querySelector('.Nav-titleLink').focus(); } closeNav() { diff --git a/js/navigation.spec.js b/js/navigation.spec.js index 0bfdb21..7287cfd 100644 --- a/js/navigation.spec.js +++ b/js/navigation.spec.js @@ -1,9 +1,12 @@ import Navigation from './navigation'; -const navHtml = ` +Another test link`; let nav; @@ -43,11 +47,44 @@ describe('Navigation', () => { expect((new Navigation()).state).toBeUndefined(); }); + it('Should fail to initialise if the menu is not provided', () => { + const noMenu = new Navigation({ + container: document.querySelector('.Nav'), + }); + noMenu.init(); + expect(noMenu.state).toBeUndefined(); + }); + it('Should be hidden by default', () => { nav.init(); expect(nav.container.classList.contains('is-open')).toBe(false); }); + it('Should close when the .Nav element is clicked', () => { + nav.init(); + nav.openNav(); + const event = new Event('click'); + nav.container.dispatchEvent(event); + expect(nav.state).toBe('hidden'); + }); + + it('Should close when the user presses ESC', () => { + nav.init(); + nav.openNav(); + const event = new Event('keydown'); + event.keyCode = 27; + nav.container.dispatchEvent(event); + expect(nav.state).toBe('hidden'); + }); + + it('Should not close when the .Nav-content element is clicked', () => { + nav.init(); + nav.openNav(); + const event = new Event('click'); + nav.container.querySelector('.Nav-content').dispatchEvent(event); + expect(nav.state).toBe('visible'); + }); + it('Should have initial ARIA attributes', () => { nav.init(); expect(nav.container.getAttribute('aria-hidden')).toBe('true'); @@ -67,30 +104,55 @@ describe('Navigation', () => { }); }); -describe('Navigation list', () => { - it('Should have initial ARIA attributes', () => { +describe('Navigation menu button', () => { + it('Should open the Navigation when clicked', () => { nav.init(); - const ul = nav.container.querySelector('.Nav-list'); - expect(ul.getAttribute('role')).toBe('menubar'); + const event = new Event('click'); + nav.menu.container.dispatchEvent(event); + expect(nav.state).toBe('visible'); }); }); -describe('Navigation list items', () => { - it('Should have initial ARIA attributes', () => { +describe('Navigation close button', () => { + it('Should close the Navigation when clicked', () => { nav.init(); - const lis = nav.container.querySelectorAll('.Nav-listItem'); - lis.forEach((li) => { - expect(li.getAttribute('role')).toBe('none'); - }); + nav.openNav(); + const event = new Event('click'); + nav.container.querySelector('.Nav-close').dispatchEvent(event); + expect(nav.state).toBe('hidden'); }); }); -describe('Navigation links', () => { - it('Should have initial ARIA attributes', () => { +describe('Navigation focus behaviour', () => { + let first; + let last; + + beforeEach(() => { nav.init(); - const links = nav.container.querySelectorAll('.Nav-link'); - links.forEach((link) => { - expect(link.getAttribute('role')).toBe('menuitem'); - }); + nav.openNav(); + first = nav.container.querySelector('.Nav-link'); + last = nav.container.querySelector('.Nav-close'); + }); + + it('Should apply focus to .Nav-titleLink when opened', () => { + const link = nav.container.querySelector('.Nav-titleLink'); + expect(document.activeElement).toBe(link); + }); + + test('Focus moves to first element when TAB pressed on last', () => { + last.focus(); + const event = new Event('keydown'); + event.keyCode = 9; + last.dispatchEvent(event); + expect(document.activeElement).toBe(first); + }); + + test('Focus moves to last element when SHIFT + TAB pressed on first', () => { + first.focus(); + const event = new Event('keydown'); + event.shiftKey = true; + event.keyCode = 9; + first.dispatchEvent(event); + expect(document.activeElement).toBe(last); }); }); diff --git a/theme/functions.php b/theme/functions.php index 63ad676..8e1127d 100644 --- a/theme/functions.php +++ b/theme/functions.php @@ -5,8 +5,8 @@ class Windycoys_Walker extends Walker_Nav_Menu { $title = $item->post_title; $uri = get_site_url() . '/' . $item->post_name; - $output .= ''; } } diff --git a/theme/partials/navigation.php b/theme/partials/navigation.php index 7961fb4..ad658f3 100644 --- a/theme/partials/navigation.php +++ b/theme/partials/navigation.php @@ -6,18 +6,17 @@ Open menu