-
Notifications
You must be signed in to change notification settings - Fork 247
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement accordion functionality for ServiceNav
- New partial for building HTML of sub-navigation items - Accordion functionality for Service Navigation component when page viewed on mobile viewport - Update configuration for building list of links for navigation and sub navigation items
- Loading branch information
1 parent
df0f3dc
commit 4edcc0c
Showing
9 changed files
with
445 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
const { KnownDevices } = require('puppeteer') | ||
|
||
const { goTo, getProperty, getAttribute } = require('./helpers/puppeteer.js') | ||
|
||
describe('Homepage', () => { | ||
let $navigationToggler | ||
|
||
async function setup(page) { | ||
$navigationToggler = await page.$('.govuk-js-service-navigation-toggle') | ||
} | ||
|
||
beforeAll(async () => { | ||
await page.emulate(KnownDevices['iPhone 6']) | ||
}) | ||
|
||
beforeEach(async () => { | ||
await page.setJavaScriptEnabled(true) | ||
await goTo(page, '/') | ||
await setup(page) | ||
}) | ||
|
||
describe('when JavaScript is unavailable or fails', () => { | ||
it('the mobile subnav will not render', async () => { | ||
await page.setJavaScriptEnabled(false) | ||
|
||
// Reload page again | ||
await page.reload() | ||
|
||
// Menu open state (visually) | ||
await expect( | ||
page.$$('.govuk-service-navigation__link .app-mobile-navigation__list') | ||
).resolves.toStrictEqual([]) | ||
}) | ||
}) | ||
|
||
describe('when JavaScript is available', () => { | ||
it('the mobile subnav will render', async () => { | ||
// Menu open state (visually) | ||
await expect( | ||
page.$$('.govuk-service-navigation__link template') | ||
).resolves.toStrictEqual([]) | ||
}) | ||
|
||
it('the mobile subnav will toggle hidden when open or closed', async () => { | ||
const $mobileNavButton = await page.$( | ||
'.govuk-service-navigation__link.app-mobile-navigation__toggle-button' | ||
) | ||
const $mobileNavMenu = await page.$('.app-mobile-navigation__list') | ||
|
||
// default state on homepage is hidden | ||
await expect(getProperty($mobileNavMenu, 'hidden')).resolves.toBeTruthy() | ||
|
||
await $navigationToggler.click() | ||
await $mobileNavButton.click() | ||
|
||
// default state on homepage is hidden | ||
await expect(getProperty($mobileNavMenu, 'hidden')).resolves.toBeFalsy() | ||
}) | ||
|
||
it('the mobile subnav will toggle aria-expanded when open or closed', async () => { | ||
const $mobileNavButton = await page.$( | ||
'.govuk-service-navigation__link.app-mobile-navigation__toggle-button' | ||
) | ||
|
||
// default state on homepage is hidden | ||
await expect( | ||
getAttribute($mobileNavButton, 'aria-expanded') | ||
).resolves.toBe('false') | ||
|
||
await $navigationToggler.click() | ||
await $mobileNavButton.click() | ||
|
||
// default state on homepage is hidden | ||
await expect( | ||
getAttribute($mobileNavButton, 'aria-expanded') | ||
).resolves.toBe('true') | ||
}) | ||
|
||
it('applies current link style and expands subnav if user on page in subnav', async () => { | ||
await goTo(page, '/get-started') | ||
await setup(page) | ||
|
||
await expect( | ||
page.$( | ||
'[aria-expanded="true"].app-mobile-navigation__toggle-button + ul [href="/get-started"].app-mobile-navigation__link--active' | ||
) | ||
).resolves.not.toBeNull() | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
import { Component } from 'govuk-frontend' | ||
|
||
/** | ||
* Mobile Navigation enhancement for Service Navigation component | ||
*/ | ||
class MobileNavigation extends Component { | ||
static moduleName = 'app-mobile-navigation' | ||
|
||
/** | ||
* @param {Element} $root - HTML element | ||
*/ | ||
constructor($root) { | ||
super($root) | ||
|
||
this.templates = this.$root.querySelectorAll( | ||
'.app-mobile-navigation__template' | ||
) | ||
this.links = this.$root.querySelectorAll('.govuk-service-navigation__link') | ||
|
||
Array.from(this.templates).forEach((template) => { | ||
const templateClone = template.content.cloneNode(true) | ||
let link | ||
|
||
if (template.parentNode.tagName === 'A') { | ||
link = template.parentNode | ||
link.removeChild(template) | ||
} else { | ||
link = template.parentNode.parentNode | ||
template.parentNode.removeChild(template) | ||
} | ||
|
||
// a is not a valid descendent of a, even within a template | ||
// can't change this about the ServiceNavigation component | ||
// so we need to have placeholder links in the template | ||
// which we then replace | ||
templateClone | ||
.querySelectorAll('.app-mobile-navigation__link') | ||
.forEach((linkPlaceholder) => { | ||
const link = document.createElement('a') | ||
link.className = linkPlaceholder.className | ||
link.href = linkPlaceholder.dataset.href | ||
link.innerHTML = linkPlaceholder.innerHTML | ||
linkPlaceholder.replaceWith(link) | ||
}) | ||
|
||
const button = document.createElement('button') | ||
button.classList.add('govuk-service-navigation__link') | ||
button.classList.add('app-mobile-navigation__toggle-button') | ||
button.textContent = link.textContent | ||
button.setAttribute('aria-expanded', 'false') | ||
|
||
link.insertAdjacentElement('afterend', templateClone.firstElementChild) | ||
link.insertAdjacentElement('afterend', button) | ||
|
||
if ( | ||
link.parentNode.classList.contains( | ||
'govuk-service-navigation__item--active' | ||
) | ||
) { | ||
this.toggleSubnav(button) | ||
} | ||
}) | ||
|
||
const currentLink = this.$root.querySelector( | ||
`.app-mobile-navigation__link[href^="${window.location.pathname.slice(0, -1)}"]` | ||
) | ||
|
||
if (currentLink) { | ||
currentLink.classList.add('app-mobile-navigation__link--active') | ||
} | ||
|
||
this.subNavs = $root.querySelectorAll('.app-mobile-navigation__list') | ||
|
||
// A global const for storing a matchMedia instance which we'll use to detect when a screen size change happens | ||
// Set the matchMedia to the govuk-frontend tablet breakpoint | ||
|
||
const breakPoint = getComputedStyle( | ||
document.documentElement | ||
).getPropertyValue('--govuk-frontend-breakpoint-tablet') | ||
|
||
this.mql = window.matchMedia(`(min-width: ${breakPoint})`) | ||
|
||
// MediaQueryList.addEventListener isn't supported by Safari < 14 so we need | ||
// to be able to fall back to the deprecated MediaQueryList.addListener | ||
if ('addEventListener' in this.mql) { | ||
this.mql.addEventListener('change', () => this.setHiddenStates()) | ||
} else { | ||
// @ts-expect-error Property 'addListener' does not exist | ||
this.mql.addListener(() => this.setHiddenStates()) | ||
} | ||
|
||
this.setHiddenStates() | ||
this.setEventListener() | ||
} | ||
|
||
/** | ||
* Toggle subnav menu open or closed | ||
* | ||
* @param {HTMLButtonElement} button - button of subnav to toggle | ||
*/ | ||
toggleSubnav(button) { | ||
const ariaExpanded = button.getAttribute('aria-expanded') === 'true' | ||
|
||
button.setAttribute('aria-expanded', `${!ariaExpanded}`) | ||
|
||
const subNav = button.nextElementSibling | ||
|
||
if ( | ||
subNav && | ||
subNav.classList && | ||
subNav.classList.contains('app-mobile-navigation__list') | ||
) { | ||
if (ariaExpanded) { | ||
subNav.setAttribute('hidden', '') | ||
} else { | ||
subNav.removeAttribute('hidden') | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Set up event delegation for button clicks | ||
*/ | ||
setEventListener() { | ||
this.$root.addEventListener('click', (e) => { | ||
if (e.target.tagName === 'BUTTON') { | ||
this.toggleSubnav(e.target) | ||
} | ||
}) | ||
} | ||
|
||
/** | ||
* Hide links if viewport is below tablet | ||
*/ | ||
setHiddenStates() { | ||
if (!this.mql.matches) { | ||
this.links.forEach((a) => a.setAttribute('hidden', '')) | ||
this.subNavs.forEach((subNav) => { | ||
if ( | ||
subNav.previousElementSibling.getAttribute('aria-expanded') === 'true' | ||
) { | ||
subNav.removeAttribute('hidden') | ||
} | ||
}) | ||
} else { | ||
this.subNavs.forEach((subNav) => subNav.setAttribute('hidden', '')) | ||
this.links.forEach((link) => link.removeAttribute('hidden')) | ||
} | ||
} | ||
} | ||
|
||
export default MobileNavigation |
Oops, something went wrong.