From a88e15e6b66755fa3fe2662fd451a56cbb841e26 Mon Sep 17 00:00:00 2001 From: Cal Mitchell Date: Mon, 16 Jun 2025 10:57:24 +0000 Subject: [PATCH 1/4] NCRS-3931 added logic so mobile menu closes on lost focus --- src/components/header-with-logo/header.js | 79 ++++++++++++++++------- 1 file changed, 56 insertions(+), 23 deletions(-) diff --git a/src/components/header-with-logo/header.js b/src/components/header-with-logo/header.js index a5f790a..19cd11e 100644 --- a/src/components/header-with-logo/header.js +++ b/src/components/header-with-logo/header.js @@ -3,6 +3,8 @@ * See Github issue https://github.com/nhsuk/nhsuk-frontend/issues/937 */ +import { resourceUsage } from "process"; + class Header { constructor() { @@ -11,7 +13,6 @@ class Header { this.navigationList = document.querySelector('.nhsuk-header__navigation-list'); this.mobileMenu = document.createElement('ul'); this.mobileMenuToggleButton = document.querySelector('.nhsuk-header__menu-toggle'); - this.mobileMenuCloseButton = document.createElement('button'); this.mobileMenuContainer = document.querySelector('.nhsuk-mobile-menu-container'); this.breakpoints = []; this.width = document.body.offsetWidth; @@ -29,22 +30,28 @@ class Header { // calculateBreakpoints and updateNavigtion need to be run twice // the second run takes into account the width of the logo - this.setupMobileMenu(); - this.calculateBreakpoints(); - this.updateNavigation(); - this.doOnOrientationChange(); - this.calculateBreakpoints(); - this.updateNavigation(); + this.setupMobileMenu(); + this.calculateBreakpoints(); + this.updateNavigation(); + this.doOnOrientationChange(); + this.calculateBreakpoints(); + this.updateNavigation(); this.handleResize = this.debounce(() => { this.calculateBreakpoints(); this.updateNavigation(); }); - this.mobileMenuToggleButton.addEventListener('click', this.toggleMobileMenu.bind(this)); window.addEventListener('resize', this.handleResize); window.addEventListener('orientationchange', this.doOnOrientationChange()); + + // Add new blur listeners to dropdown menu + // It had to be applied to all nav links as removeEventListener wasn't working + const allLinks = [this.mobileMenuToggleButton, ...this.navigation.querySelectorAll('a.nhsuk-header__navigation-link')]; + for (let i = 0; i < allLinks.length; i++) { + allLinks[i].addEventListener('blur', this.onBlur.bind(this)) + } } debounce(func, timeout = 100) { @@ -65,14 +72,13 @@ class Header { * */ calculateBreakpoints() { - let childrenWidth = 0; - for (let i = 0; i < this.navigationList.children.length; i++) { - childrenWidth += this.navigationList.children[i].offsetWidth; - this.breakpoints[i] = childrenWidth; - } - } + for (let i = 0; i < this.navigationList.children.length; i++) { + childrenWidth += this.navigationList.children[i].offsetWidth; + this.breakpoints[i] = childrenWidth; + } + } // Add the mobile menu to the DOM setupMobileMenu() { @@ -85,15 +91,13 @@ class Header { * * Closes the mobile menu and updates accessibility state. * - * Remvoes the margin-bottom from the navigation + * Removes the margin-bottom from the navigation */ closeMobileMenu() { this.menuIsOpen = false; this.mobileMenu.classList.add('nhsuk-header__drop-down--hidden'); this.navigation.style.marginBottom = 0; this.mobileMenuToggleButton.setAttribute('aria-expanded', 'false'); - this.mobileMenuToggleButton.focus(); - this.mobileMenuCloseButton.removeEventListener('click', this.closeMobileMenu.bind(this)); document.removeEventListener('keydown', this.handleEscapeKey.bind(this)); } @@ -107,6 +111,39 @@ class Header { handleEscapeKey(e) { if (e.key === 'Escape') { this.closeMobileMenu(); + this.mobileMenuToggleButton.focus(); + } + } + + /** + * Blur Listening + * + * Listener for when the focus leaves a nav link / toggle + * + * If the currentTarget is dropdown-related (toggle or child link), and the new one isn't, we want to close the + * mobileMenu. + */ + onBlur(e) { + const grandchildren = this.mobileMenu.querySelectorAll('li a'); + const mobileMenuIsClosed = this.mobileMenu.classList.contains('nhsuk-header__drop-down--hidden'); + // Checks the current link even qualifies + const currentFocusIsInDropdown = Array.from(grandchildren).includes(e.currentTarget); + const currentFocusIsToggleOrSubLink = e.currentTarget.classList.contains('nhsuk-header__menu-toggle') || currentFocusIsInDropdown + + // If the menu isn't open, or the old link isn't one of the toggles or sub links, return; + if (mobileMenuIsClosed || !currentFocusIsToggleOrSubLink) { + return; + } + + // Focus left any link + if (e.relatedTarget === null) { + this.closeMobileMenu(); + return; + } + + const newFocusIsInDropdown = Array.from(grandchildren).includes(e.relatedTarget); + if (!e.relatedTarget.classList.contains('nhsuk-header__menu-toggle') && !newFocusIsInDropdown) { + this.closeMobileMenu(); } } @@ -128,11 +165,8 @@ class Header { this.navigation.style.marginBottom = `${marginBody}px`; this.mobileMenuToggleButton.setAttribute('aria-expanded', 'true'); - // add event listerer for esc key to close menu + // add event listener for esc key to close menu document.addEventListener('keydown', this.handleEscapeKey.bind(this)); - - // add event listener for close icon to close menu - this.mobileMenuCloseButton.addEventListener('click', this.closeMobileMenu.bind(this)); } /** @@ -158,7 +192,7 @@ class Header { * If the available space is greater than the current breakpoint, * remove the mobile menu toggle button and move the first item in the * - * Additionaly will close the mobile menu if the window gets resized + * Additionally will close the mobile menu if the window gets resized * and the menu is open. */ @@ -196,7 +230,6 @@ class Header { if (document.body.offsetWidth !== this.width && this.menuIsOpen) { this.closeMobileMenu(); } - } /** From a6cff2f75dd4716f4ab2db2d7cb40dba3b65d240 Mon Sep 17 00:00:00 2001 From: Cal Mitchell Date: Tue, 17 Jun 2025 11:55:25 +0000 Subject: [PATCH 2/4] NCRS-3931 Removed double dropdown toggle logic --- .../header-with-logo/_headerWithLogo.scss | 6 ++++-- .../components/LocalNavDropdownMenu.tsx | 19 +------------------ src/components/header-with-logo/header.js | 11 ++++------- 3 files changed, 9 insertions(+), 27 deletions(-) diff --git a/src/components/header-with-logo/_headerWithLogo.scss b/src/components/header-with-logo/_headerWithLogo.scss index d40f97b..7833bc4 100644 --- a/src/components/header-with-logo/_headerWithLogo.scss +++ b/src/components/header-with-logo/_headerWithLogo.scss @@ -56,7 +56,7 @@ flex-shrink: 1; align-items: center; justify-content: flex-end; - + .nhsuk-navigation { width: 100%; max-width: 100%; @@ -109,6 +109,7 @@ right: 0; padding-right: 8px; border-radius: 0; + color: #212b32; // Chevron fixes .nhsuk-icon { @@ -117,9 +118,10 @@ height: 24px; margin-left: 6px; transform: rotate(90deg); + fill: inherit; } - &[aria-expanded='true'] { + &--expanded { .nhsuk-icon { transform: rotate(-90deg); } diff --git a/src/components/header-with-logo/components/LocalNavDropdownMenu.tsx b/src/components/header-with-logo/components/LocalNavDropdownMenu.tsx index 0c1a784..121d8c5 100644 --- a/src/components/header-with-logo/components/LocalNavDropdownMenu.tsx +++ b/src/components/header-with-logo/components/LocalNavDropdownMenu.tsx @@ -1,5 +1,4 @@ -import React, { FC, HTMLProps, useContext, useEffect, MouseEvent } from 'react'; -import HeaderContext, { IHeaderContext } from '../HeaderContext'; +import React, { FC, HTMLProps } from 'react'; import { ChevronDownIcon } from './LocalChevronDown'; export interface NavDropdownMenuProps extends HTMLProps { type?: 'button' | 'submit' | 'reset'; @@ -7,27 +6,11 @@ export interface NavDropdownMenuProps extends HTMLProps { } const NavMenuDropdown: FC = ({ onClick, dropdownText = 'More', ...rest }) => { - const { setMenuToggle, toggleMenu, menuOpen } = useContext(HeaderContext); - - const onToggleClick = (e: MouseEvent) => { - toggleMenu(); - - if (onClick) { - onClick(e); - } - }; - - useEffect(() => { - setMenuToggle(true); - return () => setMenuToggle(false); - }, []); return (