From 37877d0d53c54a296a3b598f8ee6665664bcbd58 Mon Sep 17 00:00:00 2001 From: beeps Date: Thu, 8 Aug 2024 10:12:42 +0100 Subject: [PATCH] Add service navigation component --- .../govuk-prototype-kit.config.unit.test.mjs | 4 + packages/govuk-frontend/src/govuk/all.mjs | 1 + .../src/govuk/all.puppeteer.test.js | 1 + .../src/govuk/components/_index.scss | 1 + .../src/govuk/components/header/_index.scss | 8 + .../src/govuk/components/header/header.yaml | 6 + .../components/service-navigation/README.md | 15 + .../components/service-navigation/_index.scss | 160 +++++++ .../_service-navigation.scss | 2 + .../accessibility.puppeteer.test.mjs | 30 ++ .../components/service-navigation/macro.njk | 3 + .../service-navigation/service-navigation.mjs | 171 +++++++ .../service-navigation.puppeteer.test.js | 219 +++++++++ .../service-navigation.yaml | 289 +++++++++++ .../service-navigation/template.njk | 102 ++++ .../service-navigation/template.test.js | 451 ++++++++++++++++++ .../src/govuk/init.jsdom.test.mjs | 10 +- packages/govuk-frontend/src/govuk/init.mjs | 2 + .../tasks/build/package.unit.test.mjs | 1 + 19 files changed, 1475 insertions(+), 1 deletion(-) create mode 100644 packages/govuk-frontend/src/govuk/components/service-navigation/README.md create mode 100644 packages/govuk-frontend/src/govuk/components/service-navigation/_index.scss create mode 100644 packages/govuk-frontend/src/govuk/components/service-navigation/_service-navigation.scss create mode 100644 packages/govuk-frontend/src/govuk/components/service-navigation/accessibility.puppeteer.test.mjs create mode 100644 packages/govuk-frontend/src/govuk/components/service-navigation/macro.njk create mode 100644 packages/govuk-frontend/src/govuk/components/service-navigation/service-navigation.mjs create mode 100644 packages/govuk-frontend/src/govuk/components/service-navigation/service-navigation.puppeteer.test.js create mode 100644 packages/govuk-frontend/src/govuk/components/service-navigation/service-navigation.yaml create mode 100644 packages/govuk-frontend/src/govuk/components/service-navigation/template.njk create mode 100644 packages/govuk-frontend/src/govuk/components/service-navigation/template.test.js diff --git a/packages/govuk-frontend/src/govuk-prototype-kit/govuk-prototype-kit.config.unit.test.mjs b/packages/govuk-frontend/src/govuk-prototype-kit/govuk-prototype-kit.config.unit.test.mjs index 96af4b48d8..c4621343ce 100644 --- a/packages/govuk-frontend/src/govuk-prototype-kit/govuk-prototype-kit.config.unit.test.mjs +++ b/packages/govuk-frontend/src/govuk-prototype-kit/govuk-prototype-kit.config.unit.test.mjs @@ -152,6 +152,10 @@ describe('GOV.UK Prototype Kit config', () => { importFrom: 'govuk/components/select/macro.njk', macroName: 'govukSelect' }, + { + importFrom: 'govuk/components/service-navigation/macro.njk', + macroName: 'govukServiceNavigation' + }, { importFrom: 'govuk/components/skip-link/macro.njk', macroName: 'govukSkipLink' diff --git a/packages/govuk-frontend/src/govuk/all.mjs b/packages/govuk-frontend/src/govuk/all.mjs index c4eec04427..78fe080e39 100644 --- a/packages/govuk-frontend/src/govuk/all.mjs +++ b/packages/govuk-frontend/src/govuk/all.mjs @@ -9,6 +9,7 @@ export { Header } from './components/header/header.mjs' export { NotificationBanner } from './components/notification-banner/notification-banner.mjs' export { PasswordInput } from './components/password-input/password-input.mjs' export { Radios } from './components/radios/radios.mjs' +export { ServiceNavigation } from './components/service-navigation/service-navigation.mjs' export { SkipLink } from './components/skip-link/skip-link.mjs' export { Tabs } from './components/tabs/tabs.mjs' export { initAll, createAll } from './init.mjs' diff --git a/packages/govuk-frontend/src/govuk/all.puppeteer.test.js b/packages/govuk-frontend/src/govuk/all.puppeteer.test.js index 7b1bd680b4..9343b57932 100644 --- a/packages/govuk-frontend/src/govuk/all.puppeteer.test.js +++ b/packages/govuk-frontend/src/govuk/all.puppeteer.test.js @@ -60,6 +60,7 @@ describe('GOV.UK Frontend', () => { 'NotificationBanner', 'PasswordInput', 'Radios', + 'ServiceNavigation', 'SkipLink', 'Tabs' ]) diff --git a/packages/govuk-frontend/src/govuk/components/_index.scss b/packages/govuk-frontend/src/govuk/components/_index.scss index ae246e4950..c121a37bbc 100644 --- a/packages/govuk-frontend/src/govuk/components/_index.scss +++ b/packages/govuk-frontend/src/govuk/components/_index.scss @@ -27,6 +27,7 @@ @import "phase-banner/index"; @import "radios/index"; @import "select/index"; +@import "service-navigation/index"; @import "skip-link/index"; @import "summary-list/index"; @import "table/index"; diff --git a/packages/govuk-frontend/src/govuk/components/header/_index.scss b/packages/govuk-frontend/src/govuk/components/header/_index.scss index f9571058df..92e563e3f3 100644 --- a/packages/govuk-frontend/src/govuk/components/header/_index.scss +++ b/packages/govuk-frontend/src/govuk/components/header/_index.scss @@ -38,6 +38,14 @@ border-bottom: $govuk-header-border-width solid $govuk-header-border-color; } + .govuk-header--full-width-border { + border-bottom-color: $govuk-header-border-color; + + .govuk-header__container { + border-bottom-color: transparent; + } + } + .govuk-header__logotype { display: inline-block; position: relative; diff --git a/packages/govuk-frontend/src/govuk/components/header/header.yaml b/packages/govuk-frontend/src/govuk/components/header/header.yaml index 8d5d899fca..39d3c6e50e 100644 --- a/packages/govuk-frontend/src/govuk/components/header/header.yaml +++ b/packages/govuk-frontend/src/govuk/components/header/header.yaml @@ -247,6 +247,12 @@ examples: - href: '#3' text: Navigation item 3 + - name: with full width border + description: Makes the header's bottom border full width without affecting the header's content. + options: + classes: govuk-header--full-width-border + productName: Product Name + - name: navigation item with html options: serviceName: Service Name diff --git a/packages/govuk-frontend/src/govuk/components/service-navigation/README.md b/packages/govuk-frontend/src/govuk/components/service-navigation/README.md new file mode 100644 index 0000000000..820c862eec --- /dev/null +++ b/packages/govuk-frontend/src/govuk/components/service-navigation/README.md @@ -0,0 +1,15 @@ +# Service Navigation + +## Installation + +See the [main README quick start guide](https://github.com/alphagov/govuk-frontend#quick-start) for how to install this component. + +## Guidance and Examples + +Find out when to use the service header component in your service in the [GOV.UK Design System](https://design-system.service.gov.uk/components/service-navigation). + +## Component options + +Use options to customise the appearance, content and behaviour of a component when using a macro, for example, changing the text. + +See [options table](https://design-system.service.gov.uk/components/service-navigation/#options-service-navigation-example) for details. diff --git a/packages/govuk-frontend/src/govuk/components/service-navigation/_index.scss b/packages/govuk-frontend/src/govuk/components/service-navigation/_index.scss new file mode 100644 index 0000000000..0841fa3298 --- /dev/null +++ b/packages/govuk-frontend/src/govuk/components/service-navigation/_index.scss @@ -0,0 +1,160 @@ +@include govuk-exports("govuk/component/service-navigation") { + $govuk-service-navigation-active-link-border-width: govuk-spacing(1); + $govuk-service-navigation-background: $govuk-canvas-background-colour; + $govuk-service-navigation-border-colour: $govuk-border-colour; + + // We make the link colour a little darker than normal here so that it has + // better perceptual contrast with the navigation background. + $govuk-service-navigation-link-colour: govuk-shade($govuk-link-colour, 10%); + + .govuk-service-navigation { + border-bottom: 1px solid $govuk-service-navigation-border-colour; + background-color: $govuk-service-navigation-background; + } + + .govuk-service-navigation__container { + display: flex; + flex-direction: column; + align-items: start; + + @include govuk-media-query($from: tablet) { + flex-direction: row; + flex-wrap: wrap; + } + } + + // These styles are shared between nav items and the service name, they + // ensure that both of them remain vertically aligned with one another + .govuk-service-navigation__item, + .govuk-service-navigation__service-name { + position: relative; + margin: govuk-spacing(2) 0; + border: 0 solid $govuk-service-navigation-link-colour; + + @include govuk-media-query($from: tablet) { + margin-top: 0; + margin-bottom: 0; + padding: govuk-spacing(4) 0; + + &:not(:last-child) { + @include govuk-responsive-margin(6, $direction: right); + } + } + } + + .govuk-service-navigation__item--active { + @include govuk-media-query($until: tablet) { + // Negative offset the left margin so we can place a current page indicator + // to the left without misaligning the list item text. + margin-left: ((govuk-spacing(2) + $govuk-service-navigation-active-link-border-width) * -1); + padding-left: govuk-spacing(2); + border-left-width: $govuk-service-navigation-active-link-border-width; + } + + @include govuk-media-query($from: tablet) { + padding-bottom: govuk-spacing(4) - $govuk-service-navigation-active-link-border-width; + border-bottom-width: $govuk-service-navigation-active-link-border-width; + } + } + + .govuk-service-navigation__link { + @include govuk-link-common; + @include govuk-link-style-no-underline; + @include govuk-link-style-no-visited-state; + + &:not(:hover):not(:focus) { + // We set the colour here as we don't want to override the hover or + // focus colours + color: $govuk-service-navigation-link-colour; + } + } + + // + // Service name specific code + // + + .govuk-service-navigation__service-name { + @include govuk-font($size: 19, $weight: bold); + } + + // Annoyingly this requires a compound selector in order to overcome the + // specificity of the other link colour override we're doing + .govuk-service-navigation__service-name .govuk-service-navigation__link { + @include govuk-link-style-text; + } + + // + // Navigation list specific code + // + + .govuk-service-navigation__toggle { + @include govuk-font($size: 19, $weight: bold); + display: inline-flex; + margin: 0 0 govuk-spacing(2); + padding: 0; + border: 0; + color: $govuk-service-navigation-link-colour; + background: none; + word-break: break-all; + cursor: pointer; + align-items: center; + + &:focus { + @include govuk-focused-text; + } + + &::after { + @include govuk-shape-arrow($direction: down, $base: 10px, $display: inline-block); + content: ""; + margin-left: govuk-spacing(1); + } + + &[aria-expanded="true"]::after { + @include govuk-shape-arrow($direction: up, $base: 10px, $display: inline-block); + } + + // Ensure the button stays hidden if the hidden attribute is present + &[hidden] { + display: none; + } + } + + .govuk-service-navigation__list { + @include govuk-font($size: 19); + margin: 0; + margin-bottom: govuk-spacing(3); + padding: 0; + list-style: none; + + // Make the navigation list a flexbox. Doing so resolves a couple of + // accessibility problems caused by the list items being inline-blocks: + // - Removes the extra whitespace from between each list item that screen + // readers would pointlessly announce. + // - Fixes an NVDA issue in Firefox and Chrome <= 124 where it would read + // all of the links as a run-on sentence. + @include govuk-media-query($from: tablet) { + display: flex; + flex-wrap: wrap; + margin-bottom: 0; + + // However... IE11 totally trips over flexbox and doesn't wrap anything, + // making all of the items into a single, horizontally scrolling row, + // which is no good. This CSS hack removes the flexbox definition for + // IE 10 & 11, reverting it to the flawed, but OK, non-flexbox version. + // + // CSS hack taken from https://stackoverflow.com/questions/11173106/apply-style-only-on-ie#answer-36448860 + // which also includes an explanation of why this works + @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { + display: block; + } + } + } + + // This is a element that is used as a fallback mechanism for + // visually indicating the current page in scenarios where CSS isn't + // available. We don't actually want it to be bold normally, so set it to + // inherit the parent font-weight. + .govuk-service-navigation__active-fallback { + font-weight: inherit; + } +} diff --git a/packages/govuk-frontend/src/govuk/components/service-navigation/_service-navigation.scss b/packages/govuk-frontend/src/govuk/components/service-navigation/_service-navigation.scss new file mode 100644 index 0000000000..bfabb03440 --- /dev/null +++ b/packages/govuk-frontend/src/govuk/components/service-navigation/_service-navigation.scss @@ -0,0 +1,2 @@ +@import "../../base"; +@import "./index"; diff --git a/packages/govuk-frontend/src/govuk/components/service-navigation/accessibility.puppeteer.test.mjs b/packages/govuk-frontend/src/govuk/components/service-navigation/accessibility.puppeteer.test.mjs new file mode 100644 index 0000000000..0bcb7e9381 --- /dev/null +++ b/packages/govuk-frontend/src/govuk/components/service-navigation/accessibility.puppeteer.test.mjs @@ -0,0 +1,30 @@ +import { axe, render } from '@govuk-frontend/helpers/puppeteer' +import { getExamples } from '@govuk-frontend/lib/components' + +describe('/components/service-navigation', () => { + let axeRules + + beforeAll(() => { + axeRules = { + /** + * Ignore 'Element has insufficient color contrast' for WCAG Level AAA + */ + 'color-contrast-enhanced': { enabled: false } + } + }) + + describe('component examples', () => { + it('passes accessibility tests', async () => { + const examples = await getExamples('service-navigation') + + // Remove the 'with no options set' example from being tested, as the + // component doesn't output anything in that scenario. + delete examples['with no options set'] + + for (const exampleName in examples) { + await render(page, 'service-navigation', examples[exampleName]) + await expect(axe(page, axeRules)).resolves.toHaveNoViolations() + } + }, 120000) + }) +}) diff --git a/packages/govuk-frontend/src/govuk/components/service-navigation/macro.njk b/packages/govuk-frontend/src/govuk/components/service-navigation/macro.njk new file mode 100644 index 0000000000..67305e832a --- /dev/null +++ b/packages/govuk-frontend/src/govuk/components/service-navigation/macro.njk @@ -0,0 +1,3 @@ +{% macro govukServiceNavigation(params) %} + {%- include "./template.njk" -%} +{% endmacro %} diff --git a/packages/govuk-frontend/src/govuk/components/service-navigation/service-navigation.mjs b/packages/govuk-frontend/src/govuk/components/service-navigation/service-navigation.mjs new file mode 100644 index 0000000000..8c9bb9a2a5 --- /dev/null +++ b/packages/govuk-frontend/src/govuk/components/service-navigation/service-navigation.mjs @@ -0,0 +1,171 @@ +import { getBreakpoint } from '../../common/index.mjs' +import { ElementError } from '../../errors/index.mjs' +import { GOVUKFrontendComponent } from '../../govuk-frontend-component.mjs' + +/** + * Service Navigation component + * + * @preserve + */ +export class ServiceNavigation extends GOVUKFrontendComponent { + /** @private */ + $module + + /** @private */ + $menuButton + + /** @private */ + $menu + + /** + * Remember the open/closed state of the nav so we can maintain it when the + * screen is resized. + * + * @private + */ + menuIsOpen = false + + /** + * A global const for storing a matchMedia instance which we'll use to detect + * when a screen size change happens. We rely on it being null if the feature + * isn't available to initially apply hidden attributes + * + * @private + * @type {MediaQueryList | null} + */ + mql = null + + /** + * @param {Element | null} $module - HTML element to use for header + */ + constructor($module) { + super() + + if (!$module) { + throw new ElementError({ + componentName: 'Service Navigation', + element: $module, + identifier: 'Root element (`$module`)' + }) + } + + this.$module = $module + + const $menuButton = $module.querySelector( + '.govuk-js-service-navigation-toggle' + ) + + // Headers don't necessarily have a navigation. When they don't, the menu + // toggle won't be rendered by our macro (or may be omitted when writing + // plain HTML) + if (!$menuButton) { + return this + } + + const menuId = $menuButton.getAttribute('aria-controls') + if (!menuId) { + throw new ElementError({ + componentName: 'Service Navigation', + identifier: + 'Navigation button (`