diff --git a/CHANGELOG.md b/CHANGELOG.md
index 720c769a7d..9261022e3d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -79,6 +79,14 @@ We’ve made fixes to GOV.UK Frontend in the following pull requests:
- [#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)
- [#1947 Add print styles for the panel component](https://github.com/alphagov/govuk-frontend/pull/1947)
+### Fixes
+
+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)
+- [#1947 Add print styles for the panel component](https://github.com/alphagov/govuk-frontend/pull/1947)
+
## 3.8.1 (Fix release)
### Fixes
diff --git a/src/govuk/components/header/header.js b/src/govuk/components/header/header.js
index 85caddb7d9..84eb54f8d3 100644
--- a/src/govuk/components/header/header.js
+++ b/src/govuk/components/header/header.js
@@ -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
diff --git a/src/govuk/components/header/header.test.js b/src/govuk/components/header/header.test.js
index c99dc58f34..a8a7544c4e 100644
--- a/src/govuk/components/header/header.test.js
+++ b/src/govuk/components/header/header.test.js
@@ -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')
})
})
})
diff --git a/src/govuk/components/header/header.yaml b/src/govuk/components/header/header.yaml
index 4cb7c2c87e..c2088c17d5 100644
--- a/src/govuk/components/header/header.yaml
+++ b/src/govuk/components/header/header.yaml
@@ -35,7 +35,7 @@ params:
- name: href
type: string
required: false
- description: Url of the navigation item anchor. Both `href` and `text` attributes for navigation items need to be provided to create an item.
+ description: Url of the navigation item anchor.
- name: active
type: boolean
required: false
@@ -51,11 +51,11 @@ params:
- name: navigationLabel
type: string
required: false
- description: Text for the `aria-label` attribute of the navigation. Defaults to "Top Level Navigation".
+ description: Text for the `aria-label` attribute of the navigation. Defaults to "Navigation menu".
- name: menuButtonLabel
type: string
required: false
- description: Text for the `aria-label` attribute of the button that toggles the navigation. Defaults to "Show or hide Top Level Navigation".
+ description: Text for the `aria-label` attribute of the button that toggles the navigation. Defaults to "Show or hide navigation menu".
- name: containerClasses
type: string
required: false
@@ -233,6 +233,15 @@ examples:
- href: '#3'
html: Navigation item 3
+- name: navigation item with text without link
+ data:
+ serviceName: Service Name
+ serviceUrl: '/components/header'
+ navigation:
+ - text: Navigation item 1
+ - text: Navigation item 2
+ - text: Navigation item 3
+
# Hidden examples are not shown in the review app, but are used for tests and HTML fixtures
- name: attributes
hidden: true
@@ -257,3 +266,26 @@ examples:
attributes:
data-attribute: my-attribute
data-attribute-2: my-attribute-2
+- name: navigation item with html as text
+ hidden: true
+ data:
+ serviceName: Service Name
+ serviceUrl: '/components/header'
+ navigation:
+ - href: '#1'
+ text: Navigation item 1
+ active: true
+ - href: '#2'
+ text: Navigation item 2
+ - href: '#3'
+ text: Navigation item 3
+- name: navigation item with html without link
+ hidden: true
+ data:
+ serviceName: Service Name
+ serviceUrl: '/components/header'
+ navigation:
+ - html: Navigation item 1
+ active: true
+ - html: Navigation item 2
+ - html: Navigation item 3
diff --git a/src/govuk/components/header/template.njk b/src/govuk/components/header/template.njk
index 0b31f33948..058a0d5207 100644
--- a/src/govuk/components/header/template.njk
+++ b/src/govuk/components/header/template.njk
@@ -60,15 +60,19 @@
{% endif %}
{% if params.navigation %}
-
+