From 9355b1856c786f175465d4ea2c0738632ce29cef Mon Sep 17 00:00:00 2001 From: Brett Kyle <22524634+domoscargin@users.noreply.github.com> Date: Wed, 8 Jul 2026 16:31:55 +0100 Subject: [PATCH 1/3] [SPIKE] Collapse language switcher on mobile --- packages/govuk-frontend/src/govuk/all.mjs | 1 + .../components/language-switcher/_mixin.scss | 87 ++++++-- .../language-switcher/language-switcher.mjs | 196 ++++++++++++++++++ .../language-switcher/language-switcher.yaml | 16 ++ .../components/language-switcher/template.njk | 18 +- .../service-navigation.yaml | 24 ++- .../src/govuk/init.jsdom.test.mjs | 2 + packages/govuk-frontend/src/govuk/init.mjs | 2 + .../tasks/build/package.unit.test.mjs | 1 + 9 files changed, 327 insertions(+), 20 deletions(-) create mode 100644 packages/govuk-frontend/src/govuk/components/language-switcher/language-switcher.mjs diff --git a/packages/govuk-frontend/src/govuk/all.mjs b/packages/govuk-frontend/src/govuk/all.mjs index f0fd0ba0f0..6ea3c02219 100644 --- a/packages/govuk-frontend/src/govuk/all.mjs +++ b/packages/govuk-frontend/src/govuk/all.mjs @@ -6,6 +6,7 @@ export { Checkboxes } from './components/checkboxes/checkboxes.mjs' export { ErrorSummary } from './components/error-summary/error-summary.mjs' export { ExitThisPage } from './components/exit-this-page/exit-this-page.mjs' export { FileUpload } from './components/file-upload/file-upload.mjs' +export { LanguageSwitcher } from './components/language-switcher/language-switcher.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' diff --git a/packages/govuk-frontend/src/govuk/components/language-switcher/_mixin.scss b/packages/govuk-frontend/src/govuk/components/language-switcher/_mixin.scss index 77a86cf7fb..44f204d6c1 100644 --- a/packages/govuk-frontend/src/govuk/components/language-switcher/_mixin.scss +++ b/packages/govuk-frontend/src/govuk/components/language-switcher/_mixin.scss @@ -10,28 +10,87 @@ color: base.govuk-functional-colour(text); } - .govuk-language-switcher__list { + // Modifier to remove the top margin on small viewports. Intended for use when + // the switcher is in the service navigation. Kept as a modifier so the style is + // not dependent on another component. + .govuk-language-switcher--no-top-margin { + @media #{base.govuk-until-breakpoint(tablet)} { + margin-top: 0; + } + } + + // Menu toggle button, shown on mobile when JavaScript is available. These + // styles deliberately mirror the service navigation menu toggle. + .govuk-language-switcher__toggle { + @include base.govuk-font($size: 19, $weight: bold); display: inline-flex; - flex-wrap: wrap; - row-gap: base.govuk-spacing(1); + align-items: center; + margin: base.govuk-spacing(2) 0; + padding: 0; + border: 0; + color: base.govuk-functional-colour(link); + background: none; + word-break: break-all; + cursor: pointer; + + &:focus { + @include base.govuk-focused-text; + } + + &::after { + @include base.govuk-shape-arrow($direction: down, $base: 10px, $display: inline-block); + content: ""; + margin-left: base.govuk-spacing(1); + } + + &[aria-expanded="true"]::after { + @include base.govuk-shape-arrow($direction: up, $base: 10px, $display: inline-block); + } + + &[hidden] { + display: none; + } + } + + // Icon shown before the toggle button text + .govuk-language-switcher__toggle-icon { + flex-shrink: 0; + margin-right: base.govuk-spacing(1); + } + + .govuk-language-switcher__list { margin: 0; padding: 0; list-style-type: none; + + @media #{base.govuk-from-breakpoint(tablet)} { + display: inline-flex; + flex-wrap: wrap; + row-gap: base.govuk-spacing(1); + } } .govuk-language-switcher__list-item { - display: flex; - align-items: center; + // On mobile, stack the language items vertically, matching the service + // navigation menu + @media #{base.govuk-until-breakpoint(tablet)} { + margin: base.govuk-spacing(2) 0; + } + + @media #{base.govuk-from-breakpoint(tablet)} { + display: flex; + align-items: center; - &:not(:last-child) { - &::after { - content: ""; - display: block; - height: 1em; - margin-right: base.govuk-spacing(2); - margin-left: base.govuk-spacing(2); - border-right: 1px solid; - border-right-color: base.govuk-functional-colour(secondary-text); + &:not(:last-child) { + &::after { + content: ""; + display: block; + height: 1em; + margin-right: base.govuk-spacing(2); + margin-left: base.govuk-spacing(2); + border-right: 1px solid; + border-right-color: base.govuk-functional-colour(secondary-text); + } } } } diff --git a/packages/govuk-frontend/src/govuk/components/language-switcher/language-switcher.mjs b/packages/govuk-frontend/src/govuk/components/language-switcher/language-switcher.mjs new file mode 100644 index 0000000000..236e77d1fb --- /dev/null +++ b/packages/govuk-frontend/src/govuk/components/language-switcher/language-switcher.mjs @@ -0,0 +1,196 @@ +import { getBreakpoint } from '../../common/index.mjs' +import { Component } from '../../component.mjs' +import { ElementError } from '../../errors/index.mjs' + +/** + * Language switcher component + * + * On mobile, collapses the list of languages into a menu behind a toggle + * button, mirroring the behaviour of the service navigation menu. + * + * @preserve + */ +export class LanguageSwitcher extends Component { + /** @private */ + $menuButton + + /** @private */ + $menu + + /** + * Remember the open/closed state of the menu 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} $root - HTML element to use for the language switcher + */ + constructor($root) { + super($root) + + const $menuButton = this.$root.querySelector( + '.govuk-js-language-switcher-toggle' + ) + + // The toggle button may be omitted when writing plain HTML, in which case + // the list of languages is always shown + if (!$menuButton) { + return this + } + + const menuId = $menuButton.getAttribute('aria-controls') + if (!menuId) { + throw new ElementError({ + component: LanguageSwitcher, + identifier: + 'Menu button (` + {% endif %} +