Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make use of hidden in header navigation functionality #2727

Merged
merged 3 commits into from
Aug 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@ We've deprecated the `govuk-header__navigation--no-service-name` class, and will

This change was introduced in [pull request #2694: Deprecate `.govuk-header__navigation--no-service-name`](https://github.com/alphagov/govuk-frontend/pull/2694).

### Recommended changes

We've recently made some non-breaking changes to GOV.UK Frontend. Implementing these changes will make your service work better.

#### Add `hidden` to the mobile menu button in the header component

If you're not using the Nunjucks macros, add the `hidden` attribute to the mobile menu button (button with class `govuk-header__menu-button`) in the header component.

We've changed the header's mobile menu functionality to use the `hidden` attribute over using CSS to show/hide the mobile menu. Adding `hidden` to the mobile menu button by default will ensure that it does not display for users when javascript doesn't load.

This change was introduced in [pull request 2727: Make use of hidden in header navigation functionality](https://github.com/alphagov/govuk-frontend/pull/2727)

### Fixes

In [pull request 2678: Replace ex units with ems for input lengths](https://github.com/alphagov/govuk-frontend/pull/2678), we changed how we define input lengths in our CSS. Browsers might now display these inputs as being slightly wider than before. The difference is usually fewer than 3 pixels.
Expand Down
33 changes: 12 additions & 21 deletions src/govuk/components/header/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -207,14 +207,21 @@
margin-left: govuk-spacing(1);
}

&[aria-expanded="true"]:after {
owenatgov marked this conversation as resolved.
Show resolved Hide resolved
@include govuk-shape-arrow($direction: up, $base: 10px, $display: inline-block);
}

@include govuk-media-query ($from: tablet) {
top: govuk-spacing(3);
}
}

.govuk-header__menu-button--open {
&:after {
@include govuk-shape-arrow($direction: up, $base: 10px, $display: inline-block);
.js-enabled & {
display: block;
}

&[hidden],
.js-enabled &[hidden] {
display: none;
}
}

Expand All @@ -229,25 +236,9 @@
margin: 0;
padding: 0;
list-style: none;
}

.js-enabled {
.govuk-header__menu-button {
display: block;
@include govuk-media-query ($from: desktop) {
display: none;
}
}

.govuk-header__navigation-list {
&[hidden] {
display: none;
@include govuk-media-query ($from: desktop) {
display: block;
}
}

.govuk-header__navigation-list--open {
display: block;
}
}

Expand Down
64 changes: 53 additions & 11 deletions src/govuk/components/header/header.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,76 @@ function Header ($module) {
this.$menu = this.$menuButton && $module.querySelector(
'#' + this.$menuButton.getAttribute('aria-controls')
)

// Save the opened/closed state for the nav in memory so that we can
// accurately maintain state when the screen is changed from small to
// big and back to small
this.menuIsOpen = false

// A global const for storing a matchMedia instance which we'll use to
// detect when a screen size change happens. We set this later during the
// init function and rely on it being null if the feature isn't available
// to initially apply hidden attributes
this.mql = null
}

/**
* 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.
* Feature sniff for and apply a matchMedia for desktop which will
* trigger a state sync if the browser viewport moves between states. If
* matchMedia isn't available, hide the menu button and present the "no js"
* version of the menu to the user.
*/
Header.prototype.init = function () {
if (!this.$module || !this.$menuButton || !this.$menu) {
return
}

this.syncState(this.$menu.classList.contains('govuk-header__navigation-list--open'))
this.$menuButton.addEventListener('click', this.handleMenuButtonClick.bind(this))
if ('matchMedia' in window) {
// Set the matchMedia to the govuk-frontend desktop breakpoint
this.mql = window.matchMedia('(min-width: 48.0625em)')

if ('addEventListener' in this.mql) {
this.mql.addEventListener('change', this.syncState.bind(this))
} else {
// addListener is a deprecated function, however addEventListener
// isn't supported by IE or Safari. We therefore add this in as
// a fallback for those browsers
this.mql.addListener(this.syncState.bind(this))
}

this.syncState()
this.$menuButton.addEventListener('click', this.handleMenuButtonClick.bind(this))
} else {
this.$menuButton.setAttribute('hidden', '')
}
}

/**
* 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
* Uses the global variable menuIsOpen to correctly set the accessible and
* visual states of the menu and the menu button.
* Additionally will force the menu to be visible and the menu button to be
* hidden if the matchMedia is triggered to desktop.
*/
Header.prototype.syncState = function (isVisible) {
this.$menuButton.classList.toggle('govuk-header__menu-button--open', isVisible)
this.$menuButton.setAttribute('aria-expanded', isVisible)
Header.prototype.syncState = function () {
owenatgov marked this conversation as resolved.
Show resolved Hide resolved
if (this.mql.matches) {
this.$menu.removeAttribute('hidden')
this.$menuButton.setAttribute('hidden', '')
} else {
this.$menuButton.removeAttribute('hidden')
this.$menuButton.setAttribute('aria-expanded', this.menuIsOpen)

if (this.menuIsOpen) {
this.$menu.removeAttribute('hidden')
} else {
this.$menu.setAttribute('hidden', '')
}
}
}

/**
Expand All @@ -45,8 +87,8 @@ Header.prototype.syncState = function (isVisible) {
* sync the accessibility state and menu button state
*/
Header.prototype.handleMenuButtonClick = function () {
var isVisible = this.$menu.classList.toggle('govuk-header__navigation-list--open')
this.syncState(isVisible)
this.menuIsOpen = !this.menuIsOpen
this.syncState()
}

export default Header
77 changes: 53 additions & 24 deletions src/govuk/components/header/header.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,19 @@ describe('Header navigation', () => {
})

it('shows the navigation', async () => {
await expect(page).toMatchElement('.govuk-header__navigation', {
visible: true,
timeout: 1000
})
const navDisplay = await page.$eval('.govuk-header__navigation-list',
el => window.getComputedStyle(el).getPropertyValue('display')
)

expect(navDisplay).toBe('block')
})

it('does not show the mobile menu button', async () => {
const buttonDisplay = await page.$eval('.govuk-js-header-toggle',
el => window.getComputedStyle(el).getPropertyValue('display')
)

expect(buttonDisplay).toBe('none')
})
})

Expand All @@ -49,6 +58,32 @@ describe('Header navigation', () => {
})
})

it('reveals the menu button', async () => {
const hidden = await page.$eval('.govuk-js-header-toggle',
el => el.hasAttribute('hidden')
)

const buttonDisplay = await page.$eval('.govuk-js-header-toggle',
el => window.getComputedStyle(el).getPropertyValue('display')
)

expect(hidden).toBe(false)
expect(buttonDisplay).toBe('block')
})

it('hides the menu via the hidden attribute', async () => {
const hidden = await page.$eval('.govuk-header__navigation-list',
el => el.hasAttribute('hidden')
)

const navDisplay = await page.$eval('.govuk-header__navigation-list',
el => window.getComputedStyle(el).getPropertyValue('display')
)

expect(hidden).toBe(true)
expect(navDisplay).toBe('none')
})

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')
Expand All @@ -66,20 +101,17 @@ describe('Header navigation', () => {
await page.click('.govuk-js-header-toggle')
})

it('adds the --open modifier class to the menu, making it visible', async () => {
const hasOpenClass = await page.$eval('.govuk-header__navigation-list',
el => el.classList.contains('govuk-header__navigation-list--open')
it('shows the menu', async () => {
const hidden = await page.$eval('.govuk-header__navigation-list',
el => el.hasAttribute('hidden')
)

expect(hasOpenClass).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')
const navDisplay = await page.$eval('.govuk-header__navigation-list',
el => window.getComputedStyle(el).getPropertyValue('display')
)

expect(hasOpenClass).toBeTruthy()
expect(hidden).toBe(false)
expect(navDisplay).toBe('block')
})

it('exposes the expanded state of the menu button using aria-expanded', async () => {
Expand All @@ -100,20 +132,17 @@ describe('Header navigation', () => {
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-list',
el => el.classList.contains('govuk-header__navigation-list--open')
it('adds the hidden attribute back to the menu, hiding it', async () => {
const hidden = await page.$eval('.govuk-header__navigation-list',
el => el.hasAttribute('hidden')
)

expect(hasOpenClass).toBeFalsy()
})

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')
const navDisplay = await page.$eval('.govuk-header__navigation-list',
el => window.getComputedStyle(el).getPropertyValue('display')
)

expect(hasOpenClass).toBeFalsy()
expect(hidden).toBe(true)
expect(navDisplay).toBe('none')
})

it('exposes the collapsed state of the menu button using aria-expanded', async () => {
Expand Down
2 changes: 1 addition & 1 deletion src/govuk/components/header/template.njk
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
{% endif %}
{% if params.navigation %}
<nav aria-label="{{ params.navigationLabel | default('Menu') }}" class="govuk-header__navigation {{ params.navigationClasses if params.navigationClasses }}">
<button type="button" class="govuk-header__menu-button govuk-js-header-toggle" aria-controls="navigation" aria-label="{{ params.menuButtonLabel | default('Show or hide menu') }}">Menu</button>
<button type="button" class="govuk-header__menu-button govuk-js-header-toggle" aria-controls="navigation" aria-label="{{ params.menuButtonLabel | default('Show or hide menu') }}" hidden>Menu</button>

<ul id="navigation" class="govuk-header__navigation-list">
{% for item in params.navigation %}
Expand Down
7 changes: 7 additions & 0 deletions src/govuk/components/header/template.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,13 @@ describe('header', () => {

expect($button.attr('type')).toEqual('button')
})
it('has a hidden attribute on load so that it does not show an unusable button without js', () => {
const $ = render('header', examples['with navigation'])

const $button = $('.govuk-header__menu-button')

expect($button.attr('hidden')).toBeTruthy()
})
it('renders default label correctly', () => {
const $ = render('header', examples['with navigation'])

Expand Down