Skip to content

Commit

Permalink
Keyboard navigation for mobile gnav redesign (#3158)
Browse files Browse the repository at this point in the history
* Keyboard navigation for mobile gnav redesign

* close main menu on escape

* fixed lint issue
  • Loading branch information
sharmrj authored and bandana147 committed Jan 7, 2025
1 parent c95e7a6 commit d3847da
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 13 deletions.
22 changes: 22 additions & 0 deletions libs/blocks/global-navigation/utilities/keyboard/mainNav.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,24 @@ class MainNavItem {
return;
}

const newNav = !!document.querySelector('header.new-nav');

switch (e.code) {
case 'Tab': {
if (newNav) {
const activePopup = document.querySelector(selectors.activePopup);
if (!activePopup) e.preventDefault();
const items = [...document.querySelectorAll(`${selectors.mainMenuItems}, ${selectors.mainMenuLinks}`)];
const current = items.findIndex((x) => x === e.target);
if (current > -1) {
const next = current < items.length - 1 ? current + 1 : 0;
const prev = current > 0 ? current - 1 : items.length - 1;
if (e.shiftKey) items[prev].focus();
else items[next].focus();
} else items?.[0]?.focus();
break;
}

if (e.shiftKey) {
const { prev, openTrigger } = this.getState();
if (openTrigger) {
Expand All @@ -36,9 +52,12 @@ class MainNavItem {
}
case 'Escape': {
closeAllDropdowns();
const activePopup = document.querySelector(selectors.activePopup);
if (newNav && !activePopup) document.querySelector('header.new-nav .feds-toggle').click();
break;
}
case 'ArrowLeft': {
if (newNav) break;
const { next, prev } = this.getState();
if (document.dir !== 'rtl') {
if (prev === -1) break;
Expand All @@ -50,12 +69,14 @@ class MainNavItem {
break;
}
case 'ArrowUp': {
if (newNav) break;
e.preventDefault();
e.stopPropagation();
this.focusPrev({ focus: 'last' });
break;
}
case 'ArrowRight': {
if (newNav) break;
const { next, prev, openTrigger } = this.getState();
if (document.dir !== 'rtl') {
if (next === -1) break;
Expand All @@ -70,6 +91,7 @@ class MainNavItem {
break;
}
case 'ArrowDown': {
if (newNav) break;
e.stopPropagation();
e.preventDefault();
const { items, curr } = this.getState();
Expand Down
55 changes: 43 additions & 12 deletions libs/blocks/global-navigation/utilities/keyboard/mobilePopup.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
getOpenPopup,
selectors,
} from './utils.js';
import { closeAllDropdowns, logErrorFor, setActiveDropdown } from '../utilities.js';
import { closeAllDropdowns, dropWhile, logErrorFor, setActiveDropdown, takeWhile } from '../utilities.js';

const closeHeadlines = () => {
const open = [...document.querySelectorAll(`${selectors.headline}[aria-expanded="true"]`)];
Expand Down Expand Up @@ -83,44 +83,50 @@ class Popup {
setActiveDropdown(focus === 'first' ? popupItems[first] : popupItems[last]);
}

mobileArrowUp = ({ prev, curr, element, isFooter }) => {
mobileArrowUp = ({ prev, curr, element, isFooter, newNav }) => {
// Case 1: Move focus to the previous item
const state = getState(element);
const { currentSection } = state;
const popupItems = newNav ? this.popupItems() : state.popupItems;
if (prev !== -1 && curr - 1 === prev) {
const { currentSection, popupItems } = getState(element);
popupItems[prev].focus();
if (currentSection !== getState(element).currentSection) closeHeadlines();
return;
}

// Case 2: No headline + no previous item, move to the main nav
const { prevHeadline } = getState(element);
if (!prevHeadline) {
if (!prevHeadline && !newNav) {
this.focusMainNav(isFooter);
return;
}

// Case 3: Open the previous headline
openHeadline({ headline: prevHeadline, focus: 'last' });
if (newNav) popupItems?.[popupItems.length - 1]?.focus();
else openHeadline({ headline: prevHeadline, focus: 'last' });
};

mobileArrowDown = ({ next, element, isFooter }) => {
mobileArrowDown = ({ next, element, isFooter, newNav }) => {
// Case 1: Move focus to the next item
const state = getState(element);
const { currentSection } = state;
const popupItems = newNav ? this.popupItems() : state.popupItems;
if (next !== -1) {
const { currentSection, popupItems } = getState(element);
popupItems[next].focus();
if (currentSection !== getState(element).currentSection) closeHeadlines();
return;
}
// Case 2: No headline + no next item, move to the main nav
const { nextHeadline } = getState(element);
if (!nextHeadline) {
if (!nextHeadline && !newNav) {
closeHeadlines();
this.focusMainNavNext(isFooter);
return;
}

// Case 3: Open the next headline
openHeadline({ headline: nextHeadline, focus: 'first' });
if (newNav) popupItems?.[0]?.focus();
else openHeadline({ headline: nextHeadline, focus: 'first' });
};

focusMainNav = (isFooter) => {
Expand All @@ -134,8 +140,29 @@ class Popup {
this.mainNav.open();
};

popupItems = () => {
const activePopup = document.querySelector(selectors.activePopup);
if (!activePopup) return [];
const tabs = [...activePopup.querySelectorAll(selectors.tab)];
const activeTab = tabs.find((tab) => tab.getAttribute('aria-selected') === 'true');
const anteActiveTab = takeWhile(tabs, (tab) => tab !== activeTab);
const postActiveTab = dropWhile(tabs, (tab) => tab !== activeTab).slice(1);
const activeLinks = [...activePopup.querySelectorAll(selectors.activeLinks)];
const stickyCTA = activePopup.querySelector(selectors.stickyCta);
return [
...anteActiveTab,
activeTab,
...activeLinks,
stickyCTA,
...postActiveTab,
].filter(Boolean);
};

handleKeyDown = ({ e, element, isFooter }) => {
const popupItems = [...element.querySelectorAll(selectors.popupItems)];
const newNav = !!document.querySelector('header.new-nav');
const popupItems = newNav
? this.popupItems()
: [...element.querySelectorAll(selectors.popupItems)];
const curr = popupItems.findIndex((el) => el === e.target);
const prev = getPreviousVisibleItemPosition(curr, popupItems);
const next = getNextVisibleItemPosition(curr, popupItems);
Expand All @@ -146,9 +173,9 @@ class Popup {
switch (e.code) {
case 'Tab': {
if (e.shiftKey) {
this.mobileArrowUp({ prev, curr, element, isFooter });
this.mobileArrowUp({ prev, curr, element, isFooter, newNav });
} else {
this.mobileArrowDown({ curr, next, element, isFooter });
this.mobileArrowDown({ curr, next, element, isFooter, newNav });
}
break;
}
Expand All @@ -158,6 +185,7 @@ class Popup {
break;
}
case 'ArrowLeft': {
if (newNav) break;
const { prevHeadline, nextHeadline } = getState(element);
const headline = document.dir !== 'rtl' ? prevHeadline : nextHeadline;
if (!headline) {
Expand All @@ -173,10 +201,12 @@ class Popup {
break;
}
case 'ArrowUp': {
if (newNav) break;
this.mobileArrowUp({ prev, curr, element, isFooter });
break;
}
case 'ArrowRight': {
if (newNav) break;
const { prevHeadline, nextHeadline } = getState(element);
const headline = document.dir !== 'rtl' ? nextHeadline : prevHeadline;
if (!headline) {
Expand All @@ -192,6 +222,7 @@ class Popup {
break;
}
case 'ArrowDown': {
if (newNav) break;
this.mobileArrowDown({ next, element, isFooter });
break;
}
Expand Down
8 changes: 8 additions & 0 deletions libs/blocks/global-navigation/utilities/keyboard/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ const selectors = {
socialLink: '.feds-social-link',
privacyLink: '.feds-footer-privacyLink',
menuContent: '.feds-menu-content',
/* mobile redesign popup selectors */
mainMenuItems: 'header.new-nav section.feds-navItem > button',
mainMenuLinks: ' header.new-nav feds-navItem > a',
activePopup: 'header.new-nav section.feds-dropdown--active > .feds-popup',
tab: 'button[role="tab"]',
activeTabpanel: '.tab-content [role="tabpanel"]',
activeLinks: '.tab-content [role="tabpanel"]:not([hidden="true"]) a',
stickyCta: 'header.new-nav .feds-popup .sticky-cta a',
};

selectors.profileDropdown = `
Expand Down
17 changes: 16 additions & 1 deletion libs/blocks/global-navigation/utilities/utilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ export const transformTemplateToMobile = async (popup, item, localnav = false) =
</span>
</div>
<div class="title">
${breadCrumbs || `<div class="breadcrumbs"></div>`}
${breadCrumbs || '<div class="breadcrumbs"></div>'}
<h7>${item.textContent.trim()}</h7>
</div>
<div class="tabs" role="tablist">
Expand Down Expand Up @@ -472,3 +472,18 @@ export const transformTemplateToMobile = async (popup, item, localnav = false) =
});
return originalContent;
};

export const takeWhile = (xs, f) => {
const r = [];
for (let i = 0; i < xs.length; i += 1) {
if (!f(xs[i])) return r;
r.push(xs[i]);
}
return r;
};

export const dropWhile = (xs, f) => {
if (!xs.length) return xs;
if (f(xs[0])) return dropWhile(xs.slice(1), f);
return xs;
};

0 comments on commit d3847da

Please sign in to comment.