Skip to content

Commit

Permalink
LYNX-760: initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
bl4de committed Feb 17, 2025
1 parent 3df82c1 commit 2abd09d
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 114 deletions.
12 changes: 6 additions & 6 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ module.exports = {
'linebreak-style': ['error', 'unix'], // enforce unix linebreaks
'no-param-reassign': [2, { props: false }], // allow modifying properties of param
'no-use-before-define': [2, { functions: false }],
'no-console': [
'error',
{
allow: ['warn', 'error', 'info', 'debug'],
},
],
// 'no-console': [
// 'error',
// {
// allow: ['warn', 'error', 'info', 'debug'],
// },
// ],
'no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
},
};
119 changes: 13 additions & 106 deletions blocks/header/header.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,109 +12,12 @@ import { loadFragment } from '../fragment/fragment.js';
import renderAuthCombine from './renderAuthCombine.js';
import { renderAuthDropdown } from './renderAuthDropdown.js';

// media query match that indicates mobile/tablet width
const isDesktop = window.matchMedia('(min-width: 900px)');

function closeOnEscape(e) {
if (e.code === 'Escape') {
const nav = document.getElementById('nav');
const navSections = nav.querySelector('.nav-sections');
const navSectionExpanded = navSections.querySelector('[aria-expanded="true"]');
if (navSectionExpanded && isDesktop.matches) {
// eslint-disable-next-line no-use-before-define
toggleAllNavSections(navSections);
navSectionExpanded.focus();
} else if (!isDesktop.matches) {
// eslint-disable-next-line no-use-before-define
toggleMenu(nav, navSections);
nav.querySelector('button').focus();
}
}
}

function closeOnFocusLost(e) {
const nav = e.currentTarget;
if (!nav.contains(e.relatedTarget)) {
const navSections = nav.querySelector('.nav-sections');
const navSectionExpanded = navSections.querySelector('[aria-expanded="true"]');
if (navSectionExpanded && isDesktop.matches) {
// eslint-disable-next-line no-use-before-define
toggleAllNavSections(navSections, false);
} else if (!isDesktop.matches) {
// eslint-disable-next-line no-use-before-define
toggleMenu(nav, navSections, false);
}
}
}

function openOnKeydown(e) {
const focused = document.activeElement;
const isNavDrop = focused.className === 'nav-drop';
if (isNavDrop && (e.code === 'Enter' || e.code === 'Space')) {
const dropExpanded = focused.getAttribute('aria-expanded') === 'true';
// eslint-disable-next-line no-use-before-define
toggleAllNavSections(focused.closest('.nav-sections'));
focused.setAttribute('aria-expanded', dropExpanded ? 'false' : 'true');
}
}

function focusNavSection() {
document.activeElement.addEventListener('keydown', openOnKeydown);
}

/**
* Toggles all nav sections
* @param {Element} sections The container element
* @param {Boolean} expanded Whether the element should be expanded or collapsed
*/
function toggleAllNavSections(sections, expanded = false) {
sections
.querySelectorAll('.nav-sections .default-content-wrapper > ul > li')
.forEach((section) => {
section.setAttribute('aria-expanded', expanded);
});
}

/**
* Toggles the entire nav
* @param {Element} nav The container element
* @param {Element} navSections The nav sections within the container element
* @param {*} forceExpanded Optional param to force nav expand behavior when not null
*/
function toggleMenu(nav, navSections, forceExpanded = null) {
const expanded = forceExpanded !== null ? !forceExpanded : nav.getAttribute('aria-expanded') === 'true';
const button = nav.querySelector('.nav-hamburger button');
document.body.style.overflowY = expanded || isDesktop.matches ? '' : 'hidden';
nav.setAttribute('aria-expanded', expanded ? 'false' : 'true');
toggleAllNavSections(navSections, expanded || isDesktop.matches ? 'false' : 'true');
button.setAttribute('aria-label', expanded ? 'Open navigation' : 'Close navigation');
// enable nav dropdown keyboard accessibility
const navDrops = navSections.querySelectorAll('.nav-drop');
if (isDesktop.matches) {
navDrops.forEach((drop) => {
if (!drop.hasAttribute('tabindex')) {
drop.setAttribute('tabindex', 0);
drop.addEventListener('focus', focusNavSection);
}
});
} else {
navDrops.forEach((drop) => {
drop.removeAttribute('tabindex');
drop.removeEventListener('focus', focusNavSection);
});
}

// enable menu collapse on escape keypress
if (!expanded || isDesktop.matches) {
// collapse menu on escape press
window.addEventListener('keydown', closeOnEscape);
// collapse menu on focus lost
nav.addEventListener('focusout', closeOnFocusLost);
} else {
window.removeEventListener('keydown', closeOnEscape);
nav.removeEventListener('focusout', closeOnFocusLost);
}
}
import {
isDesktop,
parseNavSections,
toggleAllNavSections,
toggleMenu,
} from './menu/menu.js';

/**
* loads and decorates the header, mainly the nav
Expand Down Expand Up @@ -147,10 +50,14 @@ export default async function decorate(block) {

const navSections = nav.querySelector('.nav-sections');
if (navSections) {
navSections
.querySelectorAll(':scope .default-content-wrapper > ul > li')
const visibleSections = parseNavSections(navSections);

visibleSections
.forEach((navSection) => {
if (navSection.querySelector('ul')) navSection.classList.add('nav-drop');
if (navSection.querySelector('ul')) {
navSection.classList.add('nav-drop');
}

navSection.addEventListener('click', () => {
if (isDesktop.matches) {
const expanded = navSection.getAttribute('aria-expanded') === 'true';
Expand Down
39 changes: 39 additions & 0 deletions blocks/header/menu/hashTagParser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Parses href hash
*
* @param urlHash
* @returns {*[]}
*/
const namespaces = [
'display_for_',
'apply_style_',
];

function parseUrlHashTag(href) {
if (!href.indexOf('#') > 0) {
return;
}

const parsed = [];
const hashTags = href.split('#').slice(1);

if (hashTags) {
namespaces.forEach((ns) => {
hashTags.forEach((tag) => {
const value = tag.split(ns);
if (value && value.length === 2) {
parsed.push({
namespace: ns,
value: value[1],
});
}
});
});
}

return parsed;

Check failure on line 34 in blocks/header/menu/hashTagParser.js

View workflow job for this annotation

GitHub Actions / build

Function 'parseUrlHashTag' expected no return value
}

export {
parseUrlHashTag,

Check failure on line 38 in blocks/header/menu/hashTagParser.js

View workflow job for this annotation

GitHub Actions / build

Prefer default export on a file with single export
};
164 changes: 164 additions & 0 deletions blocks/header/menu/menu.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { parseUrlHashTag } from './hashTagParser.js';

// media query match that indicates mobile/tablet width
const isDesktop = window.matchMedia('(min-width: 900px)');

function closeOnEscape(e) {
if (e.code === 'Escape') {
const nav = document.getElementById('nav');
const navSections = nav.querySelector('.nav-sections');
const navSectionExpanded = navSections.querySelector('[aria-expanded="true"]');
if (navSectionExpanded && isDesktop.matches) {
// eslint-disable-next-line no-use-before-define
toggleAllNavSections(navSections);
navSectionExpanded.focus();
} else if (!isDesktop.matches) {
// eslint-disable-next-line no-use-before-define
toggleMenu(nav, navSections);
nav.querySelector('button').focus();
}
}
}

function closeOnFocusLost(e) {
const nav = e.currentTarget;
if (!nav.contains(e.relatedTarget)) {
const navSections = nav.querySelector('.nav-sections');
const navSectionExpanded = navSections.querySelector('[aria-expanded="true"]');
if (navSectionExpanded && isDesktop.matches) {
// eslint-disable-next-line no-use-before-define
toggleAllNavSections(navSections, false);
} else if (!isDesktop.matches) {
// eslint-disable-next-line no-use-before-define
toggleMenu(nav, navSections, false);
}
}
}

function openOnKeydown(e) {
const focused = document.activeElement;
const isNavDrop = focused.className === 'nav-drop';
if (isNavDrop && (e.code === 'Enter' || e.code === 'Space')) {
const dropExpanded = focused.getAttribute('aria-expanded') === 'true';
// eslint-disable-next-line no-use-before-define
toggleAllNavSections(focused.closest('.nav-sections'));
focused.setAttribute('aria-expanded', dropExpanded ? 'false' : 'true');
}
}

function focusNavSection() {
document.activeElement.addEventListener('keydown', openOnKeydown);
}

/**
* Recursively gets all LI elements from menu section
*
* @param menu
* @param elements
* @returns {*[]}
*/
function getLiElements(menu, elements = []) {
const uls = menu.querySelectorAll('ul');

/*
@TODO: handle nested ul > li > ul; makes sure it wont get into infinite loop; optimization (?);
*/
uls.forEach((ul) => {
ul.childNodes.forEach((n) => {
if (n.nodeName === 'ul') {
getLiElements(n, elements);
}
// if (n.nodeName === 'li' && n.querySelectorAll('ul').length > 0) {
// getLiElements(n.querySelectorAll('ul'), elements);
// }
if (n.nodeType !== 3) {
elements.push(n);
}
});
});
return elements;
}

/**
* Parses nav fragment; disable visible elements based on hash tags
*
* @param navSections
* @returns {*}
*/
function parseNavSections(navSections) {
const visibleMenuItems = navSections.querySelectorAll(':scope .default-content-wrapper > ul > li');

visibleMenuItems.forEach((section) => {
const liElements = getLiElements(section);
liElements.forEach((liElement) => {
const link = liElement.querySelector('a') && liElement.querySelector('a').href;
if (link) {
const hashTags = parseUrlHashTag(link);
console.table(`Hash tags for ${link}`, hashTags);

Check warning on line 97 in blocks/header/menu/menu.js

View workflow job for this annotation

GitHub Actions / build

Unexpected console statement
// liElement.parentNode.removeChild(liElement);
}
});
});
return visibleMenuItems;
}

/**
* Toggles all nav sections
* @param {Element} sections The container element
* @param {Boolean} expanded Whether the element should be expanded or collapsed
*/
function toggleAllNavSections(sections, expanded = false) {
sections
.querySelectorAll('.nav-sections .default-content-wrapper > ul > li')
.forEach((section) => {
section.setAttribute('aria-expanded', expanded);
});
}

/**
* Toggles the entire nav
* @param {Element} nav The container element
* @param {Element} navSections The nav sections within the container element
* @param {*} forceExpanded Optional param to force nav expand behavior when not null
*/
function toggleMenu(nav, navSections, forceExpanded = null) {
const expanded = forceExpanded !== null ? !forceExpanded : nav.getAttribute('aria-expanded') === 'true';
const button = nav.querySelector('.nav-hamburger button');
document.body.style.overflowY = expanded || isDesktop.matches ? '' : 'hidden';
nav.setAttribute('aria-expanded', expanded ? 'false' : 'true');
toggleAllNavSections(navSections, expanded || isDesktop.matches ? 'false' : 'true');
button.setAttribute('aria-label', expanded ? 'Open navigation' : 'Close navigation');
// enable nav dropdown keyboard accessibility
const navDrops = navSections.querySelectorAll('.nav-drop');
if (isDesktop.matches) {
navDrops.forEach((drop) => {
if (!drop.hasAttribute('tabindex')) {
drop.setAttribute('tabindex', 0);
drop.addEventListener('focus', focusNavSection);
}
});
} else {
navDrops.forEach((drop) => {
drop.removeAttribute('tabindex');
drop.removeEventListener('focus', focusNavSection);
});
}

// enable menu collapse on escape keypress
if (!expanded || isDesktop.matches) {
// collapse menu on escape press
window.addEventListener('keydown', closeOnEscape);
// collapse menu on focus lost
nav.addEventListener('focusout', closeOnFocusLost);
} else {
window.removeEventListener('keydown', closeOnEscape);
nav.removeEventListener('focusout', closeOnFocusLost);
}
}

export {
isDesktop,
parseNavSections,
toggleAllNavSections,
toggleMenu,
};
4 changes: 2 additions & 2 deletions fstab.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
mountpoints:
/: https://adobe.sharepoint.com/:f:/r/sites/HelixProjects/Shared%20Documents/sites/starter-content-commerce
/: https://da.live/#/sirugh/rafals-storefront-repo

folders:
/products/: /products/default
/products/: /products/default

0 comments on commit 2abd09d

Please sign in to comment.