diff --git a/.eslintrc-code-compatibility.js b/.eslintrc-code-compatibility.js index 92080c6c42..c2a8c26aca 100644 --- a/.eslintrc-code-compatibility.js +++ b/.eslintrc-code-compatibility.js @@ -14,6 +14,7 @@ module.exports = { ], ignorePatterns: [ '/libs/deps/*', + '/libs/navigation/dist/*', '/tools/loc/*', ], }; diff --git a/.eslintrc.js b/.eslintrc.js index f24adfa387..e7c17dc5fb 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -62,6 +62,7 @@ module.exports = { '/libs/features/mas/*', '/tools/loc/*', '/libs/features/spectrum-web-components/*', + '/libs/navigation/dist/*', ], plugins: [ 'chai-friendly', diff --git a/.github/workflows/release-standalone-feds.yml b/.github/workflows/release-standalone-feds.yml new file mode 100644 index 0000000000..89a11a1886 --- /dev/null +++ b/.github/workflows/release-standalone-feds.yml @@ -0,0 +1,55 @@ +name: Create a Release for Standalone Feds GlobalNav and Footer +on: + workflow_dispatch: + inputs: + version: + description: 'Version' + required: true + type: string + +permissions: + contents: write + +jobs: + release-feds: + name: Release Standalone Feds + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20.x] + defaults: + run: + working-directory: ./libs/navigation + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Set up Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies + run: npm install + + - name: Build Files + run: node ./build.mjs + + - name: Generate tarball + run: npm pack + + - name: Create Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "feds-standalone-v${{ inputs.version }}" \ + --repo="$GITHUB_REPOSITORY" \ + --title="@adobecom/standalone-feds v${{ inputs.version }} Release" \ + --generate-notes + + - name: Upload Files to Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh release upload "feds-standalone-v${{ inputs.version }}" "adobecom-standalone-feds-${{ inputs.version }}.tgz" diff --git a/.gitignore b/.gitignore index 168ba7ef67..4952087964 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ logs/* test-html-results/ test-results/ test-a11y-results/ +libs/navigation/dist/ diff --git a/libs/blocks/global-footer/global-footer.css b/libs/blocks/global-footer/global-footer.css index 5f32030e01..951a1d72d3 100644 --- a/libs/blocks/global-footer/global-footer.css +++ b/libs/blocks/global-footer/global-footer.css @@ -252,6 +252,20 @@ height: 12px; } +@media (min-width: 600px) { + dialog.feds-dialog { + max-width: 80vw; + width: fit-content; + } +} + +@media (min-width: 1200px) { + dialog.feds-dialog { + width: 1200px; + max-width: calc((100% - 6px) - 2em); + } +} + @media (min-width: 900px) { /* If there is too much content, float it on multiple rows */ .feds-footer-wrapper .feds-menu-content { diff --git a/libs/blocks/global-footer/global-footer.js b/libs/blocks/global-footer/global-footer.js index 776e8586f7..051bda37e4 100644 --- a/libs/blocks/global-footer/global-footer.js +++ b/libs/blocks/global-footer/global-footer.js @@ -4,8 +4,8 @@ import { decorateLinks, getMetadata, getConfig, - loadBlock, localizeLink, + loadStyle, } from '../../utils/utils.js'; import { @@ -217,6 +217,8 @@ class Footer { ${regionPickerTextElem} `; + regionPickerElem.dataset.modalPath = `${url.pathname}#_inline`; + regionPickerElem.dataset.modalHash = url.hash; const regionPickerWrapperClass = 'feds-regionPicker-wrapper'; this.elements.regionPicker = toFragment`
${regionPickerElem} @@ -230,24 +232,48 @@ class Footer { // Hash -> region selector opens a modal decorateAutoBlock(regionPickerElem); // add modal-specific attributes // TODO remove logs after finding the root cause for the region picker 404s -> MWPW-143627 + regionPickerElem.href = url.hash; if (regionPickerElem.classList[0] !== 'modal') { lanaLog({ message: `Modal block class missing from region picker pre loading the block; locale: ${locale}; regionPickerElem: ${regionPickerElem.outerHTML}`, tags: 'errorType=warn,module=global-footer', }); } - await loadBlock(regionPickerElem); // load modal logic and styles + loadStyle(`${base}/blocks/modal/modal.css`); + const { default: initModal } = await import('../modal/modal.js'); + const modal = await initModal(regionPickerElem); + + const loadRegionNav = async () => { + const block = document.querySelector('.region-nav'); + if (block && getConfig().standaloneGnav) { + // on standalone the region-nav will fail to load automatically through + // the modal calling fragment.js. In that case we will have data-failed=true + // and we should manually load region nav + // If that's not the case then we're not a standalone gnav + // and we mustn't load region-nav twice. + if (block.getAttribute('data-failed') !== 'true') return; + block.classList.add('hide'); + loadStyle(`${base}/blocks/region-nav/region-nav.css`); + const { default: initRegionNav } = await import('../region-nav/region-nav.js'); + initRegionNav(block); + // decoratePlaceholders(block, getConfig()); + block.classList.remove('hide'); + } + }; + + if (modal) await loadRegionNav(); // just in case the modal is already open + if (regionPickerElem.classList[0] !== 'modal') { lanaLog({ message: `Modal block class missing from region picker post loading the block; locale: ${locale}; regionPickerElem: ${regionPickerElem.outerHTML}`, tags: 'errorType=warn,module=global-footer', }); } - // 'decorateAutoBlock' logic replaces class name entirely, need to add it back - regionPickerElem.classList.add(regionPickerClass); regionPickerElem.addEventListener('click', () => { if (!isRegionPickerExpanded()) { regionPickerElem.setAttribute('aria-expanded', 'true'); + // wait for the modal to load before we load the region nav + window.addEventListener('milo:modal:loaded', loadRegionNav, { once: true }); } }); // Set aria-expanded to false when region modal is closed @@ -262,7 +288,8 @@ class Footer { regionSelector.href = localizeLink(regionSelector.href); decorateAutoBlock(regionSelector); // add fragment-specific class(es) this.elements.regionPicker.append(regionSelector); // add fragment after regionPickerElem - await loadBlock(regionSelector); // load fragment and replace original link + const { default: initFragment } = await import('../fragment/fragment.js'); + await initFragment(regionSelector); // load fragment and replace original link // Update aria-expanded on click regionPickerElem.addEventListener('click', (e) => { e.preventDefault(); @@ -271,6 +298,7 @@ class Footer { }); // Close region picker dropdown on outside click document.addEventListener('click', (e) => { + e.preventDefault(); if (isRegionPickerExpanded() && !e.target.closest(`.${regionPickerWrapperClass}`)) { regionPickerElem.setAttribute('aria-expanded', false); @@ -278,7 +306,7 @@ class Footer { }); } - return this.regionPicker; + return this.elements.regionPicker; }; decorateSocial = () => { diff --git a/libs/blocks/global-navigation/base.css b/libs/blocks/global-navigation/base.css index 1eb4d98d29..15a7925834 100644 --- a/libs/blocks/global-navigation/base.css +++ b/libs/blocks/global-navigation/base.css @@ -118,8 +118,8 @@ align-items: center; } -header.global-navigation { - visibility: visible; +header.global-navigation.ready { + visibility: visible !important; } /* Desktop styles */ diff --git a/libs/blocks/global-navigation/global-navigation.js b/libs/blocks/global-navigation/global-navigation.js index f632adbe72..dd8dbe3179 100644 --- a/libs/blocks/global-navigation/global-navigation.js +++ b/libs/blocks/global-navigation/global-navigation.js @@ -1,3 +1,4 @@ +/* eslint import/no-relative-packages: 0 */ /* eslint-disable no-async-promise-executor */ import { getConfig, @@ -20,7 +21,6 @@ import { isTangentToViewport, lanaLog, loadBaseStyles, - loadBlock, loadDecorateMenu, rootPath, loadStyles, @@ -222,7 +222,7 @@ const decorateProfileTrigger = async ({ avatar }) => { let keyboardNav; const setupKeyboardNav = async () => { keyboardNav = keyboardNav || new Promise(async (resolve) => { - const KeyboardNavigation = await loadBlock('./keyboard/index.js'); + const { default: KeyboardNavigation } = await import('./utilities/keyboard/index.js'); const instance = new KeyboardNavigation(); resolve(instance); }); @@ -428,17 +428,17 @@ class Gnav { this.block.removeEventListener('keydown', this.loadDelayed); if (this.searchPresent()) { const [ - Search, + { default: Search }, ] = await Promise.all([ - loadBlock('../features/search/gnav-search.js'), + import('./features/search/gnav-search.js'), loadStyles(rootPath('features/search/gnav-search.css')), ]); this.Search = Search; } if (!this.useUniversalNav) { - const [ProfileDropdown] = await Promise.all([ - loadBlock('../features/profile/dropdown.js'), + const [{ default: ProfileDropdown }] = await Promise.all([ + import('./features/profile/dropdown.js'), loadStyles(rootPath('features/profile/dropdown.css')), ]); this.ProfileDropdown = ProfileDropdown; @@ -543,7 +543,7 @@ class Gnav { const unavVersion = new URLSearchParams(window.location.search).get('unavVersion') || '1.3'; await Promise.all([ loadScript(`https://${environment}.adobeccstatic.com/unav/${unavVersion}/UniversalNav.js`), - loadStyles(`https://${environment}.adobeccstatic.com/unav/${unavVersion}/UniversalNav.css`), + loadStyles(`https://${environment}.adobeccstatic.com/unav/${unavVersion}/UniversalNav.css`, true), ]); const getChildren = () => { @@ -913,7 +913,7 @@ class Gnav { const menuLogic = await loadDecorateMenu(); - menuLogic.decorateMenu({ + await menuLogic.decorateMenu({ item, template, type: itemType, @@ -1024,7 +1024,7 @@ class Gnav { const breadcrumbsElem = this.block.querySelector('.breadcrumbs'); // Breadcrumbs are not initially part of the nav, need to decorate the links if (breadcrumbsElem) decorateLinks(breadcrumbsElem); - const createBreadcrumbs = await loadBlock('../features/breadcrumbs/breadcrumbs.js'); + const { default: createBreadcrumbs } = await import('./features/breadcrumbs/breadcrumbs.js'); this.elements.breadcrumbsWrapper = await createBreadcrumbs(breadcrumbsElem); return this.elements.breadcrumbsWrapper; }; @@ -1094,5 +1094,6 @@ export default async function init(block) { const mepMartech = mep?.martech || ''; block.setAttribute('daa-lh', `gnav|${getExperienceName()}${mepMartech}`); if (isDarkMode()) block.classList.add('feds--dark'); + block.classList.add('ready'); return gnav; } diff --git a/libs/blocks/global-navigation/utilities/getUserEntitlements.js b/libs/blocks/global-navigation/utilities/getUserEntitlements.js index 1166146179..b9812236b6 100644 --- a/libs/blocks/global-navigation/utilities/getUserEntitlements.js +++ b/libs/blocks/global-navigation/utilities/getUserEntitlements.js @@ -1,3 +1,4 @@ +/* eslint import/no-relative-packages: 0 */ /* eslint-disable camelcase */ import { getConfig } from '../../../utils/utils.js'; diff --git a/libs/blocks/global-navigation/utilities/getUserEventHistory.js b/libs/blocks/global-navigation/utilities/getUserEventHistory.js index e1fcc23fee..25937e34a2 100644 --- a/libs/blocks/global-navigation/utilities/getUserEventHistory.js +++ b/libs/blocks/global-navigation/utilities/getUserEventHistory.js @@ -1,3 +1,4 @@ +/* eslint import/no-relative-packages: 0 */ /* eslint-disable no-promise-executor-return, no-async-promise-executor */ import { getConfig } from '../../../utils/utils.js'; diff --git a/libs/blocks/global-navigation/utilities/utilities.js b/libs/blocks/global-navigation/utilities/utilities.js index 9e9b6c457c..ed7fcb3491 100644 --- a/libs/blocks/global-navigation/utilities/utilities.js +++ b/libs/blocks/global-navigation/utilities/utilities.js @@ -1,3 +1,4 @@ +/* eslint import/no-relative-packages: 0 */ import { getConfig, getMetadata, loadStyle, loadLana, decorateLinks, localizeLink, } from '../../../utils/utils.js'; @@ -134,7 +135,9 @@ export function rootPath(path) { return url; } -export function loadStyles(url) { +export function loadStyles(url, override = false) { + const { standaloneGnav } = getConfig(); + if (standaloneGnav && !override) return; loadStyle(url, (e) => { if (e === 'error') { lanaLog({ @@ -155,6 +158,8 @@ export function isDarkMode() { // since they can be independent of each other. // CSS imports were not used due to duplication of file include export async function loadBaseStyles() { + const { standaloneGnav } = getConfig(); + if (standaloneGnav) return; if (isDarkMode()) { new Promise((resolve) => { loadStyle(rootPath('base.css'), resolve); }) .then(() => loadStyles(rootPath('dark-nav.css'))); @@ -164,10 +169,6 @@ export async function loadBaseStyles() { } } -export function loadBlock(path) { - return import(path).then((module) => module.default); -} - let cachedDecorateMenu; export async function loadDecorateMenu() { if (cachedDecorateMenu) return cachedDecorateMenu; @@ -177,15 +178,12 @@ export async function loadDecorateMenu() { resolve = _resolve; }); - const [{ decorateMenu, decorateLinkGroup }] = await Promise.all([ - loadBlock('./menu/menu.js'), + const [menu] = await Promise.all([ + import('./menu/menu.js'), loadStyles(rootPath('utilities/menu/menu.css')), ]); - resolve({ - decorateMenu, - decorateLinkGroup, - }); + resolve(menu.default); return cachedDecorateMenu; } diff --git a/libs/blocks/region-nav/region-nav.css b/libs/blocks/region-nav/region-nav.css index b3a9859c0b..a996434f33 100644 --- a/libs/blocks/region-nav/region-nav.css +++ b/libs/blocks/region-nav/region-nav.css @@ -56,6 +56,10 @@ column-count: 1; } +.region-nav.hide { + display: none; +} + @media (min-width: 600px) { .region-nav > div:nth-of-type(2) { column-count: 3; diff --git a/libs/navigation/base.css b/libs/navigation/base.css new file mode 100644 index 0000000000..6df2730955 --- /dev/null +++ b/libs/navigation/base.css @@ -0,0 +1 @@ +@import '../blocks/global-navigation/base.css'; diff --git a/libs/navigation/bootstrapper.js b/libs/navigation/bootstrapper.js index 4f19bb3ffd..55778464cc 100644 --- a/libs/navigation/bootstrapper.js +++ b/libs/navigation/bootstrapper.js @@ -1,10 +1,7 @@ -export default async function bootstrapBlock(miloLibs, blockConfig) { +/* eslint import/no-relative-packages: 0 */ +export default async function bootstrapBlock(initBlock, blockConfig) { const { name, targetEl, layout, noBorder, jarvis } = blockConfig; - const { getConfig, createTag, loadLink, loadScript } = await import(`${miloLibs}/utils/utils.js`); - const { default: initBlock } = await import(`${miloLibs}/blocks/${name}/${name}.js`); - - const styles = [`${miloLibs}/blocks/${name}/${name}.css`, `${miloLibs}/navigation/navigation.css`]; - styles.forEach((url) => loadLink(url, { rel: 'stylesheet' })); + const { getConfig, createTag, loadScript } = await import('../utils/utils.js'); const setNavLayout = () => { const element = document.querySelector(targetEl); @@ -41,7 +38,7 @@ export default async function bootstrapBlock(miloLibs, blockConfig) { await initBlock(document.querySelector(targetEl)); if (blockConfig.targetEl === 'footer') { - const { loadPrivacy } = await import(`${miloLibs}/scripts/delayed.js`); + const { loadPrivacy } = await import('../scripts/delayed.js'); setTimeout(() => { loadPrivacy(getConfig, loadScript); }, blockConfig.delay); diff --git a/libs/navigation/build.mjs b/libs/navigation/build.mjs new file mode 100755 index 0000000000..f94fb1f657 --- /dev/null +++ b/libs/navigation/build.mjs @@ -0,0 +1,57 @@ +import * as esbuild from 'esbuild'; // eslint-disable-line +import fs from 'node:fs'; + +fs.rmSync('./dist/', { recursive: true, force: true }); + +await esbuild.build({ + entryPoints: ['navigation.css', 'footer.css', 'dark-nav.css', 'base.css'], + bundle: true, + minify: true, + outdir: './dist/', +}); + +// This function behaves slightly different +// than the built in split function in +// that it only splits the array xs into two arrays +// on the first occurence of y only +const splitAt = (xs, y) => { + if (!xs.length) return null; + const splitInternal = (before, after) => { + if (!after.length) return [before, []]; + const [x, ...rest] = after; + if (x === y) return [before, rest]; + return splitInternal(before.concat([x]), rest); + }; + return splitInternal([], xs); +}; + +const StyleLoader = { + name: 'inline-style', + setup({ onLoad }) { + const template = (css) => ` + typeof document<'u'&& + document.head + .appendChild(document.createElement('style')) + .appendChild(document.createTextNode(${JSON.stringify(css)}))`; + onLoad({ filter: /\.css$/ }, async (args) => { + const { path } = args; + const [before, after] = splitAt(path.split('/'), 'navigation'); + const newPath = before + .concat(['navigation', 'dist']) + .concat(after) + .join('/'); + const css = await fs.promises.readFile(newPath, 'utf8'); + return { contents: template(css) }; + }); + }, +}; + +await esbuild.build({ + entryPoints: ['navigation.js'], + bundle: true, + splitting: true, + format: 'esm', + sourcemap: true, + outdir: './dist/', + plugins: [StyleLoader], +}); diff --git a/libs/navigation/dark-nav.css b/libs/navigation/dark-nav.css new file mode 100644 index 0000000000..8cf31dba0e --- /dev/null +++ b/libs/navigation/dark-nav.css @@ -0,0 +1 @@ +@import '../blocks/global-navigation/dark-nav.css'; diff --git a/libs/navigation/footer.css b/libs/navigation/footer.css new file mode 100644 index 0000000000..802e676252 --- /dev/null +++ b/libs/navigation/footer.css @@ -0,0 +1,2 @@ +@import '../blocks/global-footer/global-footer.css'; +@import '../blocks/modal/modal.css'; diff --git a/libs/navigation/navigation.css b/libs/navigation/navigation.css index cca872ed0b..ce0aae2d06 100644 --- a/libs/navigation/navigation.css +++ b/libs/navigation/navigation.css @@ -1,3 +1,8 @@ +@import '../blocks/global-navigation/global-navigation.css'; +@import '../blocks/global-navigation/features/profile/dropdown.css'; +@import '../blocks/global-navigation/features/search/gnav-search.css'; +@import '../blocks/global-navigation/utilities/menu/menu.css'; + /* Extracting the essential styles required for rendering the component independently */ :root { --navigation-link-color: #035FE6; diff --git a/libs/navigation/navigation.js b/libs/navigation/navigation.js index 23ecc702a0..3c26aabb6d 100644 --- a/libs/navigation/navigation.js +++ b/libs/navigation/navigation.js @@ -1,3 +1,5 @@ +import { loadStyle } from '../utils/utils.js'; + const blockConfig = [ { key: 'header', @@ -53,6 +55,8 @@ function getParamsConfigs(configs) { return acc; }, {}); } + +/* eslint import/no-relative-packages: 0 */ export default async function loadBlock(configs, customLib) { const { header, @@ -64,21 +68,40 @@ export default async function loadBlock(configs, customLib) { allowedOrigins, stageDomainsMap = {}, } = configs || {}; - const branch = new URLSearchParams(window.location.search).get('navbranch'); - const miloLibs = branch ? `https://${branch}--milo--adobecom.aem.page` : customLib || envMap[env]; if (!header && !footer) { // eslint-disable-next-line no-console console.error('Global navigation Error: header and footer configurations are missing.'); return; } - // Relative path can't be used, as the script will run on consumer's app + const branch = new URLSearchParams(window.location.search).get('navbranch'); + const miloLibs = branch ? `https://${branch}--milo--adobecom.aem.page` : customLib || envMap[env]; + + // The below css imports will fail when using the non-bundled standalone gnav + // and fallback to using loadStyle. On the other hand, the bundler will rewrite + // the css imports to attach the styles to the head (and point to the dist folder + // using the custom StyleLoader plugin found in build.mjs + try { + await import('./base.css'); + if (theme === 'dark') { + await import('./dark-nav.css'); + } + await import('./navigation.css'); + } catch (e) { + if (theme === 'dark') { + loadStyle(`${miloLibs}/libs/navigation/base.css`, () => loadStyle(`${miloLibs}/libs/navigation/dark-nav.css`)); + } else { + loadStyle(`${miloLibs}/libs/navigation/base.css`); + } + loadStyle(`${miloLibs}/libs/navigation/navigation.css`); + } + + // Relative paths work just fine since they exist in the context of this file's origin const [{ default: bootstrapBlock }, { default: locales }, { setConfig }] = await Promise.all([ - import(`${miloLibs}/libs/navigation/bootstrapper.js`), - import(`${miloLibs}/libs/utils/locales.js`), - import(`${miloLibs}/libs/utils/utils.js`), + import('./bootstrapper.js'), + import('../utils/locales.js'), + import('../utils/utils.js'), ]); - - const paramConfigs = getParamsConfigs(configs, miloLibs); + const paramConfigs = getParamsConfigs(configs); const clientConfig = { clientEnv: env, origin: `https://main--federal--adobecom.aem.${env === 'prod' ? 'live' : 'page'}`, @@ -90,6 +113,7 @@ export default async function loadBlock(configs, customLib) { ...paramConfigs, prodDomains, allowedOrigins, + standaloneGnav: true, stageDomainsMap: getStageDomainsMap(stageDomainsMap), }; setConfig(clientConfig); @@ -97,16 +121,25 @@ export default async function loadBlock(configs, customLib) { const configBlock = configs[block.key]; try { if (configBlock) { - await bootstrapBlock(`${miloLibs}/libs`, { - ...block, - ...(block.key === 'header' && { + if (block.key === 'header') { + const { default: init } = await import('../blocks/global-navigation/global-navigation.js'); + await bootstrapBlock(init, { + ...block, unavComponents: configBlock.unav?.unavComponents, redirect: configBlock.redirect, layout: configBlock.layout, noBorder: configBlock.noBorder, jarvis: configBlock.jarvis, - }), - }); + }); + } else if (block.key === 'footer') { + try { + await import('./footer.css'); + } catch (e) { + loadStyle(`${miloLibs}/libs/navigation/footer.css`); + } + const { default: init } = await import('../blocks/global-footer/global-footer.js'); + await bootstrapBlock(init, { ...block }); + } configBlock.onReady?.(); } } catch (e) { diff --git a/libs/navigation/package-lock.json b/libs/navigation/package-lock.json new file mode 100644 index 0000000000..f29d7ac225 --- /dev/null +++ b/libs/navigation/package-lock.json @@ -0,0 +1,439 @@ +{ + "name": "navigation", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "navigation", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "esbuild": "0.24.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", + "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", + "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", + "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", + "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", + "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", + "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", + "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", + "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", + "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", + "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", + "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", + "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", + "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", + "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", + "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", + "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", + "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", + "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", + "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", + "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", + "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", + "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", + "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", + "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", + "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.0", + "@esbuild/android-arm": "0.24.0", + "@esbuild/android-arm64": "0.24.0", + "@esbuild/android-x64": "0.24.0", + "@esbuild/darwin-arm64": "0.24.0", + "@esbuild/darwin-x64": "0.24.0", + "@esbuild/freebsd-arm64": "0.24.0", + "@esbuild/freebsd-x64": "0.24.0", + "@esbuild/linux-arm": "0.24.0", + "@esbuild/linux-arm64": "0.24.0", + "@esbuild/linux-ia32": "0.24.0", + "@esbuild/linux-loong64": "0.24.0", + "@esbuild/linux-mips64el": "0.24.0", + "@esbuild/linux-ppc64": "0.24.0", + "@esbuild/linux-riscv64": "0.24.0", + "@esbuild/linux-s390x": "0.24.0", + "@esbuild/linux-x64": "0.24.0", + "@esbuild/netbsd-x64": "0.24.0", + "@esbuild/openbsd-arm64": "0.24.0", + "@esbuild/openbsd-x64": "0.24.0", + "@esbuild/sunos-x64": "0.24.0", + "@esbuild/win32-arm64": "0.24.0", + "@esbuild/win32-ia32": "0.24.0", + "@esbuild/win32-x64": "0.24.0" + } + } + } +} diff --git a/libs/navigation/package.json b/libs/navigation/package.json new file mode 100644 index 0000000000..9323d847cd --- /dev/null +++ b/libs/navigation/package.json @@ -0,0 +1,16 @@ +{ + "name": "@adobecom/standalone-feds", + "version": "0.0.1", + "description": "", + "main": "dist/navigation.js", + "type": "module", + "scripts": { + "build": "node ./build.mjs" + }, + "files": ["dist"], + "author": "", + "license": "ISC", + "devDependencies": { + "esbuild": "0.24.0" + } +} diff --git a/libs/utils/utils.js b/libs/utils/utils.js index a22c7af550..c96169a3de 100644 --- a/libs/utils/utils.js +++ b/libs/utils/utils.js @@ -831,7 +831,7 @@ const findReplaceableNodes = (area) => { }; let placeholderRequest; -async function decoratePlaceholders(area, config) { +export async function decoratePlaceholders(area, config) { if (!area) return; const nodes = findReplaceableNodes(area); if (!nodes.length) return; diff --git a/test/blocks/global-footer/global-footer.test.js b/test/blocks/global-footer/global-footer.test.js index 3ab306b9ce..8e8ed697d8 100644 --- a/test/blocks/global-footer/global-footer.test.js +++ b/test/blocks/global-footer/global-footer.test.js @@ -187,6 +187,11 @@ describe('global footer', () => { const regionPickerElem = document.querySelector(allSelectors.regionPicker); regionPickerElem.dispatchEvent(new Event('click')); + const regionNavModal = document.createElement('div'); + regionNavModal.classList.add('region-nav'); // pretend that the modal was added to the body + // since clicking on the regionpicker elem apparently doesnt set the hash + document.body.append(regionNavModal); + window.dispatchEvent(new Event('milo:modal:loaded')); expect(regionPickerElem.getAttribute('href') === '#langnav').to.equal(true); expect(regionPickerElem.getAttribute('aria-expanded')).to.equal('true'); @@ -433,4 +438,27 @@ describe('global footer', () => { expect(document.querySelector('footer').classList.contains('feds--dark')).to.be.true; }); }); + describe('standalone footer', async () => { + it('should still load the regionnav if it\'s a standalone footer', async () => { + await createFullGlobalFooter({ + waitForDecoration: true, + customConfig: { standaloneGnav: true }, + }); + + const regionPickerElem = document.querySelector(allSelectors.regionPicker); + regionPickerElem.dispatchEvent(new Event('click')); + const regionNavModal = document.createElement('div'); + regionNavModal.classList.add('region-nav'); // pretend that the modal was added to the body + regionNavModal.setAttribute('data-failed', 'true'); + // since clicking on the regionpicker elem apparently doesnt set the hash + document.body.append(regionNavModal); + window.dispatchEvent(new Event('milo:modal:loaded')); + + expect(regionPickerElem.getAttribute('href') === '#langnav').to.equal(true); + expect(regionPickerElem.getAttribute('aria-expanded')).to.equal('true'); + + window.dispatchEvent(new Event('milo:modal:closed')); + expect(regionPickerElem.getAttribute('aria-expanded')).to.equal('false'); + }); + }); }); diff --git a/test/blocks/global-navigation/keyboard/mocks/global-nav-mobile.html b/test/blocks/global-navigation/keyboard/mocks/global-nav-mobile.html index 4b4c984091..0d3eb2cb36 100644 --- a/test/blocks/global-navigation/keyboard/mocks/global-nav-mobile.html +++ b/test/blocks/global-navigation/keyboard/mocks/global-nav-mobile.html @@ -1,5 +1,5 @@