diff --git a/tbx/project_styleguide/templates/patterns/navigation/components/primary-nav-mobile.html b/tbx/project_styleguide/templates/patterns/navigation/components/primary-nav-mobile.html
index 1ca129606..ad96cbd13 100644
--- a/tbx/project_styleguide/templates/patterns/navigation/components/primary-nav-mobile.html
+++ b/tbx/project_styleguide/templates/patterns/navigation/components/primary-nav-mobile.html
@@ -1,35 +1,16 @@
{% load wagtailcore_tags %}
-{% comment %}
-The `child_display` has 3 possible values:
-- hide_children - do not show a subnavigation
-- show_up_to_level1 - show level one child pages
-- show_up_to_level2 - show level one and level two child pages
-{% endcomment %}
-
{% for item in primarynav %}
- {% with children=item.value.page.get_children.live.public.in_menu.specific child_display=item.value.child_display_behaviour %}
-
Mode switcher
- {% include "patterns/molecules/mode_switcher/mode_switcher.html" %}
+
{% include "patterns/molecules/mode_switcher/mode_switcher.html" %}
diff --git a/tbx/static_src/javascript/components/primary-mobile-menu.js b/tbx/static_src/javascript/components/primary-mobile-menu.js
new file mode 100644
index 000000000..311d6fd75
--- /dev/null
+++ b/tbx/static_src/javascript/components/primary-mobile-menu.js
@@ -0,0 +1,84 @@
+class PrimaryMobileMenu {
+ static selector() {
+ return '[data-primary-mobile-menu-toggle]';
+ }
+
+ constructor(node) {
+ this.node = node;
+ this.body = document.querySelector('body');
+ this.primaryMobileMenu = document.querySelector(
+ '[data-primary-mobile-menu]',
+ );
+ this.lastMenuItem = document.querySelector(
+ '[data-last-menu-item-primary-mobile]',
+ );
+
+ this.state = {
+ open: false,
+ };
+
+ this.bindEventListeners();
+ }
+
+ bindEventListeners() {
+ this.node.addEventListener('click', () => {
+ this.open();
+ });
+
+ // Close mobile dropdown with escape key for improved accessibility
+ document.addEventListener('keydown', (event) => {
+ if (event.key === 'Escape') {
+ if (this.state.open) {
+ this.close();
+ this.state.open = false;
+ }
+ }
+ });
+
+ // Close mobile dropdown when clicking outside of the menu
+ document.addEventListener('click', (event) => {
+ if (this.state.open && !this.node.contains(event.target)) {
+ this.close();
+ this.state.open = false;
+ }
+ });
+
+ // Close the mobile menu when the focus moves away from the last item in the top level
+ if (this.lastMenuItem === null) {
+ return;
+ }
+
+ this.lastMenuItem.addEventListener('focusout', () => {
+ if (this.state.open) {
+ this.close();
+ this.state.open = false;
+ }
+ });
+ }
+
+ open() {
+ // Fire a custom event which is useful if we need any other items such as
+ // a search box to close when the mobile menu opens
+ // Can be listened to with
+ // document.addEventListener('onMenuOpen', () => {
+ // // do stuff here...;
+ // });
+ const menuOpenEvent = new Event('onMenuOpen');
+ document.dispatchEvent(menuOpenEvent);
+ this.node.setAttribute('aria-expanded', 'true');
+ this.body.classList.add('no-scroll');
+ this.primaryMobileMenu.classList.add('is-visible');
+
+ this.state.open = true;
+ }
+
+ close() {
+ this.node.setAttribute('aria-expanded', 'false');
+ this.body.classList.remove('no-scroll');
+ this.primaryMobileMenu.classList.remove('is-visible');
+
+ this.state.open = false;
+ }
+}
+
+export default PrimaryMobileMenu;
diff --git a/tbx/static_src/javascript/components/primary-mobile-menu.test.js b/tbx/static_src/javascript/components/primary-mobile-menu.test.js
new file mode 100644
index 000000000..417009b8f
--- /dev/null
+++ b/tbx/static_src/javascript/components/primary-mobile-menu.test.js
@@ -0,0 +1,58 @@
+import PrimaryMobileMenu from './primary-mobile-menu';
+
+describe('MobileMenu', () => {
+ beforeEach(() => {
+ document.body.innerHTML = `
+
+
+ `;
+ });
+
+ it('hides the menu by default', () => {
+ // eslint-disable-next-line no-new
+ new PrimaryMobileMenu(
+ document.querySelector(PrimaryMobileMenu.selector()),
+ );
+
+ expect(
+ document.querySelector('[data-primary-mobile-menu]').className,
+ ).toBe('primary-nav-mobile');
+ });
+
+ it('shows the menu when clicked', () => {
+ // eslint-disable-next-line no-new
+ new PrimaryMobileMenu(
+ document.querySelector(PrimaryMobileMenu.selector()),
+ );
+
+ const button = document.querySelector(
+ '[data-primary-mobile-menu-toggle]',
+ );
+ button.dispatchEvent(new Event('click'));
+
+ expect(
+ document.querySelector('[data-primary-mobile-menu]').className,
+ ).toBe('primary-nav-mobile is-visible');
+ });
+
+ it('hides then menu when clicked outside once open', () => {
+ // eslint-disable-next-line no-new
+ new PrimaryMobileMenu(
+ document.querySelector(PrimaryMobileMenu.selector()),
+ () => {},
+ );
+
+ const button = document.querySelector(
+ '[data-primary-mobile-menu-toggle]',
+ );
+ button.dispatchEvent(new Event('click'));
+ expect(
+ document.querySelector('[data-primary-mobile-menu]').className,
+ ).toBe('primary-nav-mobile is-visible');
+
+ document.dispatchEvent(new Event('click'));
+ expect(
+ document.querySelector('[data-primary-mobile-menu]').className,
+ ).toBe('primary-nav-mobile');
+ });
+});
diff --git a/tbx/static_src/javascript/main.js b/tbx/static_src/javascript/main.js
index d6111e12e..8a6ada51a 100755
--- a/tbx/static_src/javascript/main.js
+++ b/tbx/static_src/javascript/main.js
@@ -1,3 +1,4 @@
+import PrimaryMobileMenu from './components/primary-mobile-menu';
import MobileMenu from './components/mobile-menu';
import MobileSubMenu from './components/mobile-sub-menu';
import DesktopSubMenu from './components/desktop-sub-menu';
@@ -29,6 +30,7 @@ function initComponent(ComponentClass) {
document.addEventListener('DOMContentLoaded', () => {
/* eslint-disable no-new */
+ initComponent(PrimaryMobileMenu);
initComponent(MobileMenu);
initComponent(MobileSubMenu);
initComponent(DesktopSubMenu);
diff --git a/tbx/static_src/sass/components/_grid.scss b/tbx/static_src/sass/components/_grid.scss
index 33edbc667..f3e4e9d8b 100644
--- a/tbx/static_src/sass/components/_grid.scss
+++ b/tbx/static_src/sass/components/_grid.scss
@@ -346,27 +346,12 @@
}
}
- // header and navigation grid elements
- &__header-logo {
- grid-column: 2 / span 2;
-
- @include media-query(medium) {
- grid-column: 2 / span 1;
- }
- }
-
&__header-nav {
- display: none;
-
- @include media-query(menu) {
- display: flex;
- justify-content: flex-end;
- grid-column: 3 / span 3;
- align-items: flex-end;
- }
+ display: flex;
+ grid-column: 2 / span 4;
@include media-query(large) {
- grid-column: 3 / span 11;
+ grid-column: 2 / span 12;
}
}
diff --git a/tbx/static_src/sass/components/_header.scss b/tbx/static_src/sass/components/_header.scss
index 0af8de9d7..2b5efb41c 100644
--- a/tbx/static_src/sass/components/_header.scss
+++ b/tbx/static_src/sass/components/_header.scss
@@ -1,28 +1,30 @@
@use 'config' as *;
.header {
- background-color: var(--color--background);
- padding: 25px 0;
- align-items: end;
+ background-color: var(--color--black);
+ padding: 10px 0;
- @include media-query(medium) {
- padding: 35px 0 $spacer-small-plus;
- }
-
- &__logo-link {
- &:focus {
- @include focus-style();
- }
- }
-
- &__menu-toggle {
- @include z-index(header);
+ &__primary-menu-toggle {
+ @include link-styles(
+ $color: var(--color--white),
+ $underline-color: var(--color--white),
+ $interaction-color: var(--color--white)
+ );
display: flex;
- width: 100%;
- justify-content: flex-end;
+ align-items: center;
+ gap: 8px;
@include media-query(menu) {
display: none;
}
+
+ &:focus {
+ @include focus-style($keyboard-only: true);
+ }
+ }
+
+ &__primary-menu-toggle-icon {
+ width: 12px;
+ height: 12px;
}
}
diff --git a/tbx/static_src/sass/components/_mode-switcher.scss b/tbx/static_src/sass/components/_mode-switcher.scss
index b2ef0f4f0..211042c88 100644
--- a/tbx/static_src/sass/components/_mode-switcher.scss
+++ b/tbx/static_src/sass/components/_mode-switcher.scss
@@ -2,19 +2,7 @@
.mode-switcher {
$root: &;
- padding-top: 45px;
- border-top: 1px solid var(--color--border);
- display: none; // temporarily hidden until the light mode is updated
- justify-content: center;
-
- @include media-query(menu) {
- display: none; // temporarily hidden until the light mode is updated
- padding-top: 0;
- padding-left: $spacer-small;
- margin-left: $spacer-small;
- border-left: 1px solid var(--color--border);
- border-top: 0;
- }
+ margin-left: auto;
&__layout {
display: flex;
@@ -46,14 +34,13 @@
}
&__icon {
+ color: var(--color--grey-50);
&--light {
width: 16px;
height: 16px;
.mode-light & {
- @include media-query(menu) {
- color: var(--color--theme-primary);
- }
+ color: var(--color--white);
}
}
@@ -62,9 +49,7 @@
height: 13px;
.mode-dark & {
- @include media-query(menu) {
- color: var(--color--theme-primary);
- }
+ color: var(--color--white);
}
}
}
@@ -89,14 +74,10 @@
position: absolute;
border-radius: 12px;
transition: inset-inline-start 0.25s ease-out;
- background-color: var(--color--heading);
+ background-color: var(--color--white);
// for hcm
border: 1px solid transparent;
- @include media-query(menu) {
- background-color: var(--color--theme-primary);
- }
-
@include reduced-motion() {
transition: none;
}
diff --git a/tbx/static_src/sass/components/_skip-link.scss b/tbx/static_src/sass/components/_skip-link.scss
index f196cb0df..542d17fc1 100644
--- a/tbx/static_src/sass/components/_skip-link.scss
+++ b/tbx/static_src/sass/components/_skip-link.scss
@@ -1,6 +1,7 @@
@use 'config' as *;
.skip-link {
+ @include z-index('header');
position: absolute;
top: -200px;
left: 0;
diff --git a/tbx/static_src/sass/components/navigation/_primary-nav-desktop.scss b/tbx/static_src/sass/components/navigation/_primary-nav-desktop.scss
index 2fa77d0d5..a5c71a162 100644
--- a/tbx/static_src/sass/components/navigation/_primary-nav-desktop.scss
+++ b/tbx/static_src/sass/components/navigation/_primary-nav-desktop.scss
@@ -1,10 +1,10 @@
@use 'config' as *;
/*
-Styles for the top level of the navigation at desktop
+Styles for the primary navigation at desktop (top level)
*/
-// the nav element surrounding the whole navigation
+// The nav element surrounding the whole navigation
// There are separate nav elements for desktop and mobile
.primary-nav-desktop {
$root: &;
@@ -16,12 +16,11 @@ Styles for the top level of the navigation at desktop
display: inline-block;
}
- // Top level menu unordered list
&__list {
display: flex;
flex-direction: row;
justify-content: flex-end;
- gap: 3vw;
+ gap: 4vw;
overflow: visible;
@include media-query(large) {
@@ -29,90 +28,20 @@ Styles for the top level of the navigation at desktop
}
}
- // top level menu list items
&__item {
position: relative;
width: auto;
- font-weight: $weight--semibold;
}
- // top level menu links
&__link {
- &:focus {
- @include focus-style();
- }
- }
-
- // top level menu link text
- &__text {
- @include link-styles($interaction-color: var(--color--heading));
- }
-
- // desktop arrows
- // wrapper ensures the text doesn't move when the icon changes
- &__icon-wrapper {
- @include z-index(overlap);
- width: 13px;
- position: relative;
- display: inline-block;
- margin-left: 8px;
- }
-
- // chevron when menu is closed
- &__icon-closed {
- display: block;
- width: 10px;
- height: 6px;
- color: var(--color--text);
-
- @include high-contrast-light-mode() {
- color: var(--color--decoration);
- }
-
- .active &,
- #{$root}__link:hover & {
- display: none;
- }
- }
-
- // tall arrow when menu is open
- &__icon-open {
- display: none;
- width: 13px;
- height: 85px;
- position: absolute;
- top: -8px;
- color: var(--color--heading);
-
- @include high-contrast-light-mode() {
- color: var(--color--decoration);
- }
-
- .active & {
- display: block;
- }
- }
-
- // short icon when hovering
- &__icon-hover {
- display: none;
- width: 11px;
- height: 16px;
- position: absolute;
- top: -8px;
- color: var(--color--heading);
-
- @include high-contrast-light-mode() {
- color: var(--color--decoration);
- }
+ @include high-contrast-text-decoration();
+ color: var(--color--grey-20);
- #{$root}__link:hover & {
- display: block;
+ &:hover {
+ color: var(--color--white);
}
-
- // don't show as well as the active icon
- .active #{$root}__link:hover & {
- display: none;
+ &:focus {
+ @include focus-style();
}
}
}
diff --git a/tbx/static_src/sass/components/navigation/_primary-nav-mobile.scss b/tbx/static_src/sass/components/navigation/_primary-nav-mobile.scss
index f502bed92..64e94918e 100644
--- a/tbx/static_src/sass/components/navigation/_primary-nav-mobile.scss
+++ b/tbx/static_src/sass/components/navigation/_primary-nav-mobile.scss
@@ -1,19 +1,24 @@
@use 'config' as *;
/*
-Styles for the top level of the navigation at mobile
+Styles for the primary navigation at mobile (top level)
*/
-// the nav element surrounding the whole navigation
+// The nav element surrounding the whole navigation
// There are separate nav elements for desktop and mobile
.primary-nav-mobile {
$root: &;
// At mobile the navigation is only revealed when the menu icon is clicked
@include z-index(base);
- @include nav-fixed-position($header-height: $header-height-mobile);
+ @include nav-fixed-position($header-height: 12px, $full-height: false);
+ left: 15px;
+ width: auto;
+ min-width: 200px;
display: block;
- background-color: var(--color--background);
+ background-color: var(--color--black);
+ border: 1px solid var(--color--eclipse);
+ padding: $spacer-mini-plus 0;
@include media-query(menu) {
display: none;
@@ -25,69 +30,28 @@ Styles for the top level of the navigation at mobile
opacity: 1;
}
- // Top level menu unordered list
&__list {
display: flex;
flex-direction: column;
}
- // top level menu list items
&__item {
- font-weight: $weight--semibold;
position: relative;
width: 100%;
}
- // top level menu links
&__link {
- @include font-size('size-five');
+ @include font-size('size-six');
+ @include link-styles(
+ $color: var(--color--grey-20),
+ $interaction-color: var(--color--white)
+ );
+ @include high-contrast-text-decoration();
display: flex;
- padding: $spacer-small;
- border-bottom: 1px solid var(--color--border);
- justify-content: space-between;
- align-items: center;
+ padding: 10px $spacer-small;
&:focus {
@include focus-style();
}
-
- #{$root}__item:last-child & {
- border-bottom: none;
- }
-
- // styles for links with children
- &--has-children {
- color: var(--color--heading);
- }
-
- // styles for links without children
- &--no-children {
- @include link-styles(
- $color: var(--color--heading),
- $interaction-color: var(--color--heading),
- $underline-color: var(--color--heading)
- );
- }
- }
-
- // mobile arrow - appears if there are children
- &__icon-mobile {
- width: 18px;
- height: 21px;
- flex-grow: 0;
- flex-shrink: 0;
- transition: transform $transition-quick;
-
- #{$root}__link:hover & {
- transform: translateX(10px);
- }
-
- @include media-query(menu) {
- display: none;
- }
-
- @include reduced-motion() {
- transition: none;
- }
}
}
diff --git a/tbx/static_src/sass/components/navigation/_sub-nav-desktop-mini.scss b/tbx/static_src/sass/components/navigation/_sub-nav-desktop-mini.scss
index efb6aa077..0eba98f9c 100644
--- a/tbx/static_src/sass/components/navigation/_sub-nav-desktop-mini.scss
+++ b/tbx/static_src/sass/components/navigation/_sub-nav-desktop-mini.scss
@@ -12,10 +12,7 @@ Here we refer to 'level 1 children' as 'level 2' and 'level 2 children' as 'leve
.sub-nav-desktop-mini {
$root: &;
@include z-index(base);
- @include nav-fixed-position(
- $header-height: $header-height-desktop,
- $full-height: false
- );
+ @include nav-fixed-position($full-height: false);
background-color: var(--color--background);
padding: 25px 0 $spacer-mini;
position: absolute;
diff --git a/tbx/static_src/sass/components/navigation/_sub-nav-desktop.scss b/tbx/static_src/sass/components/navigation/_sub-nav-desktop.scss
index e530af040..0a307a6d3 100644
--- a/tbx/static_src/sass/components/navigation/_sub-nav-desktop.scss
+++ b/tbx/static_src/sass/components/navigation/_sub-nav-desktop.scss
@@ -14,10 +14,7 @@ Here we refer to 'level 1 children' as 'level 2' and 'level 2 children' as 'leve
.sub-nav-desktop {
$root: &;
@include z-index(base);
- @include nav-fixed-position(
- $header-height: $header-height-desktop,
- $full-height: false
- );
+ @include nav-fixed-position($full-height: false);
background-color: var(--color--background);
padding: $spacer-half $grid-gutters;
// for high contrast mode
diff --git a/tbx/static_src/sass/components/navigation/_sub-nav-mobile.scss b/tbx/static_src/sass/components/navigation/_sub-nav-mobile.scss
index 850c86ecb..4acefced5 100644
--- a/tbx/static_src/sass/components/navigation/_sub-nav-mobile.scss
+++ b/tbx/static_src/sass/components/navigation/_sub-nav-mobile.scss
@@ -13,7 +13,7 @@ Here we refer to 'level 1 children' as 'level 2' and 'level 2 children' as 'leve
.sub-nav-mobile {
$root: &;
@include z-index(base);
- @include nav-fixed-position($header-height: $header-height-mobile);
+ @include nav-fixed-position();
background-color: var(--color--background);
// the unordered list containing the level 2 menu items
diff --git a/tbx/static_src/sass/config/_mixins.scss b/tbx/static_src/sass/config/_mixins.scss
index b786da6d4..225c0352a 100755
--- a/tbx/static_src/sass/config/_mixins.scss
+++ b/tbx/static_src/sass/config/_mixins.scss
@@ -122,7 +122,8 @@
@mixin focus-style(
$color: var(--color--link),
$shadow: false,
- $underline-hover-color: $color
+ $underline-hover-color: $color,
+ $keyboard-only: false
) {
outline: $focus-width solid var(--color--theme-primary);
text-decoration-thickness: $link-underline-thickness-interaction;
@@ -131,6 +132,12 @@
@if $shadow {
text-shadow: 0 0 0.4px $color, 0 0 0.4px $color;
}
+
+ @if $keyboard-only {
+ &:focus:not(:focus-visible) {
+ outline-color: transparent;
+ }
+ }
}
@mixin arrow-focus-style() {
@@ -279,10 +286,7 @@
Navigaiton mixins
*/
-@mixin nav-fixed-position(
- $header-height: $header-height-mobile,
- $full-height: true
-) {
+@mixin nav-fixed-position($header-height: $header-height, $full-height: true) {
width: 100%;
@if $full-height {
height: calc(100vh - #{$header-height});
diff --git a/tbx/static_src/sass/config/_variables.scss b/tbx/static_src/sass/config/_variables.scss
index e2a68a8d9..572434ca6 100755
--- a/tbx/static_src/sass/config/_variables.scss
+++ b/tbx/static_src/sass/config/_variables.scss
@@ -60,6 +60,8 @@ $color--black: #000;
// The following base variables do not change with either theme or mode
--color--white: #{$color--white};
--color--grey-10: #{$color--grey-10};
+ --color--grey-20: #{$color--grey-20};
+ --color--grey-50: #{$color--grey-50};
--color--grey-70: #{$color--grey-70};
--color--eclipse: #{$color--eclipse};
--color--black: #{$color--black};
@@ -350,5 +352,4 @@ $avatar-size-100: 100px;
$avatar-size-72: 72px;
$avatar-size-80: 80px;
-$header-height-desktop: 106px;
-$header-height-mobile: 81px;
+$header-height: 46px;