From 3468ccb76e53e399ae733804a8fd19c783bc4808 Mon Sep 17 00:00:00 2001 From: Laura Jove Date: Wed, 30 Aug 2023 17:17:13 +0200 Subject: [PATCH 01/11] inpage navigation block --- .../v2-inpage-navigation.css | 180 ++++++++++++++++++ .../v2-inpage-navigation.js | 146 ++++++++++++++ scripts/scripts.js | 89 +++++++++ 3 files changed, 415 insertions(+) create mode 100644 blocks/v2-inpage-navigation/v2-inpage-navigation.css create mode 100644 blocks/v2-inpage-navigation/v2-inpage-navigation.js diff --git a/blocks/v2-inpage-navigation/v2-inpage-navigation.css b/blocks/v2-inpage-navigation/v2-inpage-navigation.css new file mode 100644 index 000000000..6dcc2da74 --- /dev/null +++ b/blocks/v2-inpage-navigation/v2-inpage-navigation.css @@ -0,0 +1,180 @@ +.v2-inpage-navigation-wrapper { + background-color: var(--c-primary-white); + left: 0; + position: sticky; + top: var(--nav-height); + width: 100%; + z-index: 2; +} + +.v2-inpage-navigation__wrapper { + box-shadow: 0 4px 24px 0 rgb(0 0 0 / 16%); + display: flex; + margin: 0 auto; + max-width: var(--wrapper-width); +} + +.v2-inpage-navigation__dropdown { + flex-grow: 1; + position: relative; +} + +.v2-inpage-navigation__items { + background-color: var(--c-primary-white); + box-shadow: 0 4px 24px 0 rgb(0 0 0 / 16%); + display: none; + left: 0; + list-style: none; + margin: 0; + padding: 0; + position: absolute; + top: 100%; + width: 100%; +} + +.v2-inpage-navigation__item--active { + display: none; +} + +.v2-inpage-navigation__item button, +.v2-inpage-navigation__selected-item-wrapper { + background: none; + border: 0; + color: var(--c-primary-black); + cursor: pointer; + display: block; + font-family: var(--ff-body-bold); + font-size: var(--body-2-font-size); + line-height: var(--body-2-line-height); + margin: 0; + padding: 14px 24px; + width: 100%; +} + +.v2-inpage-navigation__item button:hover, +.v2-inpage-navigation__selected-item-wrapper:hover { + background-color: #F1F1F1; +} + +.v2-inpage-navigation__item button:active, +.v2-inpage-navigation__selected-item-wrapper:active { + background-color: #F1F1F1; +} + +/* stylelint-disable-next-line no-descending-specificity */ +.v2-inpage-navigation__selected-item-wrapper svg { + --color-icon: var(--c-accent-red); + + height: 16px; + transition: transform var(--duration-small) var(--easing-standard); + width: 16px; +} + +/* Customization when dropdown is open */ +.v2-inpage-navigation__dropdown--open .v2-inpage-navigation__items { + display: block; +} + +.v2-inpage-navigation__dropdown--open .v2-inpage-navigation__selected-item-wrapper { + background-color: #F1F1F1; +} + +.v2-inpage-navigation__dropdown--open .v2-inpage-navigation__selected-item-wrapper svg { + transform: rotate(180deg); +} + +/* END Customization when dropdown is open */ + +/* Red button */ +.v2-inpage-navigation__cta:any-link { + align-items: center; + background-color: var(--button-primary-red-enabled); + color: var(--c-primary-white); + display: flex; + font-family: var(--ff-body); + font-size: 14px; + font-style: normal; + font-weight: 500; + letter-spacing: 1.12px; + line-height: 18px; + padding: 0 20; + text-decoration: none; +} + +.v2-inpage-navigation__cta:hover, +.v2-inpage-navigation__cta:focus { + background-color: var(--button-primary-red-hover); +} + +.v2-inpage-navigation__cta:active { + background-color: var(--button-primary-red-pressed); +} + +.v2-inpage-navigation__cta--desktop { + display: none; +} + +@media (min-width: 1200) { + .v2-inpage-navigation__wrapper { + box-shadow: none; + gap: 24px; + align-items: center; + } + + .v2-inpage-navigation__selected-item-wrapper { + display: none; + } + + .v2-inpage-navigation__items, + .v2-inpage-navigation__dropdown--open .v2-inpage-navigation__items { + display: flex; + } + + /* stylelint-disable-next-line no-descending-specificity */ + .v2-inpage-navigation__items { + box-shadow: none; + gap: 24px; + justify-content: space-between; + position: unset; + } + + .v2-inpage-navigation__item--active { + display: block; + } + + .v2-inpage-navigation__item button { + padding: 10 0; + position: relative; + } + + .v2-inpage-navigation__item button:hover { + background: none; + } + + .v2-inpage-navigation__item button::after { + bottom: 0; + content: ''; + display: block; + height: 4px; + position: absolute; + width: 100%; + } + + .v2-inpage-navigation__item--active button::after, + .v2-inpage-navigation__item button:hover::after { + background-color: var(--c-accent-red); + } + + /* Red button */ + .v2-inpage-navigation__cta:any-link { + padding: 15px 20; + } + + .v2-inpage-navigation__cta--mobile { + display: none; + } + + .v2-inpage-navigation__cta--desktop { + display: block; + } +} diff --git a/blocks/v2-inpage-navigation/v2-inpage-navigation.js b/blocks/v2-inpage-navigation/v2-inpage-navigation.js new file mode 100644 index 000000000..3dd0bf41f --- /dev/null +++ b/blocks/v2-inpage-navigation/v2-inpage-navigation.js @@ -0,0 +1,146 @@ +import { + getMetadata, +} from '../../scripts/lib-franklin.js'; +import { + createElement, +} from '../../scripts/common.js'; + +const blockName = 'v2-inpage-navigation'; + +const getInpageNavigationButtons = () => { + // if we have a button title & button link + if (getMetadata('inpage-button') && getMetadata('inpage-link')) { + const titleMobile = getMetadata('inpage-button'); + const url = getMetadata('inpage-link'); + const link = createElement('a', { + classes: `${blockName}__cta`, + props: { + href: url, + title: titleMobile, + }, + }); + const mobileText = createElement('span', { classes: `${blockName}__cta--mobile` }); + mobileText.textContent = titleMobile; + link.appendChild(mobileText); + + const titleDesktop = getMetadata('inpage-button-large'); + if (titleDesktop) { + const desktopText = createElement('span', { classes: `${blockName}__cta--desktop` }); + desktopText.textContent = titleDesktop; + link.setAttribute('title', titleDesktop); + link.appendChild(desktopText); + } + + return link; + } + + return []; +}; + +const gotoSection = (event) => { + // let waitingTime = 500; + const { target } = event; + const button = target.closest('button'); + + if (button) { + const { id } = button.dataset; + + const container = document.querySelector(`main .section[data-inpageid='${id}']`); + container?.scrollIntoView({ behavior: 'smooth' }); + + // // create an Observer instance + // const resizeObserver = new ResizeObserver((entries) => { + // console.log('Body height changed:', entries[0].target.clientHeight); + // }); + + // // start observing a DOM node + // resizeObserver.observe(document.body); + } +}; + +const updateActive = (id) => { + // console.log('updateActive', id); + const currentItem = document.querySelector(`.${blockName}__selected-item`); + const listItems = document.querySelector(`.${blockName}__item--active`); + listItems.classList.remove(`${blockName}__item--active`); + const itemsButton = document.querySelectorAll(`.${blockName}__items button`); + + const selectedButton = [...itemsButton].filter((button) => button.dataset.id === id); + if (!selectedButton[0]) return; + currentItem.textContent = selectedButton[0].textContent; + selectedButton[0].parentNode.classList.add(`${blockName}__item--active`); +}; + +const listenScroll = () => { + // const main = document.querySelector('main'); + const elements = document.querySelectorAll('main .section[data-inpageid]'); + + const io = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + // console.log(entry.intersectionRatio, entry.target.dataset.inpageid, entry.target); + updateActive(entry.target.dataset.inpageid); + } + }); + }, { + // root: main, + threshold: [0.2, 0.5, 0.7, 1], + }); + + elements.forEach((el) => { + io.observe(el); + }); +}; + +export default async function decorate(block) { + const buttons = getInpageNavigationButtons(); + + const wrapper = block.querySelector(':scope > div'); + wrapper.classList.add(`${blockName}__wrapper`); + const itemsWrapper = block.querySelector(':scope > div > div'); + + const dropdownWrapper = createElement('div', { classes: `${blockName}__dropdown` }); + const selectedItemWrapper = createElement('div', { classes: `${blockName}__selected-item-wrapper` }); + const selectedItem = createElement('div', { classes: `${blockName}__selected-item` }); + + const list = createElement('ul', { classes: `${blockName}__items` }); + + [...itemsWrapper.children].forEach((item, index) => { + const classes = [`${blockName}__item`]; + if (index === 0) { + classes.push(`${blockName}__item--active`); + selectedItem.textContent = item.textContent; + } + const listItem = createElement('li', { classes }); + + listItem.innerHTML = item.innerHTML; + list.appendChild(listItem); + }); + + const dropdownArrowIcon = document.createRange().createContextualFragment(` + `); + selectedItemWrapper.append(selectedItem); + selectedItemWrapper.appendChild(...dropdownArrowIcon.children); + + dropdownWrapper.append(selectedItemWrapper); + dropdownWrapper.append(list); + wrapper.append(dropdownWrapper); + + itemsWrapper.remove(); + + wrapper.appendChild(buttons); + + list.addEventListener('click', gotoSection); + + // Listener to toggle the dropdown or close it + document.addEventListener('click', (e) => { + if (e.target.closest(`.${blockName}__selected-item-wrapper`)) { + dropdownWrapper.classList.toggle(`${blockName}__dropdown--open`); + } else { + dropdownWrapper.classList.remove(`${blockName}__dropdown--open`); + } + }); + + // listen scroll to change the url + listenScroll(); +} diff --git a/scripts/scripts.js b/scripts/scripts.js index 576f609da..ae95b917c 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -228,6 +228,92 @@ export function decorateLinks(block) { }); } +const slugify = (text) => ( + text.toString().toLowerCase().trim() + // separate accent from letter + .normalize('NFD') + // remove all separated accents + .replace(/[\u0300-\u036f]/g, '') + // replace spaces with - + .replace(/\s+/g, '-') + // replace & with 'and' + .replace(/&/g, '-and-') + // remove all non-word chars + .replace(/[^\w-]+/g, '') + // replace multiple '-' with single '-' + .replace(/--+/g, '-') +); + +const createInpageNavigation = (main) => { + const navItems = []; + const tabItemsObj = []; + + // Extract the inpage navigation info from sections + [...main.querySelectorAll(':scope > div')].forEach((section) => { + const title = section.dataset.inpage; + if (title) { + const countDuplcated = tabItemsObj.filter((item) => item.title === title)?.length || 0; + const order = section.dataset.inpageOrder; + const anchorID = (countDuplcated > 0) ? slugify(`${section.dataset.inpage}-${countDuplcated}`) : slugify(section.dataset.inpage); + const obj = { + title, + id: anchorID, + }; + + if (order) { + obj.order = parseFloat(section.dataset.subnavOrder); + } + + tabItemsObj.push(obj); + + // Set section with ID + section.dataset.inpageid = anchorID; + } + }); + + // Sort the object by order + const sortedObject = tabItemsObj.slice().sort((obj1, obj2) => { + if (obj1.order === null || obj1.order === undefined) { + return 1; // Move 'a' to the end + } + if (obj2.order === null || obj2.order === undefined) { + return -1; // Move 'b' to the end + } + return obj1.order - obj2.order; // Compare by order values + }); + + // From the array of objects create the DOM + sortedObject.forEach((item) => { + const subnavItem = createElement('div'); + const subnavLink = createElement('button', { + props: { + 'data-id': item.id, + title: item.title, + }, + }); + + subnavLink.textContent = item.title; + + subnavItem.append(subnavLink); + navItems.push(subnavItem); + }); + + return navItems; +}; + +function buildInpageNavigationBlock(main) { + const inapgeClassName = 'v2-inpage-navigation'; + + const items = createInpageNavigation(main); + + if (items.length > 0) { + const section = createElement('div'); + section.append(buildBlock(inapgeClassName, { elems: items })); + main.prepend(section); + decorateBlock(section.querySelector(`.${inapgeClassName}`)); + } +} + /** * Decorates the main element. * @param {Element} main The main element @@ -241,6 +327,9 @@ export function decorateMain(main, head) { decorateSections(main); decorateBlocks(main); decorateLinks(main); + + // Inpage navigation + buildInpageNavigationBlock(main); } async function loadTemplate(doc, templateName) { From f48aeb244f44137fbc4d19801af3f3e62a12dcb6 Mon Sep 17 00:00:00 2001 From: Laura Jove Date: Wed, 30 Aug 2023 17:52:29 +0200 Subject: [PATCH 02/11] fix issues --- .../v2-inpage-navigation.css | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/blocks/v2-inpage-navigation/v2-inpage-navigation.css b/blocks/v2-inpage-navigation/v2-inpage-navigation.css index 6dcc2da74..fbfc340d8 100644 --- a/blocks/v2-inpage-navigation/v2-inpage-navigation.css +++ b/blocks/v2-inpage-navigation/v2-inpage-navigation.css @@ -8,10 +8,11 @@ } .v2-inpage-navigation__wrapper { + --wrapper-margin: 0 auto; + box-shadow: 0 4px 24px 0 rgb(0 0 0 / 16%); display: flex; - margin: 0 auto; - max-width: var(--wrapper-width); + margin: var(--wrapper-margin); } .v2-inpage-navigation__dropdown { @@ -62,6 +63,19 @@ } /* stylelint-disable-next-line no-descending-specificity */ +.v2-inpage-navigation__item button { + max-width: none; + text-align: left; + width: 100%; +} + +/* stylelint-disable-next-line no-descending-specificity */ +.v2-inpage-navigation__selected-item-wrapper { + display: flex; + justify-content: space-between; + align-items: center; +} + .v2-inpage-navigation__selected-item-wrapper svg { --color-icon: var(--c-accent-red); @@ -97,7 +111,7 @@ font-weight: 500; letter-spacing: 1.12px; line-height: 18px; - padding: 0 20; + padding: 0 20px; text-decoration: none; } @@ -114,11 +128,14 @@ display: none; } -@media (min-width: 1200) { +@media (min-width: 1200px) { .v2-inpage-navigation__wrapper { + --wrapper-margin: 24px auto; + + align-items: center; box-shadow: none; gap: 24px; - align-items: center; + max-width: var(--wrapper-width); } .v2-inpage-navigation__selected-item-wrapper { @@ -143,7 +160,7 @@ } .v2-inpage-navigation__item button { - padding: 10 0; + padding: 10px 0; position: relative; } @@ -167,7 +184,7 @@ /* Red button */ .v2-inpage-navigation__cta:any-link { - padding: 15px 20; + padding: 15px 20px; } .v2-inpage-navigation__cta--mobile { From 8400b1f8a38bedeab3db12274c98599f1a031f91 Mon Sep 17 00:00:00 2001 From: Laura Jove Date: Wed, 30 Aug 2023 18:00:40 +0200 Subject: [PATCH 03/11] fix shadow issues --- blocks/v2-inpage-navigation/v2-inpage-navigation.css | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/blocks/v2-inpage-navigation/v2-inpage-navigation.css b/blocks/v2-inpage-navigation/v2-inpage-navigation.css index fbfc340d8..e3242a95c 100644 --- a/blocks/v2-inpage-navigation/v2-inpage-navigation.css +++ b/blocks/v2-inpage-navigation/v2-inpage-navigation.css @@ -1,4 +1,5 @@ .v2-inpage-navigation-wrapper { + box-shadow: 0 4px 24px 0 rgb(0 0 0 / 16%); background-color: var(--c-primary-white); left: 0; position: sticky; @@ -8,11 +9,8 @@ } .v2-inpage-navigation__wrapper { - --wrapper-margin: 0 auto; - - box-shadow: 0 4px 24px 0 rgb(0 0 0 / 16%); display: flex; - margin: var(--wrapper-margin); + margin: 0 auto; } .v2-inpage-navigation__dropdown { @@ -31,6 +29,7 @@ position: absolute; top: 100%; width: 100%; + z-index: -1; } .v2-inpage-navigation__item--active { @@ -130,12 +129,10 @@ @media (min-width: 1200px) { .v2-inpage-navigation__wrapper { - --wrapper-margin: 24px auto; - align-items: center; - box-shadow: none; gap: 24px; max-width: var(--wrapper-width); + padding: 24px 0; } .v2-inpage-navigation__selected-item-wrapper { From 1758bdb5477e042f0b60d4090361a5c0ca4e2b62 Mon Sep 17 00:00:00 2001 From: Laura Jove Date: Wed, 30 Aug 2023 18:35:16 +0200 Subject: [PATCH 04/11] modify history --- blocks/v2-inpage-navigation/v2-inpage-navigation.js | 11 ++++++++++- scripts/scripts.js | 4 +++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/blocks/v2-inpage-navigation/v2-inpage-navigation.js b/blocks/v2-inpage-navigation/v2-inpage-navigation.js index 3dd0bf41f..7519863a8 100644 --- a/blocks/v2-inpage-navigation/v2-inpage-navigation.js +++ b/blocks/v2-inpage-navigation/v2-inpage-navigation.js @@ -69,17 +69,26 @@ const updateActive = (id) => { if (!selectedButton[0]) return; currentItem.textContent = selectedButton[0].textContent; selectedButton[0].parentNode.classList.add(`${blockName}__item--active`); + + const { pathname } = window.location; + window.history.replaceState({}, '', `${pathname}#${id}`); }; const listenScroll = () => { // const main = document.querySelector('main'); + let timeout; const elements = document.querySelectorAll('main .section[data-inpageid]'); const io = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { // console.log(entry.intersectionRatio, entry.target.dataset.inpageid, entry.target); - updateActive(entry.target.dataset.inpageid); + clearTimeout(timeout); + + // wait to update the active item + timeout = setTimeout(() => { + updateActive(entry.target.dataset.inpageid); + }, 500); } }); }, { diff --git a/scripts/scripts.js b/scripts/scripts.js index ae95b917c..b653f695d 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -309,7 +309,9 @@ function buildInpageNavigationBlock(main) { if (items.length > 0) { const section = createElement('div'); section.append(buildBlock(inapgeClassName, { elems: items })); - main.prepend(section); + // insert in second position, assumption is that Hero should be first + main.insertBefore(section, main.children[1]); + decorateBlock(section.querySelector(`.${inapgeClassName}`)); } } From 484e12bb2c3d1040f7049dbf78c0e71fd82a2ae6 Mon Sep 17 00:00:00 2001 From: Laura Jove Date: Thu, 31 Aug 2023 08:37:35 +0200 Subject: [PATCH 05/11] inpage navigation --- .../v2-inpage-navigation.css | 18 +++- .../v2-inpage-navigation.js | 102 +++++++++++------- scripts/common.js | 16 +++ scripts/scripts.js | 27 ++--- styles/styles.css | 4 + test/scripts/common.test.js | 36 +++++++ 6 files changed, 138 insertions(+), 65 deletions(-) diff --git a/blocks/v2-inpage-navigation/v2-inpage-navigation.css b/blocks/v2-inpage-navigation/v2-inpage-navigation.css index e3242a95c..31d005c37 100644 --- a/blocks/v2-inpage-navigation/v2-inpage-navigation.css +++ b/blocks/v2-inpage-navigation/v2-inpage-navigation.css @@ -1,3 +1,7 @@ +:root { + --inpage-navigation: 48px; +} + .v2-inpage-navigation-wrapper { box-shadow: 0 4px 24px 0 rgb(0 0 0 / 16%); background-color: var(--c-primary-white); @@ -52,7 +56,9 @@ } .v2-inpage-navigation__item button:hover, -.v2-inpage-navigation__selected-item-wrapper:hover { +.v2-inpage-navigation__item button:focus, +.v2-inpage-navigation__selected-item-wrapper:hover, +.v2-inpage-navigation__selected-item-wrapper:focus { background-color: #F1F1F1; } @@ -128,6 +134,10 @@ } @media (min-width: 1200px) { + :root { + --inpage-navigation: 96px; + } + .v2-inpage-navigation__wrapper { align-items: center; gap: 24px; @@ -161,7 +171,8 @@ position: relative; } - .v2-inpage-navigation__item button:hover { + .v2-inpage-navigation__item button:hover, + .v2-inpage-navigation__item button:focus { background: none; } @@ -175,7 +186,8 @@ } .v2-inpage-navigation__item--active button::after, - .v2-inpage-navigation__item button:hover::after { + .v2-inpage-navigation__item button:hover::after, + .v2-inpage-navigation__item button:focus::after { background-color: var(--c-accent-red); } diff --git a/blocks/v2-inpage-navigation/v2-inpage-navigation.js b/blocks/v2-inpage-navigation/v2-inpage-navigation.js index 7519863a8..cce57e5d7 100644 --- a/blocks/v2-inpage-navigation/v2-inpage-navigation.js +++ b/blocks/v2-inpage-navigation/v2-inpage-navigation.js @@ -1,12 +1,27 @@ -import { - getMetadata, -} from '../../scripts/lib-franklin.js'; -import { - createElement, -} from '../../scripts/common.js'; +import { getMetadata } from '../../scripts/lib-franklin.js'; +import { createElement } from '../../scripts/common.js'; const blockName = 'v2-inpage-navigation'; +const scrollToSection = (id) => { + let timeout; + + const container = document.querySelector(`main .section[data-inpageid='${id}']`); + container?.scrollIntoView({ behavior: 'smooth' }); + + // Checking if the height of the main element changes while scrolling (caused by layout shift) + const main = document.querySelector('main'); + const resizeObserver = new ResizeObserver(() => { + clearTimeout(timeout); + container?.scrollIntoView({ behavior: 'smooth' }); + + timeout = setTimeout(() => { + resizeObserver.disconnect(); + }, 500); + }); + resizeObserver.observe(main); +}; + const getInpageNavigationButtons = () => { // if we have a button title & button link if (getMetadata('inpage-button') && getMetadata('inpage-link')) { @@ -38,61 +53,56 @@ const getInpageNavigationButtons = () => { }; const gotoSection = (event) => { - // let waitingTime = 500; const { target } = event; const button = target.closest('button'); if (button) { const { id } = button.dataset; - const container = document.querySelector(`main .section[data-inpageid='${id}']`); - container?.scrollIntoView({ behavior: 'smooth' }); - - // // create an Observer instance - // const resizeObserver = new ResizeObserver((entries) => { - // console.log('Body height changed:', entries[0].target.clientHeight); - // }); - - // // start observing a DOM node - // resizeObserver.observe(document.body); + scrollToSection(id); } }; const updateActive = (id) => { - // console.log('updateActive', id); const currentItem = document.querySelector(`.${blockName}__selected-item`); const listItems = document.querySelector(`.${blockName}__item--active`); - listItems.classList.remove(`${blockName}__item--active`); + listItems?.classList.remove(`${blockName}__item--active`); const itemsButton = document.querySelectorAll(`.${blockName}__items button`); + const { pathname } = window.location; - const selectedButton = [...itemsButton].filter((button) => button.dataset.id === id); - if (!selectedButton[0]) return; - currentItem.textContent = selectedButton[0].textContent; - selectedButton[0].parentNode.classList.add(`${blockName}__item--active`); + if (id) { + const selectedButton = [...itemsButton].filter((button) => button.dataset.id === id); + if (!selectedButton[0]) return; + currentItem.textContent = selectedButton[0].textContent; + selectedButton[0].parentNode.classList.add(`${blockName}__item--active`); - const { pathname } = window.location; - window.history.replaceState({}, '', `${pathname}#${id}`); + window.history.replaceState({}, '', `${pathname}#${id}`); + } else { + window.history.replaceState({}, '', `${pathname}`); + } }; const listenScroll = () => { - // const main = document.querySelector('main'); let timeout; - const elements = document.querySelectorAll('main .section[data-inpageid]'); + const elements = document.querySelectorAll('main .section'); const io = new IntersectionObserver((entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - // console.log(entry.intersectionRatio, entry.target.dataset.inpageid, entry.target); - clearTimeout(timeout); - - // wait to update the active item - timeout = setTimeout(() => { - updateActive(entry.target.dataset.inpageid); - }, 500); - } - }); + // Reduce entries to the one with higher intersectionRatio + const intersectedEntry = entries.reduce((prev, current) => ( + prev.intersectionRatio > current.intersectionRatio ? prev : current + )); + + if (intersectedEntry.isIntersecting && intersectedEntry.target.dataset?.inpageid) { + clearTimeout(timeout); + + // wait to update the active item + timeout = setTimeout(() => { + updateActive(intersectedEntry.target.dataset.inpageid); + }, 500); + } else { + updateActive(); + } }, { - // root: main, threshold: [0.2, 0.5, 0.7, 1], }); @@ -116,7 +126,7 @@ export default async function decorate(block) { [...itemsWrapper.children].forEach((item, index) => { const classes = [`${blockName}__item`]; - if (index === 0) { + if (index === 0) { // Default selected item classes.push(`${blockName}__item--active`); selectedItem.textContent = item.textContent; } @@ -141,7 +151,17 @@ export default async function decorate(block) { list.addEventListener('click', gotoSection); - // Listener to toggle the dropdown or close it + // on load Go to section if defined + const hash = window.location.hash.substring(1); + if (hash) { + updateActive(hash); + + setTimeout(() => { + scrollToSection(hash); + }, 1000); + } + + // Listener to toggle the dropdown (open / close) document.addEventListener('click', (e) => { if (e.target.closest(`.${blockName}__selected-item-wrapper`)) { dropdownWrapper.classList.toggle(`${blockName}__dropdown--open`); diff --git a/scripts/common.js b/scripts/common.js index 1bd72c262..0e082fd41 100644 --- a/scripts/common.js +++ b/scripts/common.js @@ -146,3 +146,19 @@ export const variantsClassesToBEM = (blockClasses, expectedVariantsNames, blockN } }); }; + +export const slugify = (text) => ( + text.toString().toLowerCase().trim() + // separate accent from letter + .normalize('NFD') + // remove all separated accents + .replace(/[\u0300-\u036f]/g, '') + // replace spaces with - + .replace(/\s+/g, '-') + // replace & with 'and' + .replace(/&/g, '-and-') + // remove all non-word chars + .replace(/[^\w-]+/g, '') + // replace multiple '-' with single '-' + .replace(/--+/g, '-') +); diff --git a/scripts/scripts.js b/scripts/scripts.js index b653f695d..bce0a5ab9 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -23,6 +23,7 @@ import { addFavIcon, loadDelayed, getPlaceholders, + slugify, } from './common.js'; import { isVideoLink, @@ -228,22 +229,6 @@ export function decorateLinks(block) { }); } -const slugify = (text) => ( - text.toString().toLowerCase().trim() - // separate accent from letter - .normalize('NFD') - // remove all separated accents - .replace(/[\u0300-\u036f]/g, '') - // replace spaces with - - .replace(/\s+/g, '-') - // replace & with 'and' - .replace(/&/g, '-and-') - // remove all non-word chars - .replace(/[^\w-]+/g, '') - // replace multiple '-' with single '-' - .replace(/--+/g, '-') -); - const createInpageNavigation = (main) => { const navItems = []; const tabItemsObj = []; @@ -253,7 +238,7 @@ const createInpageNavigation = (main) => { const title = section.dataset.inpage; if (title) { const countDuplcated = tabItemsObj.filter((item) => item.title === title)?.length || 0; - const order = section.dataset.inpageOrder; + const order = parseFloat(section.dataset.inpageOrder); const anchorID = (countDuplcated > 0) ? slugify(`${section.dataset.inpage}-${countDuplcated}`) : slugify(section.dataset.inpage); const obj = { title, @@ -261,7 +246,7 @@ const createInpageNavigation = (main) => { }; if (order) { - obj.order = parseFloat(section.dataset.subnavOrder); + obj.order = order; } tabItemsObj.push(obj); @@ -273,13 +258,13 @@ const createInpageNavigation = (main) => { // Sort the object by order const sortedObject = tabItemsObj.slice().sort((obj1, obj2) => { - if (obj1.order === null || obj1.order === undefined) { + if (!obj1.order) { return 1; // Move 'a' to the end } - if (obj2.order === null || obj2.order === undefined) { + if (!obj2.order) { return -1; // Move 'b' to the end } - return obj1.order - obj2.order; // Compare by order values + return obj1.order - obj2.order; }); // From the array of objects create the DOM diff --git a/styles/styles.css b/styles/styles.css index 0c66fadab..e367620f4 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -143,6 +143,9 @@ --easing-exit: cubic-bezier(0.2, 0, 1, 1); --easing-standard: cubic-bezier(0.2, 0, 0.1, 1); + /* In page navigation */ + --inpage-navigation: 0; + /* OLD STYLES */ @@ -936,6 +939,7 @@ main .section.responsive-title h1 { /* stylelint-disable-next-line no-descending-specificity */ .redesign-v2 .section { padding: 40px 0; + scroll-margin-top: calc(var(--nav-height) + var(--inpage-navigation)); } /* stylelint-disable-next-line no-descending-specificity */ diff --git a/test/scripts/common.test.js b/test/scripts/common.test.js index b815ba0a8..d4fa82222 100644 --- a/test/scripts/common.test.js +++ b/test/scripts/common.test.js @@ -93,3 +93,39 @@ describe('addFavIcon', () => { expect(link.getAttribute('href')).to.equal(newFavIconHref); }); }); + +describe('slugify', () => { + before(async () => { + commonScript = await import('../../scripts/common.js'); + }); + + it('should trim spaces', () => { + const result = commonScript.slugify(' Cards '); + expect(result).to.equal('cards'); + }); + + it('should convert uppercapse to lowercase', () => { + const result = commonScript.slugify('Cards'); + expect(result).to.equal('cards'); + }); + + it('should convert spaces in hyphen', () => { + const result = commonScript.slugify('Cards 1'); + expect(result).to.equal('cards-1'); + }); + + it('should convert double hyphen in single hyphen', () => { + const result = commonScript.slugify('Cards--1'); + expect(result).to.equal('cards-1'); + }); + + it('should convert accents to characters', () => { + const result = commonScript.slugify('Cárüñs'); + expect(result).to.equal('caruns'); + }); + + it('should remove special characters', () => { + const result = commonScript.slugify('传'); + expect(result).to.equal(''); + }); +}); From 7299b267a60483545ecf5783ea416dd19895ab0e Mon Sep 17 00:00:00 2001 From: Laura Jove Date: Thu, 31 Aug 2023 08:50:28 +0200 Subject: [PATCH 06/11] rename button function --- blocks/v2-inpage-navigation/v2-inpage-navigation.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/blocks/v2-inpage-navigation/v2-inpage-navigation.js b/blocks/v2-inpage-navigation/v2-inpage-navigation.js index cce57e5d7..1393f6ce0 100644 --- a/blocks/v2-inpage-navigation/v2-inpage-navigation.js +++ b/blocks/v2-inpage-navigation/v2-inpage-navigation.js @@ -22,7 +22,7 @@ const scrollToSection = (id) => { resizeObserver.observe(main); }; -const getInpageNavigationButtons = () => { +const inpageNavigationRedButton = () => { // if we have a button title & button link if (getMetadata('inpage-button') && getMetadata('inpage-link')) { const titleMobile = getMetadata('inpage-button'); @@ -49,7 +49,7 @@ const getInpageNavigationButtons = () => { return link; } - return []; + return null; }; const gotoSection = (event) => { @@ -112,7 +112,7 @@ const listenScroll = () => { }; export default async function decorate(block) { - const buttons = getInpageNavigationButtons(); + const redButton = inpageNavigationRedButton(); const wrapper = block.querySelector(':scope > div'); wrapper.classList.add(`${blockName}__wrapper`); @@ -147,7 +147,9 @@ export default async function decorate(block) { itemsWrapper.remove(); - wrapper.appendChild(buttons); + if (redButton) { + wrapper.appendChild(redButton); + } list.addEventListener('click', gotoSection); From fd7e985bc8f8c157b3ca5ea592650e6b88ccbd12 Mon Sep 17 00:00:00 2001 From: Laura Jove Date: Thu, 31 Aug 2023 10:21:25 +0200 Subject: [PATCH 07/11] prevent layout shifting --- scripts/scripts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/scripts.js b/scripts/scripts.js index bce0a5ab9..74c599949 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -269,7 +269,7 @@ const createInpageNavigation = (main) => { // From the array of objects create the DOM sortedObject.forEach((item) => { - const subnavItem = createElement('div'); + const subnavItem = createElement('span'); const subnavLink = createElement('button', { props: { 'data-id': item.id, From 1af2f42a667acdf5254b828c6f7b7558859474b5 Mon Sep 17 00:00:00 2001 From: Laura Jove Date: Thu, 31 Aug 2023 10:42:16 +0200 Subject: [PATCH 08/11] prevent layout shifting --- blocks/v2-inpage-navigation/v2-inpage-navigation.css | 4 +++- scripts/scripts.js | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/blocks/v2-inpage-navigation/v2-inpage-navigation.css b/blocks/v2-inpage-navigation/v2-inpage-navigation.css index 31d005c37..702a3f7a3 100644 --- a/blocks/v2-inpage-navigation/v2-inpage-navigation.css +++ b/blocks/v2-inpage-navigation/v2-inpage-navigation.css @@ -3,9 +3,11 @@ } .v2-inpage-navigation-wrapper { - box-shadow: 0 4px 24px 0 rgb(0 0 0 / 16%); background-color: var(--c-primary-white); + box-shadow: 0 4px 24px 0 rgb(0 0 0 / 16%); + height: auto !important; left: 0; + overflow: unset !important; position: sticky; top: var(--nav-height); width: 100%; diff --git a/scripts/scripts.js b/scripts/scripts.js index 74c599949..86c4aa885 100644 --- a/scripts/scripts.js +++ b/scripts/scripts.js @@ -269,7 +269,7 @@ const createInpageNavigation = (main) => { // From the array of objects create the DOM sortedObject.forEach((item) => { - const subnavItem = createElement('span'); + const subnavItem = createElement('div'); const subnavLink = createElement('button', { props: { 'data-id': item.id, @@ -293,6 +293,11 @@ function buildInpageNavigationBlock(main) { if (items.length > 0) { const section = createElement('div'); + Object.assign(section.style, { + height: '48px', + overflow: 'hidden', + }); + section.append(buildBlock(inapgeClassName, { elems: items })); // insert in second position, assumption is that Hero should be first main.insertBefore(section, main.children[1]); From 855367070af237672cb63fb7df02b725bf775416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Jove=CC=81?= Date: Thu, 31 Aug 2023 16:39:26 +0200 Subject: [PATCH 09/11] fix comments --- .../v2-inpage-navigation.css | 22 ++++++++----------- .../v2-inpage-navigation.js | 14 ++++++------ styles/styles.css | 4 ++-- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/blocks/v2-inpage-navigation/v2-inpage-navigation.css b/blocks/v2-inpage-navigation/v2-inpage-navigation.css index 702a3f7a3..619176e5f 100644 --- a/blocks/v2-inpage-navigation/v2-inpage-navigation.css +++ b/blocks/v2-inpage-navigation/v2-inpage-navigation.css @@ -1,5 +1,5 @@ :root { - --inpage-navigation: 48px; + --inpage-navigation-height: 48px; } .v2-inpage-navigation-wrapper { @@ -57,15 +57,14 @@ width: 100%; } +/* stylelint-disable-next-line no-descending-specificity */ .v2-inpage-navigation__item button:hover, +.v2-inpage-navigation__item button:active, .v2-inpage-navigation__item button:focus, .v2-inpage-navigation__selected-item-wrapper:hover, -.v2-inpage-navigation__selected-item-wrapper:focus { - background-color: #F1F1F1; -} - -.v2-inpage-navigation__item button:active, -.v2-inpage-navigation__selected-item-wrapper:active { +.v2-inpage-navigation__selected-item-wrapper:active +.v2-inpage-navigation__selected-item-wrapper:focus, +.v2-inpage-navigation__dropdown--open .v2-inpage-navigation__selected-item-wrapper { background-color: #F1F1F1; } @@ -73,7 +72,6 @@ .v2-inpage-navigation__item button { max-width: none; text-align: left; - width: 100%; } /* stylelint-disable-next-line no-descending-specificity */ @@ -96,10 +94,6 @@ display: block; } -.v2-inpage-navigation__dropdown--open .v2-inpage-navigation__selected-item-wrapper { - background-color: #F1F1F1; -} - .v2-inpage-navigation__dropdown--open .v2-inpage-navigation__selected-item-wrapper svg { transform: rotate(180deg); } @@ -137,7 +131,7 @@ @media (min-width: 1200px) { :root { - --inpage-navigation: 96px; + --inpage-navigation-height: 96px; } .v2-inpage-navigation__wrapper { @@ -184,6 +178,7 @@ display: block; height: 4px; position: absolute; + transition: background-color var(--duration-small) var(--easing-standard); width: 100%; } @@ -195,6 +190,7 @@ /* Red button */ .v2-inpage-navigation__cta:any-link { + border-radius: 2px; padding: 15px 20px; } diff --git a/blocks/v2-inpage-navigation/v2-inpage-navigation.js b/blocks/v2-inpage-navigation/v2-inpage-navigation.js index 1393f6ce0..45b6fa7db 100644 --- a/blocks/v2-inpage-navigation/v2-inpage-navigation.js +++ b/blocks/v2-inpage-navigation/v2-inpage-navigation.js @@ -64,17 +64,17 @@ const gotoSection = (event) => { }; const updateActive = (id) => { - const currentItem = document.querySelector(`.${blockName}__selected-item`); - const listItems = document.querySelector(`.${blockName}__item--active`); - listItems?.classList.remove(`${blockName}__item--active`); + const selectedItem = document.querySelector(`.${blockName}__selected-item`); + const activeItemInList = document.querySelector(`.${blockName}__item--active`); + activeItemInList?.classList.remove(`${blockName}__item--active`); const itemsButton = document.querySelectorAll(`.${blockName}__items button`); const { pathname } = window.location; if (id) { - const selectedButton = [...itemsButton].filter((button) => button.dataset.id === id); - if (!selectedButton[0]) return; - currentItem.textContent = selectedButton[0].textContent; - selectedButton[0].parentNode.classList.add(`${blockName}__item--active`); + const selectedButton = [...itemsButton].find((button) => button.dataset.id === id); + if (!selectedButton) return; + selectedItem.textContent = selectedButton.textContent; + selectedButton.parentNode.classList.add(`${blockName}__item--active`); window.history.replaceState({}, '', `${pathname}#${id}`); } else { diff --git a/styles/styles.css b/styles/styles.css index e367620f4..d293bdefa 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -144,7 +144,7 @@ --easing-standard: cubic-bezier(0.2, 0, 0.1, 1); /* In page navigation */ - --inpage-navigation: 0; + --inpage-navigation-height: 0; /* OLD STYLES @@ -939,7 +939,7 @@ main .section.responsive-title h1 { /* stylelint-disable-next-line no-descending-specificity */ .redesign-v2 .section { padding: 40px 0; - scroll-margin-top: calc(var(--nav-height) + var(--inpage-navigation)); + scroll-margin-top: calc(var(--nav-height) + var(--inpage-navigation-height)); } /* stylelint-disable-next-line no-descending-specificity */ From 99036ad5ecac13f560d15ad7210d8e7ed77a488b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Jove=CC=81?= Date: Thu, 31 Aug 2023 18:11:32 +0200 Subject: [PATCH 10/11] fix comments --- blocks/v2-inpage-navigation/v2-inpage-navigation.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/blocks/v2-inpage-navigation/v2-inpage-navigation.css b/blocks/v2-inpage-navigation/v2-inpage-navigation.css index 619176e5f..b026902d7 100644 --- a/blocks/v2-inpage-navigation/v2-inpage-navigation.css +++ b/blocks/v2-inpage-navigation/v2-inpage-navigation.css @@ -158,6 +158,10 @@ position: unset; } + .v2-inpage-navigation__item { + margin-right: auto; + } + .v2-inpage-navigation__item--active { display: block; } From d7db84fdf2f3ddeaac6241748c16000c39864abd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Jove=CC=81?= Date: Fri, 1 Sep 2023 14:21:29 +0200 Subject: [PATCH 11/11] fix focus state --- blocks/v2-inpage-navigation/v2-inpage-navigation.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/blocks/v2-inpage-navigation/v2-inpage-navigation.js b/blocks/v2-inpage-navigation/v2-inpage-navigation.js index 45b6fa7db..fe73e0b5b 100644 --- a/blocks/v2-inpage-navigation/v2-inpage-navigation.js +++ b/blocks/v2-inpage-navigation/v2-inpage-navigation.js @@ -64,8 +64,16 @@ const gotoSection = (event) => { }; const updateActive = (id) => { - const selectedItem = document.querySelector(`.${blockName}__selected-item`); const activeItemInList = document.querySelector(`.${blockName}__item--active`); + + // Prevent reassign active value + if (activeItemInList?.firstElementChild?.dataset.id === id) return; + + // Remove focus position + document.activeElement.blur(); + + // check active id is equal to id dont do anything + const selectedItem = document.querySelector(`.${blockName}__selected-item`); activeItemInList?.classList.remove(`${blockName}__item--active`); const itemsButton = document.querySelectorAll(`.${blockName}__items button`); const { pathname } = window.location;