diff --git a/CHANGELOG.md b/CHANGELOG.md index e64de9b80c..cf347a2430 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ For advice on how to use these release notes see [our guidance on staying up to ## Unreleased +### New features + +#### Make it easier to navigate complex services with the Service navigation component + +We've added a new [Service navigation component](https://design-system.service.gov.uk/components/service-navigation/) to help users to navigate services with multiple top-level sections. This replaces the navigation functions of the Header component, which will be deprecated in a future release of GOV.UK Frontend. + +This component includes some features we consider experimental. We intend to iterate these features in response to user feedback. These are: + +- moving the service name from the Header to the Service navigation +- providing slots for injecting custom HTML into specified locations within the component + +We introduced this change in [pull request #5206: Service navigation component](https://github.com/alphagov/govuk-frontend/pull/5206). + ## v5.5.0 (Feature release) To install this version with npm, run `npm install govuk-frontend@5.5.0`. You can also find more information about [how to stay up to date](https://frontend.design-system.service.gov.uk/staying-up-to-date/#updating-to-the-latest-version) in our documentation. 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..87e7f1f08f --- /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 navigation 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 (`