diff --git a/config/navigation.js b/config/navigation.js index 2ee18bcf98..a6e50247e5 100644 --- a/config/navigation.js +++ b/config/navigation.js @@ -5,28 +5,28 @@ */ const config = [ { - label: 'Get started', - url: 'get-started' + text: 'Get started', + href: 'get-started' }, { - label: 'Styles', - url: 'styles' + text: 'Styles', + href: 'styles' }, { - label: 'Components', - url: 'components' + text: 'Components', + href: 'components' }, { - label: 'Patterns', - url: 'patterns' + text: 'Patterns', + href: 'patterns' }, { - label: 'Community', - url: 'community' + text: 'Community', + href: 'community' }, { - label: 'Accessibility', - url: 'accessibility' + text: 'Accessibility', + href: 'accessibility' } ] diff --git a/lib/navigation.js b/lib/navigation.js index d4bbe864d6..334ea2ffe5 100644 --- a/lib/navigation.js +++ b/lib/navigation.js @@ -23,7 +23,7 @@ module.exports = (config) => (files, metalsmith, done) => { for (const item of items) { // Match navigation item child directories // (for example, ['components/breadcrumbs/index.html', 'components/checkboxes/index.html', ...]) - const itemPaths = metalsmith.match(`${item.url}/*/index.html`, paths) + const itemPaths = metalsmith.match(`${item.href}/*/index.html`, paths) // No sub items required for this path if (!itemPaths.length) { @@ -47,8 +47,8 @@ module.exports = (config) => (files, metalsmith, done) => { // Add subitem to navigation item.items.push({ - url: dirname(itemPath), - label: frontmatter.title, + href: `/${dirname(itemPath)}`, + text: frontmatter.title, order: frontmatter.order, theme: frontmatter.theme, @@ -65,6 +65,8 @@ module.exports = (config) => (files, metalsmith, done) => { // Sort navigation sub items using 'order' (optional) item.items?.sort((a, b) => compare(a.order, b.order)) + + item.href = `/${item.href}` } // Add navigation to global variables diff --git a/src/javascripts/application.mjs b/src/javascripts/application.mjs index 0039759f75..758d2e5e49 100644 --- a/src/javascripts/application.mjs +++ b/src/javascripts/application.mjs @@ -1,6 +1,12 @@ /* eslint-disable no-new */ -import { createAll, Button, NotificationBanner, SkipLink } from 'govuk-frontend' +import { + createAll, + Button, + NotificationBanner, + ServiceNavigation, + SkipLink +} from 'govuk-frontend' import { loadAnalytics } from './components/analytics.mjs' import BackToTop from './components/back-to-top.mjs' @@ -14,7 +20,7 @@ import CookiesPage from './components/cookies-page.mjs' import Copy from './components/copy.mjs' import EmbedCard from './components/embed-card.mjs' import ExampleFrame from './components/example-frame.mjs' -import Navigation from './components/navigation.mjs' +import MobileNavigation from './components/mobile-navigation.mjs' import OptionsTable from './components/options-table.mjs' import ScrollContainer from './components/scroll-container.mjs' import Search from './components/search.mjs' @@ -23,6 +29,7 @@ import AppTabs from './components/tabs.mjs' // Initialise GOV.UK Frontend createAll(Button) createAll(NotificationBanner) +createAll(ServiceNavigation) createAll(SkipLink) // Cookies and analytics @@ -47,8 +54,8 @@ createAll(AppTabs) createAll(Copy) new OptionsTable() -// Initialise mobile navigation -createAll(Navigation) +// Initialise mobile navigation (again) +createAll(MobileNavigation) // Initialise scrollable container handling createAll(ScrollContainer) diff --git a/src/javascripts/components/mobile-navigation.mjs b/src/javascripts/components/mobile-navigation.mjs new file mode 100644 index 0000000000..1958ee37b5 --- /dev/null +++ b/src/javascripts/components/mobile-navigation.mjs @@ -0,0 +1,128 @@ +import { Component } from 'govuk-frontend' + +/** + * Mobile Navigation enhancement for Service Navigation component + */ +class MobileNavigation extends Component { + static moduleName = 'app-mobile-navigation' + + /** + * @param {Element} $root - HTML element + */ + constructor($root) { + super($root) + + this.templates = this.$root.querySelectorAll( + '.app-mobile-navigation__template' + ) + this.links = this.$root.querySelectorAll('a') + + Array.from(this.templates).forEach((template) => { + const templateClone = template.content.cloneNode(true) + let link + + if (template.parentNode.tagName === 'A') { + link = template.parentNode + link.removeChild(template) + } else { + link = template.parentNode.parentNode + template.parentNode.removeChild(template) + } + + const button = document.createElement('button') + button.classList.add('govuk-service-navigation__link') + button.classList.add('app-mobile-navigation__toggle-button') + button.setAttribute( + 'aria-expanded', + String( + link.parentNode.classList.contains( + 'govuk-service-navigation__item--active' + ) + ) + ) + button.textContent = link.textContent + + link.insertAdjacentElement('afterend', templateClone.firstElementChild) + link.insertAdjacentElement('afterend', button) + }) + + const currentLink = Array.from( + this.$root.querySelectorAll( + `.app-navigation__subnav-item a[href="${window.location.pathname.slice(0, -1)}"]` + ) + ).pop() + + if (currentLink) { + currentLink.classList.add('app-mobile-navigation__link--active') + } + + this.subNavs = $root.querySelectorAll('.app-mobile-navigation__list') + + // A global const for storing a matchMedia instance which we'll use to detect when a screen size change happens + // Set the matchMedia to the govuk-frontend tablet breakpoint + + const breakPoint = getComputedStyle( + document.documentElement + ).getPropertyValue('--govuk-frontend-breakpoint-tablet') + + this.mql = window.matchMedia(`(min-width: ${breakPoint})`) + + // MediaQueryList.addEventListener isn't supported by Safari < 14 so we need + // to be able to fall back to the deprecated MediaQueryList.addListener + if ('addEventListener' in this.mql) { + this.mql.addEventListener('change', () => this.setHiddenStates()) + } else { + // @ts-expect-error Property 'addListener' does not exist + this.mql.addListener(() => this.setHiddenStates()) + } + + this.setHiddenStates() + this.setEventListener() + } + + /** + * Set up event delegation for button clicks + */ + setEventListener() { + this.$root.addEventListener('click', (e) => { + if (e.target.tagName === 'BUTTON') { + const ariaExpanded = e.target.getAttribute('aria-expanded') === 'true' + + e.target.setAttribute('aria-expanded', !ariaExpanded) + + const subNav = e.target.nextSibling + + if ( + subNav && + subNav.classList && + subNav.classList.contains('app-mobile-navigation__list') + ) { + if (ariaExpanded) { + subNav.setAttribute('hidden', '') + } else { + subNav.removeAttribute('hidden') + } + } + } + }) + } + + /** + * Hide links if viewport is below tablet + */ + setHiddenStates() { + if (!this.mql.matches) { + this.links.forEach((a) => a.setAttribute('hidden', '')) + this.subNavs.forEach((subNav) => { + if (subNav.previousSibling.getAttribute('aria-expanded') === 'true') { + subNav.removeAttribute('hidden') + } + }) + } else { + this.subNavs.forEach((x) => x.setAttribute('hidden', '')) + this.links.forEach((a) => a.removeAttribute('hidden')) + } + } +} + +export default MobileNavigation diff --git a/src/stylesheets/components/_mobile-navigation.scss b/src/stylesheets/components/_mobile-navigation.scss new file mode 100644 index 0000000000..ecd9b04ad0 --- /dev/null +++ b/src/stylesheets/components/_mobile-navigation.scss @@ -0,0 +1,101 @@ +$navigation-height: 50px; + +.app-mobile-navigation__theme { + @include govuk-typography-common; + position: relative; // this is to get around the artificial click area generated by the :after of the parent button + margin: 0; + padding: 0; + color: govuk-colour("dark-grey"); + // Font is defined as a hard 19px so + // it does not re-size on mobile viewport + font-size: 19px; + font-size: govuk-px-to-rem(19px); + font-weight: normal; +} + +.app-mobile-navigation__list { + margin: 0; + padding: 0; + list-style: none; + + @include govuk-media-query($from: tablet) { + position: relative; + + // Offset gutter by tablet list-item padding + left: $govuk-gutter-half * -1; + width: calc(100% - $govuk-gutter); + min-height: $navigation-height; + } +} + +.app-mobile-navigation__list-item--current { + @include govuk-media-query($from: tablet) { + border-bottom: 4px solid govuk-colour("blue"); + } +} + +.app-navigation__subnav-item { + display: block; + position: relative; + padding: govuk-spacing(3); +} + +.app-navigation__subnav-item--current { + $_current-indicator-width: 4px; + padding-left: govuk-spacing(4) - $_current-indicator-width; + border-left: $_current-indicator-width solid govuk-colour("blue"); +} + +.app-mobile-navigation .govuk-service-navigation__item--active { + @include govuk-media-query($until: tablet) { + border-color: transparent; + } +} + +.app-mobile-navigation__link--active { + @include govuk-media-query($until: tablet) { + border-left: 5px solid rgb(26.1, 100.8, 165.6); + } +} + +.app-mobile-navigation__toggle-button { + @include govuk-media-query($from: tablet) { + display: none; + } + position: relative; + padding: 0; + border: 0; + + background: none; + font-size: inherit; +} + +.app-mobile-navigation__toggle-button::after { + content: ""; + box-sizing: border-box; + display: block; + position: absolute; + right: govuk-px-to-rem(-14px); + bottom: govuk-px-to-rem(10px); + width: govuk-px-to-rem(6px); + height: govuk-px-to-rem(6px); + transform: rotate(135deg); + border-top: govuk-px-to-rem(2px) solid; + border-right: govuk-px-to-rem(2px) solid; +} + +.app-mobile-navigation__toggle-button[aria-expanded="false"] ~ .app-navigation__list { + display: none; +} + +.app-mobile-navigation__toggle-button[aria-expanded="true"]::after { + transform: rotate(-45deg); +} + +.app-mobile-navigation__toggle-button[aria-expanded="true"] ~ .app-navigation__list { + @include govuk-media-query($until: tablet) { + display: block; + } + + display: none; +} diff --git a/src/stylesheets/components/_navigation.scss b/src/stylesheets/components/_navigation.scss index 01dca33300..17865e471e 100644 --- a/src/stylesheets/components/_navigation.scss +++ b/src/stylesheets/components/_navigation.scss @@ -1,5 +1,15 @@ $navigation-height: 50px; +.app-navigation__list-toggle { + @include govuk-media-query($from: tablet) { + display: none; + } +} + +.js-app-navigation__list--hidden { + display: none; +} + .app-navigation { border-bottom: 1px solid $govuk-border-colour; background-color: $app-light-grey; diff --git a/src/stylesheets/main.scss b/src/stylesheets/main.scss index 9099717b9c..e574ad4a0c 100644 --- a/src/stylesheets/main.scss +++ b/src/stylesheets/main.scss @@ -26,6 +26,7 @@ $govuk-new-typography-scale: true; @import "govuk/components/panel"; @import "govuk/components/phase-banner"; @import "govuk/components/radios"; +@import "govuk/components/service-navigation"; @import "govuk/components/skip-link"; @import "govuk/components/summary-list"; @import "govuk/components/table"; @@ -58,7 +59,7 @@ $app-code-color: #d13118; @import "components/highlight"; @import "components/image-card"; @import "components/masthead"; -@import "components/navigation"; +@import "components/mobile-navigation"; @import "components/options"; @import "components/page-navigation"; @import "components/phase-banner"; @@ -68,8 +69,17 @@ $app-code-color: #d13118; @import "components/tabs"; @import "components/table"; +.app-nav-subitems-mobile { + @include govuk-media-query($from: tablet) { + display: none; + } + + background-color: govuk-colour("mid-grey"); +} + // We don't change the global width container width so that examples are the current width. -.app-width-container { +.app-width-container, +.govuk-service-navigation .govuk-width-container { @include govuk-width-container(1100px); } diff --git a/views/layouts/_generic.njk b/views/layouts/_generic.njk index 84850c14e8..a570490601 100644 --- a/views/layouts/_generic.njk +++ b/views/layouts/_generic.njk @@ -3,8 +3,6 @@ {% set assetUrl = 'https://design-system.service.gov.uk/assets' %} -{% set bodyAttributes = { "data-module": "app-navigation" } %} - {% block pageTitle %}{{ title | smartypants }} – GOV.UK Design System{% endblock %} {% block head %} diff --git a/views/partials/_header.njk b/views/partials/_header.njk index 18d7338068..7f75d83f9d 100644 --- a/views/partials/_header.njk +++ b/views/partials/_header.njk @@ -36,8 +36,5 @@ Sitemap - diff --git a/views/partials/_mobile-navigation.njk b/views/partials/_mobile-navigation.njk new file mode 100644 index 0000000000..0d8fefe644 --- /dev/null +++ b/views/partials/_mobile-navigation.njk @@ -0,0 +1,20 @@ +{% macro mobileNavigation(params) %} + {% if params.items %} + + {% endif %} +{% endmacro %} \ No newline at end of file diff --git a/views/partials/_navigation.njk b/views/partials/_navigation.njk index 92c6aff795..f464633a5e 100644 --- a/views/partials/_navigation.njk +++ b/views/partials/_navigation.njk @@ -1,45 +1,34 @@ - +{% set navigationItems = [] %} + +{% for item in navigation %} + {% set subNavItems = [{ + href: item.href, + text: item.text + " overview", + active: permalink and permalink.startsWith(item.href.slice(1)), + current: permalink === item.href + }] %} + + {% if item.items %} + {% set subNavItems = subNavItems.concat(item.items) %} + {% endif %} + + {% set subNavItemHtml = mobileNavigation({ items: subNavItems, theme: item.theme }) %} + + {% set navItem = { + href: item.href, + html: item.text + '', + active: permalink and permalink.startsWith(item.href.slice(1)), + current: permalink === item.href + } %} + + {% set navigationItems = navigationItems.concat(navItem) %} +{% endfor %} + +
+ {{ govukServiceNavigation({ + navigation: navigationItems + }) }} +