From 2b917603c6f22a807a20bb003d04e6ec24434b98 Mon Sep 17 00:00:00 2001 From: Dev Ashish Saradhana <41122199+Deva309@users.noreply.github.com> Date: Mon, 9 Dec 2024 14:24:48 +0530 Subject: [PATCH] [MWPW-163735] - Added Local Nav keyboard navigation for mobile GNAV redesign (#3321) * Added localnav keyboard navigation for mobile gnav redesign * Added fixes for Local nav along with keyboard navigation * lint fix --- .../global-navigation/global-navigation.css | 34 ++++++-- .../global-navigation/global-navigation.js | 87 ++++++++++++------- .../utilities/keyboard/index.js | 16 +++- .../utilities/keyboard/localNav.js | 38 ++++++++ .../utilities/keyboard/utils.js | 3 + .../global-navigation/utilities/utilities.js | 4 +- libs/styles/styles.css | 22 ++++- 7 files changed, 158 insertions(+), 46 deletions(-) create mode 100644 libs/blocks/global-navigation/utilities/keyboard/localNav.js diff --git a/libs/blocks/global-navigation/global-navigation.css b/libs/blocks/global-navigation/global-navigation.css index 8948fa0fbb6..60f45a172a4 100644 --- a/libs/blocks/global-navigation/global-navigation.css +++ b/libs/blocks/global-navigation/global-navigation.css @@ -699,10 +699,6 @@ header.global-navigation { .feds-breadcrumbs a:hover { text-decoration: underline; } - - header + div.feds-localnav { - display: none; - } } /* Small desktop styles */ @@ -1051,9 +1047,8 @@ header.new-nav .feds-breadcrumbs li:first-child:not(:nth-last-child(-n+3)):after /* local-nav */ -header + .feds-localnav { +.feds-localnav { position: sticky; - display: block; top: 0; width: 100%; left: 0; @@ -1099,6 +1094,7 @@ header + .feds-localnav { display: block; width: 100%; position: absolute; + overflow: auto; } .feds-localnav.active .feds-localnav-items .feds-menu-items { @@ -1127,6 +1123,8 @@ header + .feds-localnav { font-size: 14px; padding: 12px 20px; background: var(--feds-background-popup); + outline-offset: -1px; + cursor: pointer; } .feds-localnav .feds-localnav-items .feds-menu-headline { @@ -1134,28 +1132,50 @@ header + .feds-localnav { font-weight: 400; border-bottom: 0; padding: 12px 38px; + outline-offset: -1px; } .feds-localnav .feds-localnav-items .feds-navItem--centered { padding: 10px 20px; } +.feds-localnav.active .feds-localnav-items { + box-sizing: border-box; + max-height: calc(100vh - (var(--global-height-nav) + var(--feds-localnav-height))); +} + +.feds-localnav.active.is-sticky .feds-localnav-items { + max-height: calc(100vh - var(--feds-localnav-height)); +} + .feds-localnav .feds-dropdown--active::before { display: none; } +.feds-localnav .feds-localnav-exit { + display: none; +} + .feds-localnav.active .feds-localnav-title::after { transform: rotateZ(-135deg); } .feds-localnav.active .feds-localnav-curtain { width: 100%; - height: 100vh; + height: calc(100vh - (var(--feds-height-nav) + var(--feds-localnav-height))); position: absolute; background: var(--feds-color-black-v2); opacity: 0.7; } +.feds-localnav.active.is-sticky .feds-localnav-curtain { + height: calc(100vh - var(--feds-localnav-height)); +} + +.feds-localnav.active .feds-localnav-exit { + display: block; +} + @keyframes slideright { from { translate: 0 0; diff --git a/libs/blocks/global-navigation/global-navigation.js b/libs/blocks/global-navigation/global-navigation.js index db9daed8ea9..0bf32070ff5 100644 --- a/libs/blocks/global-navigation/global-navigation.js +++ b/libs/blocks/global-navigation/global-navigation.js @@ -223,10 +223,10 @@ const decorateProfileTrigger = async ({ avatar }) => { }; let keyboardNav; -const setupKeyboardNav = async () => { +const setupKeyboardNav = async (newMobileWithLnav) => { keyboardNav = keyboardNav || new Promise(async (resolve) => { const KeyboardNavigation = await loadBlock('./keyboard/index.js'); - const instance = new KeyboardNavigation(); + const instance = new KeyboardNavigation(newMobileWithLnav); resolve(instance); }); }; @@ -331,10 +331,13 @@ class Gnav { this.ims, this.addChangeEventListeners, ]; + const fetchKeyboardNav = () => { + setupKeyboardNav(this.newMobileNav && this.isLocalNav()); + }; this.block.addEventListener('click', this.loadDelayed); - this.block.addEventListener('keydown', setupKeyboardNav); + this.block.addEventListener('keydown', fetchKeyboardNav); setTimeout(this.loadDelayed, CONFIG.delays.loadDelayed); - setTimeout(setupKeyboardNav, CONFIG.delays.keyboardNav); + setTimeout(fetchKeyboardNav, CONFIG.delays.keyboardNav); for await (const task of tasks) { await yieldToMain(); await task(); @@ -375,38 +378,50 @@ class Gnav { if (!this.isLocalNav()) return; const localNavItems = this.elements.navWrapper.querySelector('.feds-nav').querySelectorAll('.feds-navItem:not(.feds-navItem--section)'); const [title, navTitle = ''] = this.getOriginalTitle(localNavItems); + let localNav = document.querySelector('.feds-localnav'); + if (!localNav) { + lanaLog({ message: 'GNAV: Localnav does not include \'localnav\' in its name.', tags: 'errorType=info,module=gnav' }); + localNav = toFragment`
`; + this.block.after(localNav); + } + localNav.append(toFragment``, toFragment`
`, toFragment`
`, toFragment`.`); - if (this.elements.localNav || !this.newMobileNav || isDesktop.matches) { - localNavItems[0].querySelector('a').textContent = title.trim(); - } else { - let localNav = document.querySelector('.feds-localnav'); - if (!localNav) { - lanaLog({ message: 'GNAV: Localnav does not include \'localnav\' in its name.', tags: 'errorType=info,module=gnav' }); - localNav = toFragment`
`; - this.block.after(localNav); - } - localNav.append(toFragment``, toFragment`
`, toFragment`
`); - - const itemWrapper = localNav.querySelector('.feds-localnav-items'); - const titleLabel = await replaceKey('overview', getFedsPlaceholderConfig()); + const itemWrapper = localNav.querySelector('.feds-localnav-items'); + const titleLabel = await replaceKey('overview', getFedsPlaceholderConfig()); - localNavItems.forEach((elem, idx) => { - const clonedItem = elem.cloneNode(true); - const link = clonedItem.querySelector('a'); + localNavItems.forEach((elem, idx) => { + const clonedItem = elem.cloneNode(true); + const link = clonedItem.querySelector('a'); - if (idx === 0) { - localNav.querySelector('.feds-localnav-title').innerText = title.trim(); - link.textContent = navTitle.trim() || titleLabel; - } + if (idx === 0) { + localNav.querySelector('.feds-localnav-title').innerText = title.trim(); + link.textContent = navTitle.trim() || titleLabel; + } - itemWrapper.appendChild(clonedItem); - }); + itemWrapper.appendChild(clonedItem); + }); - localNav.querySelector('.feds-localnav-title').addEventListener('click', () => { - localNav.classList.toggle('active'); - }); - this.elements.localNav = localNav; - } + localNav.querySelector('.feds-localnav-title').addEventListener('click', () => { + localNav.classList.toggle('active'); + const isActive = localNav.classList.contains('active'); + localNav.querySelector('.feds-localnav-title').setAttribute('aria-expanded', isActive); + }); + this.elements.localNav = localNav; + localNavItems[0].querySelector('a').textContent = title.trim(); + const isAtTop = () => { + const rect = this.elements.localNav.getBoundingClientRect(); + return rect.top === 0; + }; + window.addEventListener('scroll', () => { + const classList = this.elements.localNav?.classList; + if (isAtTop()) { + if (!classList?.contains('is-sticky')) { + classList?.add('is-sticky'); + } + } else { + classList?.remove('is-sticky'); + } + }); }; decorateTopnavWrapper = async () => { @@ -423,7 +438,9 @@ class Gnav { this.elements.topnavWrapper, ); - this.decorateLocalNav(); + if (this.newMobileNav) { + await this.decorateLocalNav(); + } }; addChangeEventListeners = () => { @@ -454,7 +471,6 @@ class Gnav { this.elements.navWrapper.prepend(this.elements.breadcrumbsWrapper); } } - this.decorateLocalNav(); }); // Add a modifier when the nav is tangent to the viewport and content is partly hidden @@ -1003,6 +1019,11 @@ class Gnav { elements.querySelectorAll('.feds-menu-headline').forEach((elem) => { // Reattach click event listener to headlines + elem?.setAttribute('role', 'button'); + elem?.setAttribute('tabindex', 0); + elem?.removeAttribute('aria-level'); + elem?.setAttribute('aria-haspopup', true); + elem?.setAttribute('aria-expanded', false); elem?.addEventListener('click', (e) => { trigger({ element: e.currentTarget, event: e, type: 'headline' }); setActiveDropdown(e.currentTarget); diff --git a/libs/blocks/global-navigation/utilities/keyboard/index.js b/libs/blocks/global-navigation/utilities/keyboard/index.js index 204bac3f598..174095108d8 100644 --- a/libs/blocks/global-navigation/utilities/keyboard/index.js +++ b/libs/blocks/global-navigation/utilities/keyboard/index.js @@ -1,7 +1,7 @@ /* eslint-disable class-methods-use-this */ import { getNextVisibleItemPosition, getPreviousVisibleItemPosition, selectors } from './utils.js'; import MainNav from './mainNav.js'; -import { closeAllDropdowns, lanaLog, logErrorFor } from '../utilities.js'; +import { closeAllDropdowns, lanaLog, logErrorFor, loadBlock } from '../utilities.js'; const cycleOnOpenSearch = ({ e, isDesktop }) => { const withoutBreadcrumbs = [ @@ -73,16 +73,28 @@ const focusPrevProfileItem = ({ e }) => { }; class KeyboardNavigation { - constructor() { + constructor(newNavWithLnav) { try { this.addEventListeners(); this.mainNav = new MainNav(); + if (newNavWithLnav) { + this.loadLnavNavigation(); + } this.desktop = window.matchMedia('(min-width: 900px)'); } catch (e) { lanaLog({ message: 'Keyboard Navigation failed to load', e, tags: 'errorType=error,module=gnav-keyboard' }); } } + loadLnavNavigation = async () => { + this.localNav = this.localNav || new Promise((resolve) => { + loadBlock('./keyboard/localNav.js').then((LnavNavigation) => { + const instance = new LnavNavigation(); + resolve(instance); + }); + }); + }; + addEventListeners = () => { [...document.querySelectorAll(`${selectors.globalNav}, ${selectors.globalFooter}`)] .forEach((el) => { diff --git a/libs/blocks/global-navigation/utilities/keyboard/localNav.js b/libs/blocks/global-navigation/utilities/keyboard/localNav.js new file mode 100644 index 00000000000..a161dc6323a --- /dev/null +++ b/libs/blocks/global-navigation/utilities/keyboard/localNav.js @@ -0,0 +1,38 @@ +import { selectors } from './utils.js'; +import { trigger, setActiveDropdown } from '../utilities.js'; + +class LocalNavItem { + constructor() { + this.localNav = document.querySelector(selectors.localNav); + this.localNavTrigger = this.localNav?.querySelector(selectors.localNavTitle); + this.exitLink = this.localNav?.querySelector(selectors.localNavExit); + this.addEventListeners(); + } + + static handleKeyDown = (e) => { + const isHeadline = e.target.classList.contains(selectors.headline.slice(1)); + switch (e.code) { + case 'Space': + case 'Enter': + if (isHeadline) { + e.preventDefault(); // Prevent default scrolling behavior for Space key + trigger({ element: e.target, event: e, type: 'headline' }); + setActiveDropdown(e.target); + } + break; + default: + break; + } + }; + + addEventListeners = () => { + this.localNav?.addEventListener('keydown', LocalNavItem.handleKeyDown); + this.exitLink?.addEventListener('focus', (e) => { + e.preventDefault(); + this.localNavTrigger?.click(); + this.localNavTrigger?.focus(); + }); + }; +} + +export default LocalNavItem; diff --git a/libs/blocks/global-navigation/utilities/keyboard/utils.js b/libs/blocks/global-navigation/utilities/keyboard/utils.js index 72d984f5bc2..40df16b67ed 100644 --- a/libs/blocks/global-navigation/utilities/keyboard/utils.js +++ b/libs/blocks/global-navigation/utilities/keyboard/utils.js @@ -38,6 +38,9 @@ const selectors = { activeTabpanel: '.tab-content [role="tabpanel"]', activeLinks: '.tab-content [role="tabpanel"]:not([hidden="true"]) a', stickyCta: 'header.new-nav .feds-popup .sticky-cta a', + localNav: '.feds-localnav', + localNavTitle: '.feds-localnav-title', + localNavExit: '.feds-localnav-exit', }; selectors.profileDropdown = ` diff --git a/libs/blocks/global-navigation/utilities/utilities.js b/libs/blocks/global-navigation/utilities/utilities.js index abf8881d9c8..6b961443df6 100644 --- a/libs/blocks/global-navigation/utilities/utilities.js +++ b/libs/blocks/global-navigation/utilities/utilities.js @@ -418,9 +418,9 @@ export const [setUserProfile, getUserProfile] = (() => { export const transformTemplateToMobile = async (popup, item, localnav = false) => { const notMegaMenu = popup.parentElement.tagName === 'DIV'; - if (notMegaMenu) return null; - const originalContent = popup.innerHTML; + if (notMegaMenu) return originalContent; + const tabs = [...popup.querySelectorAll('.feds-menu-section')] .filter((section) => !section.querySelector('.feds-promo') && section.textContent) .map((section) => { diff --git a/libs/styles/styles.css b/libs/styles/styles.css index c90f72f9079..dfaab2db8f0 100644 --- a/libs/styles/styles.css +++ b/libs/styles/styles.css @@ -6,6 +6,7 @@ /* stylelint-disable-next-line custom-property-pattern */ --global-height-navPromo: 72px; --feds-totalheight-nav: calc(var(--feds-height-nav, --global-height-nav) + var(--feds-height-breadcrumbs, --global-height-breadcrumbs)); + --feds-localnav-height: 40px; /* Colors */ --link-color: #035fe6; @@ -724,14 +725,31 @@ header.global-navigation a { text-decoration: unset; } -.feds-localnav { - height: 40px; +header.global-navigation + .feds-localnav { + display: block; + height: var(--feds-localnav-height); +} + +.feds-sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; } @media (min-width: 900px) { header.global-navigation.has-breadcrumbs { padding-bottom: var(--global-height-breadcrumbs); } + + header.global-navigation + .feds-localnav { + display: none; + } } .breadcrumbs {