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 %} - +