From bd0d1f248a87e6c27a5c91a08a338fa53e14ee7e Mon Sep 17 00:00:00 2001 From: Florin Raducan Date: Fri, 23 Aug 2024 10:37:45 +0300 Subject: [PATCH 1/2] #514117 - Fix navigation and footer --- blocks/accordion/accordion.js | 14 +- blocks/footer/footer.js | 2 +- blocks/navigation/navigation.css | 31 +++-- blocks/navigation/navigation.js | 226 ++++++++++++++++++++++++------- blocks/popup/popup.js | 13 +- scripts/component-base.js | 8 +- scripts/libs.js | 8 ++ styles/styles.css | 6 +- 8 files changed, 230 insertions(+), 78 deletions(-) diff --git a/blocks/accordion/accordion.js b/blocks/accordion/accordion.js index 98dd613c..bb7ce8ed 100644 --- a/blocks/accordion/accordion.js +++ b/blocks/accordion/accordion.js @@ -34,10 +34,18 @@ export default class Accordion extends ComponentBase { this.setupContent(children.filter((_, ind) => ind % 2 === 1)); } + createIcon(elem) { + const icon = document.createElement('raqn-icon'); + icon.dataset.icon = 'chevron-right'; + + const hasIcon = elem?.querySelectorAll(`raqn-icon[data-icon="${icon.dataset.icon}"]`)?.length; + if (!hasIcon) { + elem.append(icon); + } + } + setupControls(controls) { controls.forEach((control, index) => { - const icon = document.createElement('raqn-icon'); - icon.dataset.icon = 'chevron-right'; const children = Array.from(control.children); if (children.length === 0) { const child = document.createElement('span'); @@ -45,7 +53,7 @@ export default class Accordion extends ComponentBase { control.innerHTML = ''; control.append(child); } - control.children[0].append(icon); + this.createIcon(control.children[0]); control.setAttribute('role', 'button'); control.setAttribute('aria-expanded', 'false'); control.setAttribute('tabindex', '0'); diff --git a/blocks/footer/footer.js b/blocks/footer/footer.js index ec0a61a1..b1ffc13d 100644 --- a/blocks/footer/footer.js +++ b/blocks/footer/footer.js @@ -28,7 +28,7 @@ export default class Footer extends ComponentBase { if (!child) return; child.replaceWith(...child.children); this.nav = this.querySelector('ul'); - this.nav.setAttribute('role', 'navigation'); + this.nav?.setAttribute('role', 'navigation'); this.classList.add('full-width'); this.classList.add('horizontal'); } diff --git a/blocks/navigation/navigation.css b/blocks/navigation/navigation.css index 4f999d7f..0890d919 100644 --- a/blocks/navigation/navigation.css +++ b/blocks/navigation/navigation.css @@ -16,6 +16,10 @@ raqn-navigation > nav p { padding: 0; } +raqn-navigation > nav ul { + list-style: none; +} + raqn-navigation .level-1 a:not(:hover) { color: var(--accent-background, #000); } @@ -34,6 +38,10 @@ raqn-navigation.active > nav p { display: block; } +raqn-navigation > nav a { + display: inline-block; +} + raqn-navigation.active > nav a { display: inline-flex; align-items: center; @@ -73,7 +81,6 @@ raqn-navigation.active button { raqn-navigation.active > nav > ul { position: fixed; display: block; - list-style: none; max-width: 0; background: var(--background, #fff); min-width: 100%; @@ -82,12 +89,9 @@ raqn-navigation.active > nav > ul { height: 100%; max-height: calc(100vh - var(--header-height, 64px)); margin: 0 auto; - padding: 0; -} - -raqn-navigation.active > nav > ul li { - max-width: var(--max-width, 100%); - margin: 0 auto; + padding-block: 0; + padding-inline: var(--container-width); + overflow-y: auto; } raqn-navigation.active > nav > ul li a { @@ -105,6 +109,8 @@ raqn-navigation:not([data-compact='true']) > nav a { raqn-navigation:not([data-compact='true']) > nav ul { list-style: none; display: flex; + column-gap: var(--padding-vertical, 40px); + margin: 0; } raqn-navigation:not([data-compact='true']) > nav > ul { @@ -120,8 +126,8 @@ raqn-navigation:not([data-compact='true']) > nav [data-icon='chevron-right'] { transform: rotate(90deg); } -raqn-navigation:not([data-compact='true']) > nav .level-1 a { - padding: var(--padding-vertical, 10px) var(--padding-horizontal, 20px); +raqn-navigation:not([data-compact='true']) > nav :where(.level-1, .level-2) > a { + padding-block: var(--padding-horizontal, 20px); } raqn-navigation:not([data-compact='true']) > nav .level-2 > a { @@ -150,9 +156,11 @@ raqn-navigation:not([data-compact='true']) > nav .level-1 > ul { position: absolute; padding: 0; inset-block-start: var(--header-height, 64px); - inset-inline-start: calc((100vw - var(--max-width)) / 2); + inset-inline-start: 0; + width: 100%; transition: clip-path 0.4s ease-in-out; overflow: visible; + padding-inline: var(--container-width); } raqn-navigation:not([data-compact='true']) > nav .level-1 > ul .level-2 { @@ -164,10 +172,9 @@ raqn-navigation:not([data-compact='true']) > nav .level-1 > ul .level-2 { raqn-navigation:not([data-compact='true']) > nav .level-1 > ul::after { content: ' '; - margin-inline: calc(-1 * ((100vw - var(--max-width)) / 2)); position: absolute; height: 100%; - width: 100vw; + width: 100%; inset-inline-start: 0; background: var(--background, #fff); border-block-start: 1px solid var(--accent-background, #000); diff --git a/blocks/navigation/navigation.js b/blocks/navigation/navigation.js index bd18bfd5..95e72eaf 100644 --- a/blocks/navigation/navigation.js +++ b/blocks/navigation/navigation.js @@ -1,88 +1,218 @@ +import component from '../../scripts/init.js'; +import { blockBodyScroll } from '../../scripts/libs.js'; import Column from '../column/column.js'; export default class Navigation extends Column { - static observedAttributes = ['data-icon', 'data-compact', 'data-justify']; + static observedAttributes = ['data-menu-icon', 'data-item-icon', 'data-compact', ...Column.observedAttributes]; dependencies = ['icon', 'accordion']; - createButton() { - const button = document.createElement('button'); - button.setAttribute('aria-label', 'Menu'); - button.setAttribute('aria-expanded', 'false'); - button.setAttribute('aria-controls', 'navigation'); - button.setAttribute('aria-haspopup', 'true'); - button.setAttribute('type', 'button'); - button.setAttribute('tabindex', '0'); - button.innerHTML = ''; - button.addEventListener('click', () => { - this.classList.toggle('active'); - button.setAttribute('aria-expanded', this.classList.contains('active')); - }); - return button; - } + attributesValues = { + all: { + data: { + menu: { + icon: 'menu__close', + }, + item: { + icon: 'chevron-right', + }, + }, + }, + m: { + data: { + compact: true, + }, + }, + x: { + data: { + compact: true, + }, + }, + xs: { + data: { + compact: true, + }, + }, + }; - ready() { + setDefaults() { + super.setDefaults(); this.active = {}; - this.list = this.querySelector('ul'); + this.isActive = false; + this.navContentInit = false; + this.navCompactedContentInit = false; + } + + async ready() { + this.navContent = this.querySelector('ul'); + this.innerHTML = ''; + this.navCompactedContent = this.navContent.cloneNode(true); // the clone need to be done before `this.navContent` is modified this.nav = document.createElement('nav'); - this.nav.append(this.list); - this.setAttribute('role', 'navigation'); - this.compact = this.getAttribute('compact') === 'true' || false; - this.icon = this.getAttribute('icon') || 'menu'; - if (this.compact) { - this.nav.append(this.createButton()); - } + this.isCompact = this.dataset.compact === 'true'; this.append(this.nav); - this.setupClasses(this.list); - if (this.compact) { - this.addEventListener('click', (e) => this.activate(e)); + this.nav.setAttribute('role', 'navigation'); + this.nav.setAttribute('id', 'navigation'); + + if (this.isCompact) { + await this.setupCompactedNav(); + } else { + this.setupNav(); + } + } + + setupNav() { + if (!this.navContentInit) { + this.navContentInit = true; + this.setupClasses(this.navContent); } + this.navButton?.remove(); + this.nav.append(this.navContent); } - createIcon(name = this.icon) { + async setupCompactedNav() { + if (!this.navCompactedContentInit) { + this.navCompactedContentInit = true; + await component.multiLoadAndDefine(['accordion', 'icon']); + this.setupClasses(this.navCompactedContent, true); + this.navCompactedContent.addEventListener('click', (e) => this.activate(e)); + } + + this.prepend(this.createButton()); + this.nav.append(this.navCompactedContent); + } + + onAttributeCompactChanged({ oldValue, newValue }) { + if (!this.initialized) return; + if (oldValue === newValue) return; + this.isCompact = newValue === 'true'; + this.nav.innerHTML = ''; + + if (this.isCompact) { + this.setupCompactedNav(); + } else { + if (this.navButton) { + this.isActive = false; + this.classList.remove('active'); + this.navButton.removeAttribute('aria-expanded'); + this.navIcon.dataset.active = this.isActive; + this.closeAllLevels(); + } + this.setupNav(); + } + } + + onAttributeIconChanged({ newValue }) { + if (!this.initialized) return; + if (!this.isCompact) return; + this.navIcon.dataset.icon = newValue; + } + + createButton() { + this.navButton = document.createElement('button'); + this.navButton.setAttribute('aria-label', 'Menu'); + this.navButton.setAttribute('aria-expanded', 'false'); + this.navButton.setAttribute('aria-controls', 'navigation'); + this.navButton.setAttribute('aria-haspopup', 'true'); + this.navButton.setAttribute('type', 'button'); + this.navButton.innerHTML = ``; + this.navIcon = this.navButton.querySelector('raqn-icon'); + this.navButton.addEventListener('click', () => { + this.isActive = !this.isActive; + this.classList.toggle('active'); + this.navButton.setAttribute('aria-expanded', this.isActive); + this.navIcon.dataset.active = this.isActive; + blockBodyScroll(this.isActive); + this.closeAllLevels(); + }); + return this.navButton; + } + + addIcon(elem) { const icon = document.createElement('raqn-icon'); - icon.setAttribute('icon', name); - return icon; + icon.dataset.icon = this.dataset.itemIcon; + elem.append(icon); } - creaeteAccordion(replaceChildrenElement) { + createAccordionOld(elem) { + component.init({ + componentName: 'accordion', + targets: [elem], + componentConfig: { + addToTargetMethod: 'append', + }, + nestedComponentsConfig: { + button: { active: false }, + }, + }); + } + + createAccordion(replaceChildrenElement) { const accordion = document.createElement('raqn-accordion'); - const children = Array.from(replaceChildrenElement.children); - accordion.append(...children); + accordion.append(...replaceChildrenElement.childNodes); replaceChildrenElement.append(accordion); } - setupClasses(ul, level = 1) { + setupClasses(ul, isCompact, level = 1) { const children = Array.from(ul.children); - children.forEach((child) => { + + children.forEach(async (child) => { const hasChildren = child.querySelector('ul'); child.classList.add(`level-${level}`); child.dataset.level = level; if (hasChildren) { - const anchor = child.querySelector('a'); - if (this.compact) { - this.creaeteAccordion(child); + if (isCompact) { + this.createAccordion(child); } else if (level === 1) { - anchor.append(this.createIcon('chevron-right')); + const anchor = child.querySelector('a'); + + this.addIcon(anchor); } child.classList.add('has-children'); - this.setupClasses(hasChildren, level + 1); + this.setupClasses(hasChildren, isCompact, level + 1); } }); } activate(e) { - e.preventDefault(); - if (e.target.tagName.toLowerCase() === 'a') { + if (e.target.tagName.toLowerCase() === 'raqn-icon' || e.target.closest('raqn-icon')) { + e.preventDefault(); + const current = e.target.closest('li'); const { level } = current.dataset; - if (this.active[level] && this.active[level] !== current) { - this.active[level].classList.remove('active'); + const currentLevel = Number(level); + const activeLevel = Number(this.getAttribute('active')); + const isCurrentLevel = this.active[currentLevel] && this.active[currentLevel] === current; + const hasActiveChildren = currentLevel < activeLevel; + + if (!isCurrentLevel || hasActiveChildren) { + const whileCurrentLevel = isCurrentLevel && hasActiveChildren ? currentLevel + 1 : currentLevel; + this.closeLevels(activeLevel, whileCurrentLevel); } - this.active[level] = current; - this.setAttribute('active', level); - this.active[level].classList.toggle('active'); + + this.setAttribute('active', isCurrentLevel ? Math.max(0, currentLevel - 1) || '' : currentLevel); + this.active[currentLevel]?.classList.toggle('active'); + this.active[currentLevel] = isCurrentLevel ? null : current; + } + } + + closeLevels(activeLevel, currentLevel = 1) { + let whileCurrentLevel = currentLevel; + while (whileCurrentLevel <= activeLevel) { + this.active[whileCurrentLevel].classList.remove('active'); + const accordion = this.active[whileCurrentLevel].querySelector('raqn-accordion'); + const control = accordion.querySelector('.accordion-control'); + accordion.toggleControl(control); + this.active[whileCurrentLevel] = null; + whileCurrentLevel += 1; + } + } + + closeAllLevels() { + const activeLevel = Number(this.getAttribute('active')); + if (activeLevel) { + this.closeLevels(activeLevel); + this.removeAttribute('active'); } } } diff --git a/blocks/popup/popup.js b/blocks/popup/popup.js index 11560e19..df7515e7 100644 --- a/blocks/popup/popup.js +++ b/blocks/popup/popup.js @@ -5,6 +5,7 @@ import { popupState, focusTrap, focusFirstElementInContainer, + blockBodyScroll, } from '../../scripts/libs.js'; /** @@ -46,7 +47,6 @@ export default class Popup extends ComponentBase { classes: { popupClosing: 'popup__base--closing', popupFlyout: 'popup__base--flyout', - noScroll: 'no-scroll', hide: 'hide', }, }, @@ -129,7 +129,7 @@ export default class Popup extends ComponentBase { popupState.closeActivePopup(); popupState.activePopup = this; - this.blockBodyScroll(true); + blockBodyScroll(true); this.showPopup(true); this.toggleCloseOnEsc(true); focusFirstElementInContainer(this.elements.popupContainer); @@ -215,7 +215,7 @@ export default class Popup extends ComponentBase { closePopup() { popupState.activePopup = null; - this.blockBodyScroll(false); + blockBodyScroll(false); this.updatePopupTrigger(false); this.toggleCloseOnEsc(false); this.classList.add('popup--closing'); @@ -230,7 +230,7 @@ export default class Popup extends ComponentBase { popupState.closeActivePopup(); popupState.activePopup = this; - this.blockBodyScroll(true); + blockBodyScroll(true); await this.addFragmentContent(); this.setInnerBlocks(); await this.initChildComponents(); @@ -259,11 +259,6 @@ export default class Popup extends ComponentBase { if (this.popupTrigger) this.popupTrigger.dataset.active = isActive; } - blockBodyScroll(boolean) { - const { noScroll } = this.config.classes; - document.body.classList.toggle(noScroll, boolean); - } - showPopup(boolean) { const { hide } = this.config.classes; this.classList.toggle(hide, !boolean); diff --git a/scripts/component-base.js b/scripts/component-base.js index 3a5036c9..d7203a20 100644 --- a/scripts/component-base.js +++ b/scripts/component-base.js @@ -129,6 +129,7 @@ export default class ComponentBase extends HTMLElement { // ! Needs to be called after the element is created; async init(initOptions) { try { + await this.Handler; this.wasInitBeforeConnected = true; this.initOptions = initOptions || {}; this.setInitialAttributesValues(); @@ -153,7 +154,7 @@ export default class ComponentBase extends HTMLElement { * When the element was created with data attributes before the ini() method is called * use the data attr values as default for attributesValues */ - setInitialAttributesValues() { + async setInitialAttributesValues() { const initialAttributesValues = { all: {} }; this.Handler.observedAttributes.map((dataAttr) => { @@ -221,6 +222,7 @@ export default class ComponentBase extends HTMLElement { async initOnConnected() { if (this.wasInitBeforeConnected) return; + await this.Handler; this.setInitialAttributesValues(); await this.buildExternalConfig(); this.runConfigsByViewport(); @@ -305,7 +307,7 @@ export default class ComponentBase extends HTMLElement { runConfigsByViewport() { const { name } = getBreakPoints().active; const current = deepMerge({}, this.attributesValues.all, this.attributesValues[name]); - this.className = ''; + this.removeAttribute('class'); Object.keys(current).forEach((key) => { const action = `apply${key.charAt(0).toUpperCase() + key.slice(1)}`; if (typeof this[action] === 'function') { @@ -344,7 +346,7 @@ export default class ComponentBase extends HTMLElement { if (isObject(className)) { // if an object is passed, it's flat and splited this.classList.add(...flatAsValue(className).split(' ')); - } else { + } else if (className) { // strings are added as is this.setAttribute('class', className); } diff --git a/scripts/libs.js b/scripts/libs.js index 520e20c3..11c0be2b 100644 --- a/scripts/libs.js +++ b/scripts/libs.js @@ -14,6 +14,9 @@ export const globalConfig = { medium: 500, bold: 700, }, + classes: { + noScroll: 'no-scroll', + }, }; export const metaTags = { @@ -467,3 +470,8 @@ export const classToFlat = (classes = [], valueLength = 1, extend = {}) => return acc; }, extend), ); + +export function blockBodyScroll(boolean) { + const { noScroll } = globalConfig.classes; + document.body.classList.toggle(noScroll, boolean); +} diff --git a/styles/styles.css b/styles/styles.css index b0bd7387..743467c6 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -2,6 +2,7 @@ --max-width: 80%; --padding-container: 20px; --header-height: 110px; + --container-width: max(calc((100% - var(--max-width)) / 2), var(--padding-container)); } @media screen and (max-width: 768px) { @@ -182,7 +183,7 @@ main > div > *:not(.full-width) { } .full-width { - padding-inline: max(calc((100% - var(--max-width)) / 2), var(--padding-container)); + padding-inline: var(--container-width); } main > div > div { @@ -236,7 +237,8 @@ button { } .raqn-grid { - width: var(--max-width, 100%); + width: 100%; + padding-inline: var(--container-width); margin: 0 auto; display: grid; grid-template-columns: var(--grid-template-columns, 1fr); From 658eb1010ccd9924bc250ee309adbf61fc6793c1 Mon Sep 17 00:00:00 2001 From: Florin Raducan Date: Fri, 23 Aug 2024 11:02:09 +0300 Subject: [PATCH 2/2] #514117 - code cleanup --- blocks/navigation/navigation.js | 24 +++++++----------------- scripts/component-base.js | 2 +- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/blocks/navigation/navigation.js b/blocks/navigation/navigation.js index 95e72eaf..9aecf808 100644 --- a/blocks/navigation/navigation.js +++ b/blocks/navigation/navigation.js @@ -133,19 +133,6 @@ export default class Navigation extends Column { elem.append(icon); } - createAccordionOld(elem) { - component.init({ - componentName: 'accordion', - targets: [elem], - componentConfig: { - addToTargetMethod: 'append', - }, - nestedComponentsConfig: { - button: { active: false }, - }, - }); - } - createAccordion(replaceChildrenElement) { const accordion = document.createElement('raqn-accordion'); accordion.append(...replaceChildrenElement.childNodes); @@ -182,7 +169,8 @@ export default class Navigation extends Column { const { level } = current.dataset; const currentLevel = Number(level); const activeLevel = Number(this.getAttribute('active')); - const isCurrentLevel = this.active[currentLevel] && this.active[currentLevel] === current; + const activeElem = this.active[currentLevel]; + const isCurrentLevel = activeElem && activeElem === current; const hasActiveChildren = currentLevel < activeLevel; if (!isCurrentLevel || hasActiveChildren) { @@ -191,7 +179,7 @@ export default class Navigation extends Column { } this.setAttribute('active', isCurrentLevel ? Math.max(0, currentLevel - 1) || '' : currentLevel); - this.active[currentLevel]?.classList.toggle('active'); + activeElem?.classList.toggle('active'); this.active[currentLevel] = isCurrentLevel ? null : current; } } @@ -199,8 +187,10 @@ export default class Navigation extends Column { closeLevels(activeLevel, currentLevel = 1) { let whileCurrentLevel = currentLevel; while (whileCurrentLevel <= activeLevel) { - this.active[whileCurrentLevel].classList.remove('active'); - const accordion = this.active[whileCurrentLevel].querySelector('raqn-accordion'); + const activeElem = this.active[currentLevel]; + + activeElem.classList.remove('active'); + const accordion = activeElem.querySelector('raqn-accordion'); const control = accordion.querySelector('.accordion-control'); accordion.toggleControl(control); this.active[whileCurrentLevel] = null; diff --git a/scripts/component-base.js b/scripts/component-base.js index d7203a20..5582c745 100644 --- a/scripts/component-base.js +++ b/scripts/component-base.js @@ -154,7 +154,7 @@ export default class ComponentBase extends HTMLElement { * When the element was created with data attributes before the ini() method is called * use the data attr values as default for attributesValues */ - async setInitialAttributesValues() { + setInitialAttributesValues() { const initialAttributesValues = { all: {} }; this.Handler.observedAttributes.map((dataAttr) => {