Skip to content

Commit

Permalink
Merge pull request #1942 from alphagov/header-aria-fixes
Browse files Browse the repository at this point in the history
Set aria-expanded and aria-hidden attributes on header menu button and menu when page loads
  • Loading branch information
36degrees committed Sep 7, 2020
2 parents 5672a08 + 4e77059 commit befc348
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 108 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ This was added in [pull request #1905: Set navigation and mobile menu labels of
We’ve made fixes to GOV.UK Frontend in the following pull requests:

- [#1943: Change header menu button label](https://github.com/alphagov/govuk-frontend/pull/1943)

- [#1942: Set aria-expanded and aria-hidden attributes on header menu button and menu when page loads](https://github.com/alphagov/govuk-frontend/pull/1942)

## 3.8.1 (Fix release)

Expand Down
72 changes: 34 additions & 38 deletions src/govuk/components/header/header.js
Original file line number Diff line number Diff line change
@@ -1,57 +1,53 @@
import '../../vendor/polyfills/Event'
import '../../vendor/polyfills/Element/prototype/classList'
import '../../vendor/polyfills/Function/prototype/bind'
import '../../vendor/polyfills/Event' // addEventListener and event.target normaliziation

function Header ($module) {
this.$module = $module
this.$menuButton = $module && $module.querySelector('.govuk-js-header-toggle')
this.$menu = this.$menuButton && $module.querySelector(
'#' + this.$menuButton.getAttribute('aria-controls')
)
}

/**
* Initialise header
*
* Check for the presence of the header, menu and menu button – if any are
* missing then there's nothing to do so return early.
*/
Header.prototype.init = function () {
// Check for module
var $module = this.$module
if (!$module) {
return
}

// Check for button
var $toggleButton = $module.querySelector('.govuk-js-header-toggle')
if (!$toggleButton) {
if (!this.$module || !this.$menuButton || !this.$menu) {
return
}

// Handle $toggleButton click events
$toggleButton.addEventListener('click', this.handleClick.bind(this))
this.syncState(this.$menu.classList.contains('govuk-header__navigation--open'))
this.$menuButton.addEventListener('click', this.handleMenuButtonClick.bind(this))
}

/**
* Toggle class
* @param {object} node element
* @param {string} className to toggle
*/
Header.prototype.toggleClass = function (node, className) {
if (node.className.indexOf(className) > 0) {
node.className = node.className.replace(' ' + className, '')
} else {
node.className += ' ' + className
}
* Sync menu state
*
* Sync the menu button class and the accessible state of the menu and the menu
* button with the visible state of the menu
*
* @param {boolean} isVisible Whether the menu is currently visible
*/
Header.prototype.syncState = function (isVisible) {
this.$menuButton.classList.toggle('govuk-header__menu-button--open', isVisible)
this.$menuButton.setAttribute('aria-expanded', isVisible)
this.$menu.setAttribute('aria-hidden', !isVisible)
}

/**
* An event handler for click event on $toggleButton
* @param {object} event event
*/
Header.prototype.handleClick = function (event) {
var $module = this.$module
var $toggleButton = event.target || event.srcElement
var $target = $module.querySelector('#' + $toggleButton.getAttribute('aria-controls'))

// If a button with aria-controls, handle click
if ($toggleButton && $target) {
this.toggleClass($target, 'govuk-header__navigation--open')
this.toggleClass($toggleButton, 'govuk-header__menu-button--open')

$toggleButton.setAttribute('aria-expanded', $toggleButton.getAttribute('aria-expanded') !== 'true')
$target.setAttribute('aria-hidden', $target.getAttribute('aria-hidden') === 'false')
}
* Handle menu button click
*
* When the menu button is clicked, change the visibility of the menu and then
* sync the accessibility state and menu button state
*/
Header.prototype.handleMenuButtonClick = function () {
var isVisible = this.$menu.classList.toggle('govuk-header__navigation--open')
this.syncState(isVisible)
}

export default Header
176 changes: 107 additions & 69 deletions src/govuk/components/header/header.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,108 +7,146 @@ const PORT = configPaths.ports.test

const baseUrl = 'http://localhost:' + PORT

beforeAll(async (done) => {
await page.emulate(iPhone)
done()
})
describe('Header navigation', () => {
beforeAll(async (done) => {
await page.emulate(iPhone)
done()
})

describe('/components/header', () => {
describe('/components/header/with-navigation/preview', () => {
describe('when JavaScript is unavailable or fails', () => {
beforeAll(async () => {
await page.setJavaScriptEnabled(false)
describe('when JavaScript is unavailable or fails', () => {
beforeAll(async () => {
await page.setJavaScriptEnabled(false)
await page.goto(`${baseUrl}/components/header/with-navigation/preview`, {
waitUntil: 'load'
})
})

afterAll(async () => {
await page.setJavaScriptEnabled(true)
afterAll(async () => {
await page.setJavaScriptEnabled(true)
})

it('shows the navigation', async () => {
await expect(page).toMatchElement('.govuk-header__navigation', {
visible: true,
timeout: 1000
})
})
})

it('falls back to making the navigation visible', async () => {
await page.goto(baseUrl + '/components/header/with-navigation/preview', { waitUntil: 'load' })
const isContentVisible = await page.waitForSelector('.govuk-header__navigation', { visible: true, timeout: 1000 })
expect(isContentVisible).toBeTruthy()
describe('when JavaScript is available', () => {
describe('when no navigation is present', () => {
it('exits gracefully with no errors', async () => {
// Errors logged to the console will cause this test to fail
await page.goto(`${baseUrl}/components/header/preview`, {
waitUntil: 'load'
})
})
})

describe('when JavaScript is available', () => {
describe('when menu button is pressed', () => {
it('should indicate the open state of the toggle button', async () => {
await page.goto(baseUrl + '/components/header/with-navigation/preview', { waitUntil: 'load' })
describe('on page load', () => {
beforeAll(async () => {
await page.goto(`${baseUrl}/components/header/with-navigation/preview`, {
waitUntil: 'load'
})
})

await page.click('.govuk-js-header-toggle')
it('exposes the hidden state of the menu using aria-hidden', async () => {
const ariaHidden = await page.$eval('.govuk-header__navigation',
el => el.getAttribute('aria-hidden')
)

const toggleButtonIsOpen = await page.evaluate(() => document.body.querySelector('.govuk-header__menu-button').classList.contains('govuk-header__menu-button--open'))
expect(toggleButtonIsOpen).toBeTruthy()
})
expect(ariaHidden).toBe('true')
})

it('should indicate the expanded state of the toggle button using aria-expanded', async () => {
await page.goto(baseUrl + '/components/header/with-navigation/preview', { waitUntil: 'load' })
it('exposes the collapsed state of the menu button using aria-expanded', async () => {
const ariaExpanded = await page.$eval('.govuk-header__menu-button',
el => el.getAttribute('aria-expanded')
)

await page.click('.govuk-js-header-toggle')
expect(ariaExpanded).toBe('false')
})
})

const toggleButtonAriaExpanded = await page.evaluate(() => document.body.querySelector('.govuk-header__menu-button').getAttribute('aria-expanded'))
expect(toggleButtonAriaExpanded).toBe('true')
describe('when menu button is pressed', () => {
beforeAll(async () => {
await page.goto(`${baseUrl}/components/header/with-navigation/preview`, {
waitUntil: 'load'
})
await page.click('.govuk-js-header-toggle')
})

it('should indicate the open state of the navigation', async () => {
await page.goto(baseUrl + '/components/header/with-navigation/preview', { waitUntil: 'load' })
it('adds the --open modifier class to the menu, making it visible', async () => {
const hasOpenClass = await page.$eval('.govuk-header__navigation',
el => el.classList.contains('govuk-header__navigation--open')
)

await page.click('.govuk-js-header-toggle')
expect(hasOpenClass).toBeTruthy()
})

const navigationIsOpen = await page.evaluate(() => document.body.querySelector('.govuk-header__navigation').classList.contains('govuk-header__navigation--open'))
expect(navigationIsOpen).toBeTruthy()
})
it('adds the --open modifier class to the menu button', async () => {
const hasOpenClass = await page.$eval('.govuk-header__menu-button',
el => el.classList.contains('govuk-header__menu-button--open')
)

it('should indicate the visible state of the navigation using aria-hidden', async () => {
await page.goto(baseUrl + '/components/header/with-navigation/preview', { waitUntil: 'load' })
expect(hasOpenClass).toBeTruthy()
})

await page.click('.govuk-js-header-toggle')
it('exposes the visible state of the menu using aria-hidden', async () => {
const ariaHidden = await page.$eval('.govuk-header__navigation',
el => el.getAttribute('aria-hidden')
)

const navigationAriaHidden = await page.evaluate(() => document.body.querySelector('.govuk-header__navigation').getAttribute('aria-hidden'))
expect(navigationAriaHidden).toBe('false')
})
expect(ariaHidden).toBe('false')
})

describe('when menu button is pressed twice', () => {
it('should indicate the open state of the toggle button', async () => {
await page.goto(baseUrl + '/components/header/with-navigation/preview', { waitUntil: 'load' })
it('exposes the expanded state of the menu button using aria-expanded', async () => {
const ariaExpanded = await page.$eval('.govuk-header__menu-button',
el => el.getAttribute('aria-expanded')
)

await page.click('.govuk-js-header-toggle')
await page.click('.govuk-js-header-toggle')
expect(ariaExpanded).toBe('true')
})
})

const toggleButtonIsOpen = await page.evaluate(() => document.body.querySelector('.govuk-header__menu-button').classList.contains('govuk-header__menu-button--open'))
expect(toggleButtonIsOpen).toBeFalsy()
describe('when menu button is pressed twice', () => {
beforeAll(async () => {
await page.goto(`${baseUrl}/components/header/with-navigation/preview`, {
waitUntil: 'load'
})
await page.click('.govuk-js-header-toggle')
await page.click('.govuk-js-header-toggle')
})

it('should indicate the expanded state of the toggle button using aria-expanded', async () => {
await page.goto(baseUrl + '/components/header/with-navigation/preview', { waitUntil: 'load' })

await page.click('.govuk-js-header-toggle')
await page.click('.govuk-js-header-toggle')
it('removes the --open modifier class from the menu, hiding it', async () => {
const hasOpenClass = await page.$eval('.govuk-header__navigation',
el => el.classList.contains('govuk-header__navigation--open')
)

const toggleButtonAriaExpanded = await page.evaluate(() => document.body.querySelector('.govuk-header__menu-button').getAttribute('aria-expanded'))
expect(toggleButtonAriaExpanded).toBe('false')
})
expect(hasOpenClass).toBeFalsy()
})

it('should indicate the open state of the navigation', async () => {
await page.goto(baseUrl + '/components/header/with-navigation/preview', { waitUntil: 'load' })
it('removes the --open modifier class from the menu button', async () => {
const hasOpenClass = await page.$eval('.govuk-header__menu-button',
el => el.classList.contains('govuk-header__menu-button--open')
)

await page.click('.govuk-js-header-toggle')
await page.click('.govuk-js-header-toggle')
expect(hasOpenClass).toBeFalsy()
})

const navigationIsOpen = await page.evaluate(() => document.body.querySelector('.govuk-header__navigation').classList.contains('govuk-header__navigation--open'))
expect(navigationIsOpen).toBeFalsy()
})
it('exposes the hidden state of the menu using aria-hidden', async () => {
const ariaHidden = await page.$eval('.govuk-header__navigation',
el => el.getAttribute('aria-hidden')
)

it('should indicate the visible state of the navigation using aria-hidden', async () => {
await page.goto(baseUrl + '/components/header/with-navigation/preview', { waitUntil: 'load' })
expect(ariaHidden).toBe('true')
})

await page.click('.govuk-js-header-toggle')
await page.click('.govuk-js-header-toggle')
it('exposes the collapsed state of the menu button using aria-expanded', async () => {
const ariaExpanded = await page.$eval('.govuk-header__menu-button',
el => el.getAttribute('aria-expanded')
)

const navigationAriaHidden = await page.evaluate(() => document.body.querySelector('.govuk-header__navigation').getAttribute('aria-hidden'))
expect(navigationAriaHidden).toBe('true')
})
expect(ariaExpanded).toBe('false')
})
})
})
Expand Down

0 comments on commit befc348

Please sign in to comment.