From f2fbeaa7970af2573fa4f6a595c1db7c8d149c09 Mon Sep 17 00:00:00 2001
From: Raghav Sharma <118168183+sharmrj@users.noreply.github.com>
Date: Wed, 4 Dec 2024 14:48:14 +0530
Subject: [PATCH 01/11] MWPW-161273 Standalone Gnav needs a release cycle
[Bundle] (#3132)
* bundled standalone gnav and footer
* Fixed dark mode; load css from navigation.js
* Refactored global footer to no longer use milo modal to render region-nav
* global footer unit test
* fix lint
* Added keyboard navigation to the new region nav modal
* export stuff from modal.js
* replaced the new modal implementation with a more explicit usage of the current milo modal so that it can be bundled
* code compatibility ignore pattern
* Fix keyboard navigation unit tests
* fixed footer unit test after changing the implementation of the region nav (again)
* added sourcemaps; fixed dark mode issue
* navigation unit tests
* Cover uncovered lines in global-footer.js
* prevent FOUC in region nav
* built latest gnav changes
* modified package.json to have a files field
* use evergreen css for non-bundled and built css for bundled
* Fixed region picker when there's no hash
* Added a workflow to release standalone feds and removed dist from the PR
* changed workflow_call to workflow_dispatch
* Adjusted the cd command in the workflow
* added a working directory
* missed a space in the gh release upload command
* added GITHUB_TOKEN to the upload asset step
* fixed an error with file upload in the workflow
* Removed a console.log from the build script; explicitly load fragment block in footer
* Removed an unused import
* Renamed a funciton in the build file and added a comment
* Fixed region nav breaking on certain milo consumers
* Fixed region nav breaking on some milo consumers for real this time
* Removed an unused import
* Added logic to not call the region nav code twice
* unit test
* modified a standalone footer unit test slightly
* Removed a comment
---
.eslintrc-code-compatibility.js | 1 +
.eslintrc.js | 1 +
.github/workflows/release-standalone-feds.yml | 55 +++
.gitignore | 1 +
libs/blocks/global-footer/global-footer.css | 14 +
libs/blocks/global-footer/global-footer.js | 40 +-
libs/blocks/global-navigation/base.css | 4 +-
.../global-navigation/global-navigation.js | 19 +-
.../utilities/getUserEntitlements.js | 1 +
.../utilities/getUserEventHistory.js | 1 +
.../global-navigation/utilities/utilities.js | 20 +-
libs/blocks/region-nav/region-nav.css | 4 +
libs/navigation/base.css | 1 +
libs/navigation/bootstrapper.js | 11 +-
libs/navigation/build.mjs | 57 +++
libs/navigation/dark-nav.css | 1 +
libs/navigation/footer.css | 2 +
libs/navigation/navigation.css | 5 +
libs/navigation/navigation.js | 58 ++-
libs/navigation/package-lock.json | 439 ++++++++++++++++++
libs/navigation/package.json | 16 +
libs/utils/utils.js | 2 +-
.../global-footer/global-footer.test.js | 28 ++
.../keyboard/mocks/global-nav-mobile.html | 2 +-
.../keyboard/mocks/global-nav.html | 2 +-
test/navigation/bootstrapper.test.js | 19 +-
test/navigation/navigation.test.js | 3 +-
27 files changed, 749 insertions(+), 58 deletions(-)
create mode 100644 .github/workflows/release-standalone-feds.yml
create mode 100644 libs/navigation/base.css
create mode 100755 libs/navigation/build.mjs
create mode 100644 libs/navigation/dark-nav.css
create mode 100644 libs/navigation/footer.css
create mode 100644 libs/navigation/package-lock.json
create mode 100644 libs/navigation/package.json
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 2cd9f35559..6c74620291 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 {
@@ -218,6 +218,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}
@@ -231,24 +233,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
@@ -263,7 +289,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();
@@ -272,6 +299,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);
@@ -279,7 +307,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 4a1903171a..55815c5994 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,
@@ -21,7 +22,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);
});
@@ -425,17 +425,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;
@@ -540,7 +540,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 = () => {
@@ -910,7 +910,7 @@ class Gnav {
const menuLogic = await loadDecorateMenu();
- menuLogic.decorateMenu({
+ await menuLogic.decorateMenu({
item,
template,
type: itemType,
@@ -1021,7 +1021,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;
};
@@ -1091,5 +1091,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 79104db68c..370ae1f2ca 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';
@@ -152,7 +153,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({
@@ -173,6 +176,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')));
@@ -182,10 +187,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;
@@ -195,15 +196,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 d54e439eaa..a4cb526218 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',
@@ -51,6 +53,7 @@ function getParamsConfigs(configs) {
}, {});
}
+/* eslint import/no-relative-packages: 0 */
export default async function loadBlock(configs, customLib) {
const {
header,
@@ -61,21 +64,40 @@ export default async function loadBlock(configs, customLib) {
theme,
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/dist/base.css`, () => loadStyle(`${miloLibs}/libs/navigation/dist/dark-nav.css`));
+ } else {
+ loadStyle(`${miloLibs}/libs/navigation/dist/base.css`);
+ }
+ loadStyle(`${miloLibs}/libs/navigation/dist/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'}`,
@@ -85,6 +107,7 @@ export default async function loadBlock(configs, customLib) {
contentRoot: authoringPath || footer.authoringPath,
theme,
...paramConfigs,
+ standaloneGnav: true,
stageDomainsMap: getStageDomainsMap(stageDomainsMap),
};
setConfig(clientConfig);
@@ -92,16 +115,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/dist/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 13ecb1d7fb..9cae5ef3e6 100644
--- a/libs/utils/utils.js
+++ b/libs/utils/utils.js
@@ -819,7 +819,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 @@
diff --git a/test/blocks/global-navigation/keyboard/mocks/global-nav.html b/test/blocks/global-navigation/keyboard/mocks/global-nav.html
index 5028fc9841..10e1e6c748 100644
--- a/test/blocks/global-navigation/keyboard/mocks/global-nav.html
+++ b/test/blocks/global-navigation/keyboard/mocks/global-nav.html
@@ -1,5 +1,5 @@
diff --git a/test/navigation/bootstrapper.test.js b/test/navigation/bootstrapper.test.js
index b16743b57b..d524c82fda 100644
--- a/test/navigation/bootstrapper.test.js
+++ b/test/navigation/bootstrapper.test.js
@@ -1,3 +1,4 @@
+/* eslint import/no-relative-packages: 0 */
import { readFile } from '@web/test-runner-commands';
import { expect } from '@esm-bundle/chai';
import { stub, useFakeTimers, restore, spy } from 'sinon';
@@ -57,7 +58,8 @@ describe('Bootstrapper', async () => {
});
it('Renders the footer block', async () => {
- await loadBlock(miloLibs, blockConfig.footer);
+ const { default: init } = await import('../../libs/blocks/global-footer/global-footer.js');
+ await loadBlock(init, blockConfig.footer);
const clock = useFakeTimers({
toFake: ['setTimeout'],
shouldAdvanceTime: true,
@@ -68,21 +70,24 @@ describe('Bootstrapper', async () => {
});
it('Renders the header block', async () => {
- await loadBlock(miloLibs, blockConfig.header);
+ const { default: init } = await import('../../libs/blocks/global-navigation/global-navigation.js');
+ await loadBlock(init, blockConfig.header);
const el = document.getElementsByTagName('header');
expect(el).to.exist;
});
it('Renders the header with full width', async () => {
blockConfig.header.layout = 'fullWidth';
- await loadBlock(miloLibs, blockConfig.header);
+ const { default: init } = await import('../../libs/blocks/global-navigation/global-navigation.js');
+ await loadBlock(init, blockConfig.header);
const el = document.querySelector('header');
expect(el.classList.contains('feds--full-width')).to.be.true;
});
it('Renders the header with no border bottom', async () => {
blockConfig.header.noBorder = true;
- await loadBlock(miloLibs, blockConfig.header);
+ const { default: init } = await import('../../libs/blocks/global-navigation/global-navigation.js');
+ await loadBlock(init, blockConfig.header);
const el = document.querySelector('header');
expect(el.classList.contains('feds--no-border')).to.be.true;
});
@@ -91,7 +96,8 @@ describe('Bootstrapper', async () => {
blockConfig.header.jarvis = { id: '1.1' };
stub(window.AdobeMessagingExperienceClient, 'isAdobeMessagingClientInitialized').returns(true);
stub(window.AdobeMessagingExperienceClient, 'getMessagingExperienceState').returns({ windowState: 'hidden' });
- await loadBlock(miloLibs, blockConfig.header);
+ const { default: init } = await import('../../libs/blocks/global-navigation/global-navigation.js');
+ await loadBlock(init, blockConfig.header);
const el = document.querySelector('.feds-cta[href*="#open-jarvis-chat"]');
const event = new CustomEvent('click', { bubbles: true });
el.dispatchEvent(event);
@@ -101,7 +107,8 @@ describe('Bootstrapper', async () => {
it('should not call openMessagingWindow when chat dialog is already open', async () => {
stub(window.AdobeMessagingExperienceClient, 'isAdobeMessagingClientInitialized').returns(true);
stub(window.AdobeMessagingExperienceClient, 'getMessagingExperienceState').returns({ windowState: 'docked' });
- await loadBlock(miloLibs, blockConfig.header);
+ const { default: init } = await import('../../libs/blocks/global-navigation/global-navigation.js');
+ await loadBlock(init, blockConfig.header);
const el = document.querySelector('.feds-cta[href*="#open-jarvis-chat"]');
const event = new CustomEvent('click', { bubbles: true });
el.dispatchEvent(event);
diff --git a/test/navigation/navigation.test.js b/test/navigation/navigation.test.js
index a6b054d184..93331eb331 100644
--- a/test/navigation/navigation.test.js
+++ b/test/navigation/navigation.test.js
@@ -1,3 +1,4 @@
+/* eslint import/no-relative-packages: 0 */
import { readFile } from '@web/test-runner-commands';
import { expect } from '@esm-bundle/chai';
import { stub, restore } from 'sinon';
@@ -60,7 +61,7 @@ describe('Navigation component', async () => {
});
it('Does not render either header or footer if configs is not passed', async () => {
- document.body.innerHTML = await readFile({ path: './mocks/body.html' });
+ document.body.innerHTML = await readFile({ path: './mocks/body.html' }, 'http://localhost:2000');
await loadBlock();
const header = document.getElementsByTagName('header');
const footer = document.getElementsByTagName('footer');
From 22dab6644861e52865ab288c0149d91b912823b3 Mon Sep 17 00:00:00 2001
From: Jason Slavin
Date: Wed, 4 Dec 2024 01:18:24 -0800
Subject: [PATCH 02/11] MWPW-155723 - Adds group metadata to ensure dyanmic nav
continuity (#3124)
* Adding dynamic-nav-group setting to allow distinction between dynamic-nav user journeys
* Utils change
* Ensuring status shows correctly dispite group
* Making metadata case insensative
---
.../dynamic-navigation/dynamic-navigation.js | 14 ++++
libs/features/dynamic-navigation/status.js | 8 ++-
test/features/dynamic-nav/dynamicNav.test.js | 30 ++++++++
test/features/dynamic-nav/status.test.js | 70 ++++++++++++++++++-
4 files changed, 120 insertions(+), 2 deletions(-)
diff --git a/libs/features/dynamic-navigation/dynamic-navigation.js b/libs/features/dynamic-navigation/dynamic-navigation.js
index 7354518a58..7d943cc597 100644
--- a/libs/features/dynamic-navigation/dynamic-navigation.js
+++ b/libs/features/dynamic-navigation/dynamic-navigation.js
@@ -14,16 +14,30 @@ export function foundDisableValues() {
return foundValues.length ? foundValues : false;
}
+function dynamicNavGroupMatches(groupMetaData) {
+ const storedGroup = window.sessionStorage.getItem('dynamicNavGroup');
+ if (groupMetaData && storedGroup) {
+ return storedGroup.toLowerCase() === groupMetaData.toLowerCase();
+ }
+ return false;
+}
+
export default function dynamicNav(url, key) {
if (foundDisableValues()) return url;
const metadataContent = getMetadata('dynamic-nav');
+ const dynamicNavGroup = getMetadata('dynamic-nav-group');
if (metadataContent === 'entry') {
window.sessionStorage.setItem('gnavSource', url);
window.sessionStorage.setItem('dynamicNavKey', key);
+ if (dynamicNavGroup) window.sessionStorage.setItem('dynamicNavGroup', dynamicNavGroup);
return url;
}
+ if (metadataContent === 'on' && dynamicNavGroup) {
+ if (!dynamicNavGroupMatches(dynamicNavGroup)) return url;
+ }
+
if (metadataContent !== 'on' || key !== window.sessionStorage.getItem('dynamicNavKey')) return url;
return window.sessionStorage.getItem('gnavSource') || url;
diff --git a/libs/features/dynamic-navigation/status.js b/libs/features/dynamic-navigation/status.js
index 792f586765..3321459581 100644
--- a/libs/features/dynamic-navigation/status.js
+++ b/libs/features/dynamic-navigation/status.js
@@ -80,7 +80,11 @@ const createStatusWidget = (dynamicNavKey) => {
const currentSource = getCurrentSource(dynamicNavSetting, storedSource, authoredSource);
const dynamicNavDisableValues = getMetadata('dynamic-nav-disable');
const foundValues = foundDisableValues();
- const status = getStatus(dynamicNavSetting, foundValues.length >= 1, storedSource);
+ const groupMetaSetting = getMetadata('dynamic-nav-group') || 'Group not set';
+ const groupsMatch = groupMetaSetting.toLowerCase() === window.sessionStorage.getItem('dynamicNavGroup').toLowerCase();
+ const groupsMatchMessage = groupsMatch ? 'Yes' : 'No';
+ const isDisabled = foundValues.length >= 1 || (!groupsMatch && groupMetaSetting !== 'Group not set');
+ const status = getStatus(dynamicNavSetting, isDisabled, storedSource);
const statusWidget = createTag('div', { class: 'dynamic-nav-status' });
statusWidget.innerHTML = `
@@ -94,6 +98,8 @@ const createStatusWidget = (dynamicNavKey) => {
Status: ${status}
Setting: ${dynamicNavSetting}
+ Group: ${groupMetaSetting}
+ Group matches stored group: ${groupsMatchMessage}
Consumer key: ${dynamicNavKey}
Authored and stored source match: ${authoredSource === currentSource}
diff --git a/test/features/dynamic-nav/dynamicNav.test.js b/test/features/dynamic-nav/dynamicNav.test.js
index a08c98ba22..710b9c78e2 100644
--- a/test/features/dynamic-nav/dynamicNav.test.js
+++ b/test/features/dynamic-nav/dynamicNav.test.js
@@ -58,6 +58,24 @@ describe('Dynamic nav', () => {
expect(url).to.equal('gnav/aem-sites');
});
+ it('Returns the provided url when the group has not been set', async () => {
+ document.head.innerHTML = await readFile({ path: './mocks/on.html' });
+ const url = dynamicNav('gnav/aem-sites', 'bacom');
+ expect(url).to.equal('some-source-string');
+ });
+
+ it('Returns the provided url when the group does not match', async () => {
+ document.head.innerHTML = await readFile({ path: './mocks/on.html' });
+ const groupMeta = document.createElement('meta');
+ groupMeta.setAttribute('name', 'dynamic-nav-group');
+ groupMeta.setAttribute('content', 'test');
+ document.head.appendChild(groupMeta);
+
+ window.sessionStorage.setItem('dynamicNavGroup', 'no-test');
+ const url = dynamicNav('gnav/aem-sites', 'bacom');
+ expect(url).to.equal('gnav/aem-sites');
+ });
+
it('Returns the sessionStorage url when dynamic nav ignore items are present but do not match the metadata', async () => {
document.head.innerHTML = await readFile({ path: './mocks/on-ignore-does-not-match.html' });
const url = dynamicNav('gnav/aem-sites', 'bacom');
@@ -75,4 +93,16 @@ describe('Dynamic nav', () => {
const url = dynamicNav('gnav/aem-sites', 'bacom');
expect(url).to.equal('some-source-string');
});
+
+ it('Returns the sessionStorage url when the groups match', async () => {
+ document.head.innerHTML = await readFile({ path: './mocks/on.html' });
+ const groupMeta = document.createElement('meta');
+ groupMeta.setAttribute('name', 'dynamic-nav-group');
+ groupMeta.setAttribute('content', 'test');
+ document.head.appendChild(groupMeta);
+
+ window.sessionStorage.setItem('dynamicNavGroup', 'test');
+ const url = dynamicNav('gnav/aem-sites', 'bacom');
+ expect(url).to.equal('some-source-string');
+ });
});
diff --git a/test/features/dynamic-nav/status.test.js b/test/features/dynamic-nav/status.test.js
index 2b9a261e61..6d43ae23a2 100644
--- a/test/features/dynamic-nav/status.test.js
+++ b/test/features/dynamic-nav/status.test.js
@@ -23,7 +23,8 @@ describe('Dynamic Nav Status', () => {
beforeEach(async () => {
const conf = { dynamicNavKey: 'bacom' };
document.body.innerHTML = await readFile({ path: './mocks/status.html' });
- document.head.innerHTML = '
';
+ document.head.innerHTML = '
';
+ window.sessionStorage.setItem('dynamicNavGroup', 'test');
setConfig(conf);
});
@@ -159,6 +160,73 @@ describe('Dynamic Nav Status', () => {
expect(info.authoredSource).to.equal('/test');
expect(info.storedSource).to.equal('/test');
});
+
+ it('displays the correct information for a group match', () => {
+ document.querySelector('meta[name="dynamic-nav"]').setAttribute('content', 'on');
+ document.querySelector('meta[name="gnav-source"]').setAttribute('content', 'https://main--milo--adobecom.hlx/test');
+
+ dynamicNav();
+ status();
+
+ const statusWidget = document.querySelector('.dynamic-nav-status');
+ const group = statusWidget.querySelector('.group span');
+ const groupMatch = statusWidget.querySelector('.group-match span');
+
+ expect(group.innerText).to.equal('test');
+ expect(groupMatch.innerText).to.equal('Yes');
+ expect(statusWidget.classList.contains(ENABLED)).to.be.true;
+ });
+
+ it('displays the correct information for a group mismatch', () => {
+ document.querySelector('meta[name="dynamic-nav"]').setAttribute('content', 'on');
+ document.querySelector('meta[name="gnav-source"]').setAttribute('content', 'https://main--milo--adobecom.hlx/test');
+
+ window.sessionStorage.setItem('dynamicNavGroup', 'no-test');
+
+ dynamicNav();
+ status();
+
+ const statusWidget = document.querySelector('.dynamic-nav-status');
+ const group = statusWidget.querySelector('.group span');
+ const groupMatch = statusWidget.querySelector('.group-match span');
+
+ expect(group.innerText).to.equal('test');
+ expect(groupMatch.innerText).to.equal('No');
+ expect(statusWidget.classList.contains(INACTIVE)).to.be.true;
+ });
+
+ it('displays the correct information for no group being set', () => {
+ document.querySelector('meta[name="dynamic-nav"]').setAttribute('content', 'on');
+ document.querySelector('meta[name="gnav-source"]').setAttribute('content', 'https://main--milo--adobecom.hlx/test');
+
+ document.querySelector('meta[name="dynamic-nav-group"]').remove();
+ window.sessionStorage.setItem('dynamicNavGroup', 'no-test');
+
+ dynamicNav();
+ status();
+
+ const statusWidget = document.querySelector('.dynamic-nav-status');
+ const group = statusWidget.querySelector('.group span');
+ const groupMatch = statusWidget.querySelector('.group-match span');
+
+ expect(group.innerText).to.equal('Group not set');
+ expect(groupMatch.innerText).to.equal('No');
+ });
+
+ it('remains active when there is no group match but the nav is active', () => {
+ document.querySelector('meta[name="dynamic-nav"]').setAttribute('content', 'on');
+ document.querySelector('meta[name="gnav-source"]').setAttribute('content', 'https://main--milo--adobecom.hlx/test');
+
+ document.querySelector('meta[name="dynamic-nav-group"]').remove();
+ window.sessionStorage.setItem('dynamicNavGroup', 'no-test');
+ window.sessionStorage.setItem('gnavSource', GNAV_SOURCE);
+
+ dynamicNav();
+ status();
+
+ const statusWidget = document.querySelector('.dynamic-nav-status');
+ expect(statusWidget.classList.contains(ACTIVE)).to.be.true;
+ });
});
describe('disabled values', () => {
From 0cca4dbd8c69c6cc29d14568d68c2e673983d3c2 Mon Sep 17 00:00:00 2001
From: Sean Archibeque
Date: Wed, 4 Dec 2024 02:18:35 -0700
Subject: [PATCH 03/11] MWPW-160774 - [Table] top variant (#3204)
* top variant handler
* adjust for requirements in sync
* update to handle element justification for top variant
---
libs/blocks/table/table.css | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/libs/blocks/table/table.css b/libs/blocks/table/table.css
index 87d889c3f0..19eb5beed9 100644
--- a/libs/blocks/table/table.css
+++ b/libs/blocks/table/table.css
@@ -47,6 +47,15 @@
justify-content: center;
}
+.table.top .section-row .col {
+ flex-direction: column;
+ justify-content: start;
+}
+
+.table.top.left .section-row .col {
+ align-items: start;
+}
+
.table:not(.merch) .col-1:not(:only-child) {
background-color: var(--color-gray-100);
}
From f8e87abe1b78fe097e99b688777711f3391c2ab3 Mon Sep 17 00:00:00 2001
From: Ruchika Sinha <69535463+Ruchika4@users.noreply.github.com>
Date: Wed, 4 Dec 2024 01:18:46 -0800
Subject: [PATCH 04/11] MWPW-163002-proxy marketing scripts for newsroom
(#3251)
proxy marketing scripts for newsroom
---
libs/martech/martech.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libs/martech/martech.js b/libs/martech/martech.js
index 74df28252c..80f5cfd319 100644
--- a/libs/martech/martech.js
+++ b/libs/martech/martech.js
@@ -179,7 +179,7 @@ const setupEntitlementCallback = () => {
};
function isProxied() {
- return /^(www|milo|business|blog)(\.stage)?\.adobe\.com$/.test(window.location.hostname);
+ return /^(www|milo|business|blog|news)(\.stage)?\.adobe\.com$/.test(window.location.hostname);
}
let filesLoadedPromise = false;
From 6e4138cac6e129e3aa54c82aa70a93a5d224e3bd Mon Sep 17 00:00:00 2001
From: sharathkannan <138484653+sharath-kannan@users.noreply.github.com>
Date: Wed, 4 Dec 2024 15:37:39 +0530
Subject: [PATCH 05/11] Feat(MWPW-146367):Added accessibility player controls
(NON MPC) (#3053)
* updated feature with accessibility code
* video accessiblity added for carousels
* added opt-out functionality
* fixed linting errors
* fixed unit test cases
* fixed adobe tv issue
* hover and focus added
* controls positioned for rtl
* hide-controls hash params added
* how to block controls position bug fix
* dark mode|bug fixes
* code enhancement
* pause-play bug mouse click bug fix
* marquee dark mode|positioning fix
* code enhancement
* handled marquee backward compatiblity
* Added placeholder for labels|indexed video aria-labels
* aria-label added for hover play videos
* async awaited decorateVideo in video.js and other linting errors
* video indexes added
* random video index and unit test cases updated
* daa-ll is synced along with aria-label
* code enhancement
* nala test fix|code coverege
* nala test bug fix
* nala test fix
* right-left positioning is done for screens > 600px and a img fix
* getFedsconfig moved to feds file|url fetched from fedRoot function
* linting fix
* icons adapted to the figma
* carousel and how-to fix with other minor fixes
* playpause wrapper adjusted for window
* icon offset bug fix
* indentation of string literal
* figma match
* figma focus match
---
libs/blocks/adobetv/adobetv.css | 1 +
libs/blocks/aside/aside.css | 1 +
libs/blocks/aside/aside.js | 4 +-
libs/blocks/brick/brick.css | 9 +
libs/blocks/carousel/carousel.css | 2 +-
libs/blocks/carousel/carousel.js | 8 +-
libs/blocks/figure/figure.js | 10 +-
libs/blocks/global-footer/global-footer.js | 3 +-
.../features/profile/dropdown.js | 3 +-
.../features/search/gnav-search.js | 2 +-
.../global-navigation/global-navigation.js | 6 +-
.../global-navigation/utilities/utilities.js | 20 +--
libs/blocks/hero-marquee/hero-marquee.js | 2 +-
libs/blocks/how-to/how-to.js | 2 +-
libs/blocks/marquee/marquee.js | 2 +-
libs/blocks/video/video.css | 149 +++++++++++++++++
libs/features/webapp-prompt/webapp-prompt.js | 2 +-
libs/utils/decorate.js | 158 +++++++++++++++++-
libs/utils/federated.js | 18 ++
nala/blocks/marquee/marquee.test.js | 10 +-
test/blocks/adobetv/adobetv.test.js | 6 +-
test/blocks/figure/figure.test.js | 3 +-
.../utilities/utilities.test.js | 2 +-
test/blocks/how-to/how-to.test.js | 2 +-
test/blocks/how-to/mocks/body.html | 2 +-
test/blocks/video/mocks/body.html | 9 +-
test/blocks/video/video.test.js | 135 ++++++++++++---
27 files changed, 493 insertions(+), 78 deletions(-)
diff --git a/libs/blocks/adobetv/adobetv.css b/libs/blocks/adobetv/adobetv.css
index 7907e053ca..28322d4585 100644
--- a/libs/blocks/adobetv/adobetv.css
+++ b/libs/blocks/adobetv/adobetv.css
@@ -1,4 +1,5 @@
@import url('../../styles/iframe.css');
+@import url('../video/video.css');
a[href*='.mp4'].hide-video {
visibility: hidden !important;
diff --git a/libs/blocks/aside/aside.css b/libs/blocks/aside/aside.css
index 7feb805ada..54615ee978 100644
--- a/libs/blocks/aside/aside.css
+++ b/libs/blocks/aside/aside.css
@@ -298,6 +298,7 @@
}
.aside.rounded-corners .foreground .image img,
+.aside.rounded-corners .foreground .image:not(:has(.video-container)) .pause-play-wrapper,
.aside.rounded-corners .foreground .image video {
border-radius: 16px;
}
diff --git a/libs/blocks/aside/aside.js b/libs/blocks/aside/aside.js
index 7519967de3..92ea2500d4 100644
--- a/libs/blocks/aside/aside.js
+++ b/libs/blocks/aside/aside.js
@@ -197,8 +197,8 @@ function decorateLayout(el) {
}
const foregroundImage = foreground.querySelector(':scope > div:not(.text) img')?.closest('div');
const bgImage = el.querySelector(':scope > div:not(.text):not(.foreground) img')?.closest('div');
- const foregroundMedia = foreground.querySelector(':scope > div:not(.text) video, :scope > div:not(.text) a:is([href*=".mp4"], [href*="tv.adobe.com"]), :scope > div:not(.text) iframe[src*="tv.adobe.com"]')?.closest('div');
-
+ const foregroundMedia = foreground.querySelector(':scope > div:not(.text) :is(.video-container, video, a[href*=".mp4"], a[href*="tv.adobe.com"]), :scope > div:not(.text) iframe[src*="tv.adobe.com"]')
+ ?.closest('div:not(.video-container)');
const bgMedia = el.querySelector(':scope > div:not(.text):not(.foreground) video, :scope > div:not(.text):not(.foreground) a:is([href*=".mp4"], [href*="tv.adobe.com"])')?.closest('div');
const image = foregroundImage ?? bgImage;
const asideMedia = foregroundMedia ?? bgMedia ?? image;
diff --git a/libs/blocks/brick/brick.css b/libs/blocks/brick/brick.css
index 43cd31584d..4ed08eb815 100644
--- a/libs/blocks/brick/brick.css
+++ b/libs/blocks/brick/brick.css
@@ -98,6 +98,10 @@
margin: 0;
}
+.brick .foreground div > .video-container {
+ margin: 0;
+}
+
.brick .foreground div > * {
margin-top: var(--spacing-xs);
}
@@ -342,6 +346,11 @@
position: absolute;
}
+ .brick.split.row .foreground .brick-media .video-container img,
+ .brick.split.row .foreground .brick-media .video-container video {
+ width: 100%;
+ }
+
.brick .foreground .brick-media video,
.brick.split.row .foreground .brick-media video {
object-fit: fill;
diff --git a/libs/blocks/carousel/carousel.css b/libs/blocks/carousel/carousel.css
index 41e70a1ef2..08bffdf704 100644
--- a/libs/blocks/carousel/carousel.css
+++ b/libs/blocks/carousel/carousel.css
@@ -397,7 +397,7 @@ html[dir="rtl"] .carousel-slides .section.carousel-slide {
overflow: hidden;
}
-.carousel .carousel-slide > div p > video {
+.carousel .carousel-slide > div p :is(.video-holder, video) {
width: 100%;
height: auto;
}
diff --git a/libs/blocks/carousel/carousel.js b/libs/blocks/carousel/carousel.js
index 1bd73281f6..4bb57f7180 100644
--- a/libs/blocks/carousel/carousel.js
+++ b/libs/blocks/carousel/carousel.js
@@ -168,7 +168,7 @@ function moveSlides(event, carouselElements, jumpToIndex) {
referenceSlide.classList.remove('reference-slide');
referenceSlide.style.order = null;
activeSlide.classList.remove('active');
- activeSlide.querySelectorAll('a').forEach((focusableElement) => { focusableElement.setAttribute('tabindex', -1); });
+ activeSlide.querySelectorAll('a, video').forEach((focusableElement) => focusableElement.setAttribute('tabindex', -1));
activeSlideIndicator.classList.remove('active');
activeSlideIndicator.setAttribute('tabindex', -1);
@@ -230,10 +230,12 @@ function moveSlides(event, carouselElements, jumpToIndex) {
if (index < show) {
tabIndex = 0;
}
- slide.querySelectorAll('a').forEach((focusableElement) => { focusableElement.setAttribute('tabindex', tabIndex); });
+ slide.querySelectorAll('a,:not(.video-container, .pause-play-wrapper) > video')
+ .forEach((focusableElement) => { focusableElement.setAttribute('tabindex', tabIndex); });
});
} else {
- activeSlide.querySelectorAll('a').forEach((focusableElement) => { focusableElement.setAttribute('tabindex', 0); });
+ activeSlide.querySelectorAll('a,:not(.video-container, .pause-play-wrapper) > video')
+ .forEach((focusableElement) => { focusableElement.setAttribute('tabindex', 0); });
}
activeSlideIndicator.classList.add('active');
activeSlideIndicator.setAttribute('tabindex', 0);
diff --git a/libs/blocks/figure/figure.js b/libs/blocks/figure/figure.js
index cd0d80cd88..40c9e40c28 100644
--- a/libs/blocks/figure/figure.js
+++ b/libs/blocks/figure/figure.js
@@ -1,4 +1,4 @@
-import { applyHoverPlay, decorateAnchorVideo } from '../../utils/decorate.js';
+import { applyHoverPlay, decorateAnchorVideo, applyAccessibilityEvents, decoratePausePlayWrapper, isVideoAccessible } from '../../utils/decorate.js';
import { createTag } from '../../utils/utils.js';
function buildCaption(pEl) {
@@ -31,7 +31,11 @@ function decorateVideo(clone, figEl) {
);
}
applyHoverPlay(videoTag);
- figEl.prepend(videoTag);
+ if (!videoTag.controls && isVideoAccessible(anchorTag)) {
+ applyAccessibilityEvents(videoTag);
+ decoratePausePlayWrapper(videoTag, 'autoplay');
+ }
+ figEl.prepend(clone.querySelector('.video-container, .pause-play-wrapper, video'));
}
}
@@ -68,7 +72,7 @@ export function buildFigure(blockEl) {
const link = clone.querySelector('a');
if (link) {
const img = figEl.querySelector('picture') || figEl.querySelector('video');
- if (img) {
+ if (img && !link.classList.contains('pause-play-wrapper')) {
// wrap picture or video in A tag
link.textContent = '';
link.append(img);
diff --git a/libs/blocks/global-footer/global-footer.js b/libs/blocks/global-footer/global-footer.js
index 6c74620291..21e18208cd 100644
--- a/libs/blocks/global-footer/global-footer.js
+++ b/libs/blocks/global-footer/global-footer.js
@@ -9,7 +9,6 @@ import {
} from '../../utils/utils.js';
import {
- getFedsPlaceholderConfig,
getExperienceName,
getAnalyticsValue,
loadDecorateMenu,
@@ -23,7 +22,7 @@ import {
isDarkMode,
} from '../global-navigation/utilities/utilities.js';
-import { getFederatedUrl } from '../../utils/federated.js';
+import { getFederatedUrl, getFedsPlaceholderConfig } from '../../utils/federated.js';
import { replaceKey } from '../../features/placeholders.js';
diff --git a/libs/blocks/global-navigation/features/profile/dropdown.js b/libs/blocks/global-navigation/features/profile/dropdown.js
index 836f9cd834..d874963e82 100644
--- a/libs/blocks/global-navigation/features/profile/dropdown.js
+++ b/libs/blocks/global-navigation/features/profile/dropdown.js
@@ -1,6 +1,7 @@
import { getConfig } from '../../../../utils/utils.js';
-import { toFragment, getFedsPlaceholderConfig, trigger, closeAllDropdowns, logErrorFor } from '../../utilities/utilities.js';
+import { toFragment, trigger, closeAllDropdowns, logErrorFor } from '../../utilities/utilities.js';
import { replaceKeyArray } from '../../../../features/placeholders.js';
+import { getFedsPlaceholderConfig } from '../../../../utils/federated.js';
const getLanguage = (ietfLocale) => {
if (!ietfLocale.length) return 'en';
diff --git a/libs/blocks/global-navigation/features/search/gnav-search.js b/libs/blocks/global-navigation/features/search/gnav-search.js
index ca6df552b8..76532c5d31 100644
--- a/libs/blocks/global-navigation/features/search/gnav-search.js
+++ b/libs/blocks/global-navigation/features/search/gnav-search.js
@@ -1,6 +1,5 @@
import {
toFragment,
- getFedsPlaceholderConfig,
isDesktop,
setCurtainState,
trigger,
@@ -10,6 +9,7 @@ import {
import { replaceKeyArray } from '../../../../features/placeholders.js';
import { getConfig } from '../../../../utils/utils.js';
import { debounce } from '../../../../utils/action.js';
+import { getFedsPlaceholderConfig } from '../../../../utils/federated.js';
const CONFIG = {
suggestions: {
diff --git a/libs/blocks/global-navigation/global-navigation.js b/libs/blocks/global-navigation/global-navigation.js
index 55815c5994..9d13051e86 100644
--- a/libs/blocks/global-navigation/global-navigation.js
+++ b/libs/blocks/global-navigation/global-navigation.js
@@ -14,7 +14,6 @@ import {
getActiveLink,
getAnalyticsValue,
getExperienceName,
- getFedsPlaceholderConfig,
hasActiveLink,
isActiveLink,
icons,
@@ -40,6 +39,7 @@ import {
setDisableAEDState,
getDisableAEDState,
} from './utilities/utilities.js';
+import { getFedsPlaceholderConfig } from '../../utils/federated.js';
import { replaceKey, replaceKeyArray } from '../../features/placeholders.js';
@@ -679,7 +679,7 @@ class Gnav {
return this.loadDelayed().then(() => {
this.blocks.search.instance = new this.Search(this.blocks.search.config);
- }).catch(() => {});
+ }).catch(() => { });
};
isToggleExpanded = () => this.elements.mobileToggle?.getAttribute('aria-expanded') === 'true';
@@ -773,7 +773,7 @@ class Gnav {
if (allSvgImgs.length === 2) return allSvgImgs[1];
const images = blockLinks.filter((blockLink) => imgRegex.test(blockLink.href)
- || imgRegex.test(blockLink.textContent));
+ || imgRegex.test(blockLink.textContent));
if (images.length === 2) return getBrandImage(images[1], isBrandImage);
}
const svgImg = rawBlock.querySelector('picture img[src$=".svg"]');
diff --git a/libs/blocks/global-navigation/utilities/utilities.js b/libs/blocks/global-navigation/utilities/utilities.js
index 370ae1f2ca..f880d14ae1 100644
--- a/libs/blocks/global-navigation/utilities/utilities.js
+++ b/libs/blocks/global-navigation/utilities/utilities.js
@@ -2,7 +2,7 @@
import {
getConfig, getMetadata, loadStyle, loadLana, decorateLinks, localizeLink,
} from '../../../utils/utils.js';
-import { getFederatedContentRoot, getFederatedUrl } from '../../../utils/federated.js';
+import { getFederatedContentRoot, getFederatedUrl, getFedsPlaceholderConfig } from '../../../utils/federated.js';
import { processTrackingLabels } from '../../../martech/attributes.js';
import { replaceText } from '../../../features/placeholders.js';
@@ -108,24 +108,6 @@ export const federatePictureSources = ({ section, forceFederate } = {}) => {
});
};
-let fedsPlaceholderConfig;
-export const getFedsPlaceholderConfig = ({ useCache = true } = {}) => {
- if (useCache && fedsPlaceholderConfig) return fedsPlaceholderConfig;
-
- const { locale, placeholders } = getConfig();
- const libOrigin = getFederatedContentRoot();
-
- fedsPlaceholderConfig = {
- locale: {
- ...locale,
- contentRoot: `${libOrigin}${locale.prefix}/federal/globalnav`,
- },
- placeholders,
- };
-
- return fedsPlaceholderConfig;
-};
-
export function getAnalyticsValue(str, index) {
if (typeof str !== 'string' || !str.length) return str;
diff --git a/libs/blocks/hero-marquee/hero-marquee.js b/libs/blocks/hero-marquee/hero-marquee.js
index c9f409757d..f747559e29 100644
--- a/libs/blocks/hero-marquee/hero-marquee.js
+++ b/libs/blocks/hero-marquee/hero-marquee.js
@@ -187,7 +187,7 @@ export default async function init(el) {
foreground.classList.add('foreground', `cols-${fRows.length}`);
let copy = fRows[0];
const anyTag = foreground.querySelector('p, h1, h2, h3, h4, h5, h6');
- const asset = foreground.querySelector('div > picture, div > video, div > a[href*=".mp4"], div > a.image-link');
+ const asset = foreground.querySelector('div > picture, :is(.video-container, .pause-play-wrapper), div > video, div > a[href*=".mp4"], div > a.image-link');
const allRows = foreground.querySelectorAll('div > div');
copy = anyTag.closest('div');
copy.classList.add('copy');
diff --git a/libs/blocks/how-to/how-to.js b/libs/blocks/how-to/how-to.js
index ce7cf146df..8080319fd0 100644
--- a/libs/blocks/how-to/how-to.js
+++ b/libs/blocks/how-to/how-to.js
@@ -47,7 +47,7 @@ const setJsonLd = (heading, description, mainImage, stepsLd) => {
};
const getImage = (el) => el.querySelector('picture') || el.querySelector('a[href$=".svg"');
-const getVideo = (el) => el.querySelector('video') || el.querySelector('.milo-video');
+const getVideo = (el) => el.querySelector('.video-container, .pause-play-wrapper, video, .milo-video');
const getHowToInfo = (el) => {
const infoDiv = el.querySelector(':scope > div > div');
diff --git a/libs/blocks/marquee/marquee.js b/libs/blocks/marquee/marquee.js
index d747586fd3..21c3e8c8a5 100644
--- a/libs/blocks/marquee/marquee.js
+++ b/libs/blocks/marquee/marquee.js
@@ -88,7 +88,7 @@ function decorateSplit(el, foreground, media) {
let mediaCreditInner;
const txtContent = media?.lastChild?.textContent?.trim();
- if (txtContent?.match(/^http.*\.mp4/) || media?.lastChild?.tagName === 'VIDEO') return;
+ if (txtContent?.match(/^http.*\.mp4/) || media?.lastChild?.tagName === 'VIDEO' || media.querySelector('.video-holder video')) return;
if (txtContent) {
mediaCreditInner = createTag('p', { class: 'body-s' }, txtContent);
} else if (media.lastElementChild?.tagName !== 'PICTURE') {
diff --git a/libs/blocks/video/video.css b/libs/blocks/video/video.css
index f6ec58c065..6538e0596a 100644
--- a/libs/blocks/video/video.css
+++ b/libs/blocks/video/video.css
@@ -5,4 +5,153 @@ a[href*='.mp4'].hide-video {
video {
max-width: 100%;
height: auto;
+ object-fit: cover;
+}
+
+:is(.marquee, .aside.split) .pause-play-wrapper img.accessibility-control {
+ min-height: 40px;
+}
+
+.video-container {
+ display: flex;
+ position: relative;
+ height: 100%;
+ width: fit-content;
+ margin: auto;
+}
+
+:is(.aside, .marquee, .quiz-marquee) .video-container {
+ width: auto;
+}
+
+.brick-media .video-container {
+ width: 100%;
+ height: 100%;
+ border-radius: inherit;
+}
+
+.pause-play-wrapper {
+ display: flex;
+ width: fit-content;
+ margin: auto;
+}
+
+.video-container .pause-play-wrapper {
+ position: absolute;
+ bottom: 2%;
+ right: 2%;
+ margin: 0;
+ justify-content: center;
+ align-items: center;
+ border-radius: 50%;
+ z-index: 2;
+ padding: 3px;
+ cursor: pointer;
+}
+
+.video-container .pause-play-wrapper .offset-filler {
+ display: inherit;
+ justify-content: center;
+ align-items: center;
+ width: 40px;
+ height: 40px;
+ border-radius: inherit;
+ background: var(--color-gray-800);
+}
+
+:is(.marquee:not(.light), .dark) .video-container .pause-play-wrapper {
+ padding: 1px;
+}
+
+:is(.marquee:not(.light), .dark) .video-container .pause-play-wrapper:focus-visible {
+ background: #000;
+}
+
+:is(.marquee:not(.light), .dark) .video-container .pause-play-wrapper .offset-filler {
+ border: 2px solid #fff;
+}
+
+.video-container .pause-play-wrapper:focus-visible {
+ background: #fff;
+}
+
+.video-container .pause-play-wrapper .offset-filler:hover {
+ background: #000;
+}
+
+.video-container .pause-play-wrapper:focus-visible {
+ outline: var(--color-accent-focus-ring) solid 2px;
+}
+
+.video-container .pause-play-wrapper .offset-filler.is-playing .play-icon,
+.video-container .pause-play-wrapper .offset-filler:not(.is-playing) .pause-icon {
+ display: none;
+}
+
+:is(.editorial-card, .hero-marquee, .marquee):not(:has(.video-container)) .pause-play-wrapper,
+.editorial-card .video-container {
+ width: auto;
+ height: 100%;
+}
+
+.brick .brick-media:not(:has(.video-container)) .pause-play-wrapper {
+ border-radius: 0;
+ border-top-right-radius: inherit;
+ border-bottom-right-radius: inherit;
+ width: auto;
+ height: 100%;
+ margin: 0;
+}
+
+[dir="rtl"] .brick .brick-media:not(:has(.video-container)) .pause-play-wrapper {
+ border-top-left-radius: inherit;
+ border-bottom-left-radius: inherit;
+}
+
+.hero-marquee .background .video-container {
+ position: inherit;
+}
+
+:is(.video-container .pause-play-wrapper, .aside.split.split-left .split-image) img.accessibility-control {
+ width: auto;
+}
+
+.video-container .pause-play-wrapper img.hidden {
+ display: none;
+}
+
+.marquee .background .video-container {
+ display: contents;
+}
+
+.how-to .how-to-media .video-container {
+ height: fit-content;
+}
+
+@media (min-width: 600px) {
+ .media:not(.media-reverse-mobile, .media-reversed) .video-container .pause-play-wrapper,
+ :is(.marquee.row-reversed .asset, .marquee-anchors, .hero-marquee.asset-left) .video-container .pause-play-wrapper,
+ :is(.aside:not(.split), .aside.split.split-right) .video-container .pause-play-wrapper {
+ left: 2%;
+ }
+
+ :is(.section[class*="-up"] .media .foreground .image:first-child, .aside .foreground .image:nth-last-child(1))
+ .video-container .pause-play-wrapper {
+ left: auto;
+ right: 2%;
+ }
+
+ [dir="rtl"] :is(.marquee.split:not(.row-reversed), .media:is(.media-reverse-mobile, .media-reversed),
+ .hero-marquee:not(.asset-left) :is([class*="foreground"]), .aside.split:not(.split-right),
+ .aside .foreground.container .image:nth-last-child(1), .brick.media-right, .how-to) .video-container .pause-play-wrapper {
+ left: 2%;
+ right: auto;
+ }
+}
+
+@media (min-width: 600px) and (max-width: 1199px) {
+ .hero-marquee.asset-left .video-container.video-holder .pause-play-wrapper {
+ left: auto;
+ right: 2%;
+ }
}
diff --git a/libs/features/webapp-prompt/webapp-prompt.js b/libs/features/webapp-prompt/webapp-prompt.js
index ea9bc072d0..894d010d0b 100644
--- a/libs/features/webapp-prompt/webapp-prompt.js
+++ b/libs/features/webapp-prompt/webapp-prompt.js
@@ -1,5 +1,4 @@
import {
- getFedsPlaceholderConfig,
getUserProfile,
icons,
lanaLog,
@@ -7,6 +6,7 @@ import {
} from '../../blocks/global-navigation/utilities/utilities.js';
import { getConfig, decorateSVG } from '../../utils/utils.js';
import { replaceKey, replaceText } from '../placeholders.js';
+import { getFedsPlaceholderConfig } from '../../utils/federated.js';
export const DISMISSAL_CONFIG = {
animationCount: 2,
diff --git a/libs/utils/decorate.js b/libs/utils/decorate.js
index 36e23dd055..580d2fcc1f 100644
--- a/libs/utils/decorate.js
+++ b/libs/utils/decorate.js
@@ -1,6 +1,17 @@
import { createTag, loadStyle, getConfig, createIntersectionObserver } from './utils.js';
+import { getFederatedContentRoot, getFedsPlaceholderConfig } from './federated.js';
const { miloLibs, codeRoot } = getConfig();
+const HIDE_CONTROLS = '_hide-controls';
+let firstVideo = null;
+let videoLabels = {
+ playMotion: 'Play',
+ pauseMotion: 'Pause',
+ pauseIcon: 'Pause icon',
+ playIcon: 'Play icon',
+ hasFetched: false,
+};
+let videoCounter = 0;
export function decorateButtons(el, size) {
const buttons = el.querySelectorAll('em a, strong a, p > a strong');
@@ -209,7 +220,7 @@ export function getImgSrc(pic) {
return source?.srcset ? `poster='${source.srcset}'` : '';
}
-function getVideoAttrs(hash, dataset) {
+export function getVideoAttrs(hash, dataset) {
const isAutoplay = hash?.includes('autoplay');
const isAutoplayOnce = hash?.includes('autoplay1');
const playOnHover = hash?.includes('hoverplay');
@@ -234,12 +245,80 @@ function getVideoAttrs(hash, dataset) {
return `${globalAttrs} controls`;
}
+export function syncPausePlayIcon(video) {
+ if (!video.getAttributeNames().includes('data-hoverplay')) {
+ const offsetFiller = video.closest('.video-holder').querySelector('.offset-filler');
+ const anchorTag = video.closest('.video-holder').querySelector('a');
+ offsetFiller?.classList.toggle('is-playing');
+ const isPlaying = offsetFiller?.classList.contains('is-playing');
+ const indexOfVideo = (anchorTag.getAttribute('video-index') === '1' && videoCounter === 1) ? '' : anchorTag.getAttribute('video-index');
+ const changedLabel = `${isPlaying ? videoLabels?.pauseMotion : videoLabels?.playMotion}`;
+ const oldLabel = `${!isPlaying ? videoLabels?.pauseMotion : videoLabels?.playMotion}`;
+ const ariaLabel = `${changedLabel} ${indexOfVideo}`.trim();
+ anchorTag?.setAttribute('aria-label', `${ariaLabel} `);
+ anchorTag?.setAttribute('aria-pressed', isPlaying ? 'true' : 'false');
+ const daaLL = anchorTag.getAttribute('daa-ll');
+ if (daaLL) anchorTag.setAttribute('daa-ll', daaLL.replace(oldLabel, changedLabel));
+ }
+}
+
+export function addAccessibilityControl(videoString, videoAttrs, indexOfVideo, tabIndex = 0) {
+ if (videoAttrs.includes('controls')) return videoString;
+ const fedRoot = getFederatedContentRoot();
+ if (videoAttrs.includes('hoverplay')) {
+ return `${videoString}`;
+ }
+ return `
+
+ `;
+}
+
+export function handlePause(event) {
+ event.stopPropagation();
+ if (event.code !== 'Enter' && event.code !== 'Space' && !['focus', 'click', 'blur'].includes(event.type)) {
+ return;
+ }
+ event.preventDefault();
+ const video = event.target.closest('.video-holder').parentElement.querySelector('video');
+ if (event.type === 'blur') {
+ video.pause();
+ } else if (video.paused || video.ended || event.type === 'focus') {
+ video.play();
+ } else {
+ video.pause();
+ }
+ syncPausePlayIcon(video);
+}
+
export function applyHoverPlay(video) {
if (!video) return;
- if (video.hasAttribute('data-hoverplay') && !video.hasAttribute('data-mouseevent')) {
- video.addEventListener('mouseenter', () => { video.play(); });
- video.addEventListener('mouseleave', () => { video.pause(); });
- video.setAttribute('data-mouseevent', true);
+ if (video.hasAttribute('data-hoverplay')) {
+ video.parentElement.addEventListener('focus', handlePause);
+ video.parentElement.addEventListener('blur', handlePause);
+ if (!video.hasAttribute('data-mouseevent')) {
+ video.addEventListener('mouseenter', () => { video.play(); });
+ video.addEventListener('mouseleave', () => { video.pause(); });
+ video.addEventListener('ended', () => { syncPausePlayIcon(video); });
+ video.setAttribute('data-mouseevent', true);
+ }
+ }
+}
+
+export function applyAccessibilityEvents(videoEl) {
+ const pausePlayWrapper = videoEl.parentElement.querySelector('.pause-play-wrapper') || videoEl.closest('.pause-play-wrapper');
+ if (pausePlayWrapper?.querySelector('.accessibility-control')) {
+ pausePlayWrapper.addEventListener('click', handlePause);
+ pausePlayWrapper.addEventListener('keydown', handlePause);
+ }
+ if (videoEl.hasAttribute('autoplay')) {
+ videoEl.addEventListener('ended', () => { syncPausePlayIcon(videoEl); });
}
}
@@ -275,7 +354,7 @@ function getVideoIntersectionObserver() {
const isHaveLoopAttr = video.getAttributeNames().includes('loop');
const { playedOnce = false } = video.dataset;
const isPlaying = video.currentTime > 0 && !video.paused && !video.ended
- && video.readyState > video.HAVE_CURRENT_DATA;
+ && video.readyState > video.HAVE_CURRENT_DATA;
if (intersectionRatio <= 0.8) {
video.pause();
@@ -331,13 +410,72 @@ export async function loadCDT(el, classList) {
}
}
+export function isVideoAccessible(anchorTag) {
+ return !anchorTag?.hash.includes(HIDE_CONTROLS);
+}
+
+function updateFirstVideo() {
+ if (firstVideo != null && firstVideo?.controls === false && videoCounter > 1) {
+ let videoHolder = document.querySelector('[video-index="1"]') || firstVideo.closest('.video-holder');
+ if (videoHolder.nodeName !== 'A') videoHolder = videoHolder.querySelector('a.pause-play-wrapper');
+ const firstVideoLabel = videoHolder.getAttribute('aria-label');
+ videoHolder.setAttribute('aria-label', `${firstVideoLabel} 1`);
+ firstVideo = null;
+ }
+}
+
+function updateAriaLabel(videoEl, videoAttrs) {
+ if (!videoEl.getAttributeNames().includes('data-hoverplay')) {
+ const pausePlayWrapper = videoEl.parentElement.querySelector('.pause-play-wrapper') || videoEl.closest('.pause-play-wrapper');
+ const pauseIcon = pausePlayWrapper.querySelector('.pause-icon');
+ const playIcon = pausePlayWrapper.querySelector('.play-icon');
+ const indexOfVideo = pausePlayWrapper.getAttribute('video-index');
+ let ariaLabel = `${videoAttrs.includes('autoplay') ? videoLabels.pauseMotion : videoLabels.playMotion}`;
+ ariaLabel = ariaLabel.concat(` ${indexOfVideo === '1' && videoCounter === 1 ? '' : indexOfVideo}`);
+ pausePlayWrapper.setAttribute('aria-label', ariaLabel);
+ pauseIcon.setAttribute('alt', videoLabels.pauseMotion);
+ playIcon.setAttribute('alt', videoLabels.playMotion);
+ updateFirstVideo();
+ }
+}
+
+export function decoratePausePlayWrapper(videoEl, videoAttrs) {
+ if (!videoLabels.hasFetched) {
+ import('../features/placeholders.js').then(({ replaceKeyArray }) => {
+ replaceKeyArray(['pause-motion', 'play-motion', 'pause-icon', 'play-icon'], getFedsPlaceholderConfig())
+ .then(([pauseMotion, playMotion, pauseIcon, playIcon]) => {
+ videoLabels = { playMotion, pauseMotion, pauseIcon, playIcon };
+ videoLabels.hasFetched = true;
+ updateAriaLabel(videoEl, videoAttrs);
+ });
+ });
+ } else {
+ updateAriaLabel(videoEl, videoAttrs);
+ }
+}
+
export function decorateAnchorVideo({ src = '', anchorTag }) {
if (!src.length || !(anchorTag instanceof HTMLElement)) return;
+ const accessibilityEnabled = isVideoAccessible(anchorTag);
+ anchorTag.hash = anchorTag.hash.replace(`#${HIDE_CONTROLS}`, '');
if (anchorTag.closest('.marquee, .aside, .hero-marquee, .quiz-marquee') && !anchorTag.hash) anchorTag.hash = '#autoplay';
const { dataset, parentElement } = anchorTag;
- const video = ``;
+ const attrs = getVideoAttrs(anchorTag.hash, dataset);
+ const tabIndex = anchorTag.tabIndex || 0;
+ const videoIndex = (tabIndex === -1) ? 'tabindex=-1' : '';
+ let video = ``;
+ if (!attrs.includes('controls') && !attrs.includes('hoverplay') && accessibilityEnabled) {
+ videoCounter += 1;
+ }
+ const indexOfVideo = videoCounter;
+ if (accessibilityEnabled) {
+ video = addAccessibilityControl(video, attrs, indexOfVideo, tabIndex);
+ }
anchorTag.insertAdjacentHTML('afterend', video);
const videoEl = parentElement.querySelector('video');
+ if (indexOfVideo === 1) {
+ firstVideo = videoEl;
+ }
createIntersectionObserver({
el: videoEl,
options: { rootMargin: '1000px' },
@@ -345,6 +483,12 @@ export function decorateAnchorVideo({ src = '', anchorTag }) {
videoEl?.appendChild(createTag('source', { src, type: 'video/mp4' }));
},
});
+ if (accessibilityEnabled) {
+ applyAccessibilityEvents(videoEl);
+ if (!videoEl.controls) {
+ decoratePausePlayWrapper(videoEl, attrs);
+ }
+ }
applyHoverPlay(videoEl);
applyInViewPortPlay(videoEl);
anchorTag.remove();
diff --git a/libs/utils/federated.js b/libs/utils/federated.js
index 759f518eb0..c3e3d3d24e 100644
--- a/libs/utils/federated.js
+++ b/libs/utils/federated.js
@@ -39,3 +39,21 @@ export const getFederatedUrl = (url = '') => {
}
return url;
};
+
+let fedsPlaceholderConfig;
+export const getFedsPlaceholderConfig = ({ useCache = true } = {}) => {
+ if (useCache && fedsPlaceholderConfig) return fedsPlaceholderConfig;
+
+ const { locale, placeholders } = getConfig();
+ const libOrigin = getFederatedContentRoot();
+
+ fedsPlaceholderConfig = {
+ locale: {
+ ...locale,
+ contentRoot: `${libOrigin}${locale.prefix}/federal/globalnav`,
+ },
+ placeholders,
+ };
+
+ return fedsPlaceholderConfig;
+};
diff --git a/nala/blocks/marquee/marquee.test.js b/nala/blocks/marquee/marquee.test.js
index 246503a78b..9f9905e41d 100644
--- a/nala/blocks/marquee/marquee.test.js
+++ b/nala/blocks/marquee/marquee.test.js
@@ -543,7 +543,7 @@ test.describe('Milo Marquee Block test suite', () => {
await test.step('step-3: Verify analytic attributes', async () => {
await expect(await marquee.marqueeSmallDark).toHaveAttribute('daa-lh', await webUtil.getBlockDaalh('marquee', 1));
- await expect(await marquee.blueButtonL).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.blueButtonText, 1, data.h2Text));
+ await expect(await marquee.blueButtonL).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.blueButtonText, 2, data.h2Text));
});
await test.step('step-4: Verify the accessibility test on the Marquee (small) background video playsinline block', async () => {
@@ -570,7 +570,7 @@ test.describe('Milo Marquee Block test suite', () => {
await expect(await marquee.headingXXL).toContainText(data.h2Text);
await expect(await marquee.bodyXL).toContainText(data.bodyText);
await expect(await marquee.blueButtonXL).toContainText(data.blueButtonText);
- await expect(await marquee.actionLink2).toContainText(data.linkText);
+ await expect(await marquee.actionLink3).toContainText(data.linkText);
await expect(await marquee.backgroundVideoDesktop).toBeVisible();
expect(await webUtil.verifyAttributes(marquee.backgroundVideoDesktop, marquee.attributes['backgroundVideo.inline'])).toBeTruthy();
@@ -581,8 +581,8 @@ test.describe('Milo Marquee Block test suite', () => {
await test.step('step-3: Verify analytic attributes', async () => {
await expect(await marquee.marqueeLargeLight).toHaveAttribute('daa-lh', await webUtil.getBlockDaalh('marquee', 1));
- await expect(await marquee.blueButtonXL).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.blueButtonText, 1, data.h2Text));
- await expect(await marquee.actionLink2).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.linkText, 2, data.h2Text));
+ await expect(await marquee.blueButtonXL).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.blueButtonText, 2, data.h2Text));
+ await expect(await marquee.actionLink3).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.linkText, 3, data.h2Text));
});
});
@@ -615,7 +615,7 @@ test.describe('Milo Marquee Block test suite', () => {
await test.step('step-3: Verify analytic attributes', async () => {
await expect(await marquee.marqueeLargeDark).toHaveAttribute('daa-lh', await webUtil.getBlockDaalh('marquee', 2));
- await expect(await marquee.blueButtonXL).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.blueButtonText, 1, data.h2Text));
+ await expect(await marquee.blueButtonXL).toHaveAttribute('daa-ll', await webUtil.getLinkDaall(data.blueButtonText, 2, data.h2Text));
});
});
diff --git a/test/blocks/adobetv/adobetv.test.js b/test/blocks/adobetv/adobetv.test.js
index a54528e496..223622c7bd 100644
--- a/test/blocks/adobetv/adobetv.test.js
+++ b/test/blocks/adobetv/adobetv.test.js
@@ -1,8 +1,10 @@
import { readFile } from '@web/test-runner-commands';
import { expect } from '@esm-bundle/chai';
import { waitForElement } from '../../helpers/waitfor.js';
+import { setConfig } from '../../../libs/utils/utils.js';
document.body.innerHTML = await readFile({ path: './mocks/body.html' });
+setConfig({});
const { default: init } = await import('../../../libs/blocks/adobetv/adobetv.js');
describe('adobetv autoblock', () => {
@@ -10,7 +12,7 @@ describe('adobetv autoblock', () => {
const wrapper = document.body.querySelector('.adobe-tv');
const a = wrapper.querySelector(':scope > a');
- init(a);
+ await init(a);
const iframe = await waitForElement('.adobe-tv iframe');
expect(wrapper.querySelector(':scope > a')).to.be.null;
expect(iframe).to.be.exist;
@@ -20,7 +22,7 @@ describe('adobetv autoblock', () => {
const wrapper = document.body.querySelector('#adobetvAsBg');
const a = wrapper.querySelector(':scope a[href*=".mp4"]');
- init(a);
+ await init(a);
const video = await waitForElement('#adobetvAsBg video');
expect(wrapper.querySelector(':scope a[href*=".mp4"]')).to.be.null;
expect(video).to.be.exist;
diff --git a/test/blocks/figure/figure.test.js b/test/blocks/figure/figure.test.js
index 61ee37b909..6f9d7ab0bf 100644
--- a/test/blocks/figure/figure.test.js
+++ b/test/blocks/figure/figure.test.js
@@ -1,10 +1,11 @@
import { expect } from '@esm-bundle/chai';
import { readFile } from '@web/test-runner-commands';
+import { setConfig } from '../../../libs/utils/utils.js';
document.body.innerHTML = await readFile({ path: './mocks/body.html' });
const ogDocument = document.body.innerHTML;
-
const { default: init } = await import('../../../libs/blocks/figure/figure.js');
+setConfig({});
describe('init', () => {
afterEach(() => {
diff --git a/test/blocks/global-navigation/utilities/utilities.test.js b/test/blocks/global-navigation/utilities/utilities.test.js
index c50aec5b2b..895a2ea6e2 100644
--- a/test/blocks/global-navigation/utilities/utilities.test.js
+++ b/test/blocks/global-navigation/utilities/utilities.test.js
@@ -3,7 +3,6 @@ import sinon from 'sinon';
import {
fetchAndProcessPlainHtml,
toFragment,
- getFedsPlaceholderConfig,
federatePictureSources,
getAnalyticsValue,
decorateCta,
@@ -18,6 +17,7 @@ import {
import { setConfig, getConfig } from '../../../../libs/utils/utils.js';
import { createFullGlobalNavigation, config } from '../test-utilities.js';
import mepInBlock from '../mocks/mep-config.js';
+import { getFedsPlaceholderConfig } from '../../../../libs/utils/federated.js';
const baseHost = 'https://main--federal--adobecom.aem.page';
describe('global navigation utilities', () => {
diff --git a/test/blocks/how-to/how-to.test.js b/test/blocks/how-to/how-to.test.js
index fb20b1f962..1e0207d3cf 100644
--- a/test/blocks/how-to/how-to.test.js
+++ b/test/blocks/how-to/how-to.test.js
@@ -105,7 +105,7 @@ describe('How To', () => {
it('Mp4 Link video', async () => {
const howTo = document.getElementById('test6');
- videoinit(howTo.querySelector('a'));
+ await videoinit(howTo.querySelector('a'));
init(howTo);
const video = howTo.querySelector('video');
expect(video).to.exist;
diff --git a/test/blocks/how-to/mocks/body.html b/test/blocks/how-to/mocks/body.html
index ff4fe17059..1c2525625c 100644
--- a/test/blocks/how-to/mocks/body.html
+++ b/test/blocks/how-to/mocks/body.html
@@ -117,7 +117,7 @@
diff --git a/test/blocks/video/mocks/body.html b/test/blocks/video/mocks/body.html
index b3f1963c85..1c59b91e82 100644
--- a/test/blocks/video/mocks/body.html
+++ b/test/blocks/video/mocks/body.html
@@ -16,7 +16,7 @@
-
+
@@ -86,4 +86,9 @@
https://main--milo--adobecom.hlx.page/media_1e798d01c6ddc7e7eadc8f134d69e4f8d7193fdbb6.mp4#autoplay1#_hoverplay#viewportplay
+
diff --git a/test/blocks/video/video.test.js b/test/blocks/video/video.test.js
index e55c4e2915..c4e167cb20 100644
--- a/test/blocks/video/video.test.js
+++ b/test/blocks/video/video.test.js
@@ -4,20 +4,44 @@ import sinon from 'sinon';
import { waitFor, waitForElement } from '../../helpers/waitfor.js';
import { setConfig, createTag } from '../../../libs/utils/utils.js';
-import { decorateAnchorVideo } from '../../../libs/utils/decorate.js';
+import { decorateAnchorVideo, handlePause, applyHoverPlay, decoratePausePlayWrapper } from '../../../libs/utils/decorate.js';
setConfig({});
const { default: init } = await import('../../../libs/blocks/video/video.js');
describe('video uploaded using franklin bot', () => {
+ let clock;
+ const callback = sinon.spy();
beforeEach(async () => {
+ clock = sinon.useFakeTimers({
+ toFake: ['setTimeout'],
+ shouldAdvanceTime: true,
+ });
document.body.innerHTML = await readFile({ path: './mocks/body.html' });
});
afterEach(() => {
+ clock.restore();
document.body.innerHTML = '';
});
+ it('aria-label should not have index when page has only one video', async () => {
+ const block = document.querySelector('.video.autoplay.single');
+ const block2 = document.querySelector('.video.autoplay.second');
+ const a = block.querySelector('a');
+ const a2 = block2.querySelector('a');
+ init(a);
+ setTimeout(callback, 600);
+ await clock.runAllAsync();
+ const pausePlayWrapper = block.querySelector('.pause-play-wrapper');
+ pausePlayWrapper.removeAttribute('video-index');
+ init(a2);
+ setTimeout(callback, 500);
+ await clock.runAllAsync();
+ const videoIndex = pausePlayWrapper.getAttribute('video-index');
+ expect(videoIndex).to.be.null;
+ });
+
it('removes the element, if it does not have a parent node', (done) => {
const anchor = createTag('a');
anchor.remove = () => done();
@@ -114,6 +138,85 @@ describe('video uploaded using franklin bot', () => {
expect(video.hasAttribute('data-play-viewport')).to.be.true;
});
+ it('accessibility controls should pause autoplay videos', async () => {
+ const block = document.querySelector('.video.autoplay.viewportplay');
+ const fetchStub = sinon.stub(window, 'fetch');
+ fetchStub.resolves({
+ total: 19,
+ offset: 0,
+ limit: 19,
+ data: [
+ {
+ key: 'play-motion',
+ value: 'Play',
+ },
+ {
+ key: 'pause-motion',
+ value: 'Pause',
+ },
+ {
+ key: 'play-icon',
+ value: 'play icon',
+ },
+ {
+ key: 'pause-icon',
+ value: 'pause icon',
+ },
+ ],
+ ':type': 'sheet',
+ });
+
+ const a = block.querySelector('a');
+ init(a);
+ const video = block.querySelector('video');
+ decoratePausePlayWrapper(video, '');
+ const pausePlayWrapper = block.querySelector('.pause-play-wrapper');
+ pausePlayWrapper.click();
+ setTimeout(callback, 500);
+ await clock.runAllAsync();
+ expect(pausePlayWrapper.ariaPressed).to.eql('false');
+ });
+
+ it('accessibility controls should play autoplay videos after pausing', async () => {
+ const block = document.querySelector('.video.autoplay.viewportplay');
+ const a = block.querySelector('a');
+ init(a);
+ const pausePlayWrapper = block.querySelector('.pause-play-wrapper');
+ pausePlayWrapper.click();
+ pausePlayWrapper.setAttribute('daa-ll', 'pause-motion');
+ setTimeout(callback, 500);
+ await clock.runAllAsync();
+ pausePlayWrapper.click();
+ expect(pausePlayWrapper.querySelector('.is-playing')).to.exist;
+ });
+
+ it('handlePause should return undefined if called with unknown event', async () => {
+ const event = {};
+ event.stopPropagation = sinon.stub();
+ const x = handlePause(event);
+ expect(x).to.be.undefined;
+ });
+
+ it('video should be paused on focus out or blur', async () => {
+ const block = document.querySelector('.video.autoplay1.hoverplay.no-viewportplay');
+ const a = block.querySelector('a');
+ init(a);
+ setTimeout(callback, 0);
+ await clock.runAllAsync();
+ const pausePlayWrapper = block.querySelector('.pause-play-wrapper');
+ const video = block.querySelector('video');
+ pausePlayWrapper.focus();
+ setTimeout(callback, 0);
+ await clock.runAllAsync();
+ pausePlayWrapper.blur();
+ expect(video.paused).to.be.true;
+ });
+
+ it('should return undefined if video is not present', async () => {
+ const returnValue = applyHoverPlay();
+ expect(returnValue).to.be.undefined;
+ });
+
it('play video when element reached 80% viewport', async () => {
const block = document.querySelector('.video.autoplay.viewportplay.scrolled-80');
const a = block.querySelector('a');
@@ -130,9 +233,8 @@ describe('video uploaded using franklin bot', () => {
await waitFor(intersectionObserverAddsSource);
video.scrollIntoView();
await nextFrame();
- await new Promise((resolve) => {
- setTimeout(resolve, 100);
- });
+ setTimeout(callback, 100);
+ await clock.runAllAsync();
assert.isTrue(playSpy.calledOnce);
// push the video out of the viewport
@@ -141,9 +243,8 @@ describe('video uploaded using franklin bot', () => {
video.parentNode.insertBefore(div, video);
await nextFrame();
- await new Promise((resolve) => {
- setTimeout(resolve, 100);
- });
+ setTimeout(callback, 100);
+ await clock.runAllAsync();
assert.isTrue(pauseSpy.calledOnce);
expect(video.hasAttribute('data-play-viewport')).to.be.true;
});
@@ -167,9 +268,8 @@ describe('video uploaded using franklin bot', () => {
video.addEventListener('ended', endedSpy);
video.scrollIntoView();
await nextFrame();
- await new Promise((resolve) => {
- setTimeout(resolve, 100);
- });
+ setTimeout(callback, 100);
+ await clock.runAllAsync();
assert.isTrue(playSpy.calledOnce);
// push the video out of the viewport
@@ -178,21 +278,18 @@ describe('video uploaded using franklin bot', () => {
video.parentNode.insertBefore(div, video);
await nextFrame();
- await new Promise((resolve) => {
- setTimeout(resolve, 100);
- });
+ setTimeout(callback, 200);
+ await clock.runAllAsync();
assert.isTrue(pauseSpy.calledOnce);
video.dispatchEvent(new Event('ended'));
await nextFrame();
- await new Promise((resolve) => {
- setTimeout(resolve, 100);
- });
+ setTimeout(callback, 100);
+ await clock.runAllAsync();
video.scrollIntoView();
await nextFrame();
- await new Promise((resolve) => {
- setTimeout(resolve, 100);
- });
+ setTimeout(callback, 100);
+ await clock.runAllAsync();
expect(playSpy.callCount).to.equal(1);
expect(video.hasAttribute('data-play-viewport')).to.be.true;
From 91accb65f1cf7d3623a125f8121ae927eef339de Mon Sep 17 00:00:00 2001
From: Rares Munteanu
Date: Wed, 4 Dec 2024 11:41:19 +0100
Subject: [PATCH 06/11] Add Express SOTs (#3287)
---
.github/workflows/merge-to-stage.js | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/merge-to-stage.js b/.github/workflows/merge-to-stage.js
index 71f290ace4..7c85415da1 100644
--- a/.github/workflows/merge-to-stage.js
+++ b/.github/workflows/merge-to-stage.js
@@ -20,19 +20,20 @@ const LABELS = {
zeroImpact: 'zero-impact',
};
const TEAM_MENTIONS = [
- '@adobecom/miq-sot',
'@adobecom/bacom-sot',
- '@adobecom/homepage-sot',
'@adobecom/creative-cloud-sot',
'@adobecom/document-cloud-sot',
+ '@adobecom/express-sot',
+ '@adobecom/homepage-sot',
+ '@adobecom/miq-sot',
];
const SLACK = {
merge: ({ html_url, number, title, prefix = '' }) => `:merged: PR merged to stage: ${prefix} <${html_url}|${number}: ${title}>.`,
openedSyncPr: ({ html_url, number }) => `:fast_forward: Created <${html_url}|Stage to Main PR ${number}>`,
};
-let github;
-let owner;
+let github;
+let owner;
let repo;
let body = `
From b632a21ed42b5bd9149d8857012b1f13fc695a1a Mon Sep 17 00:00:00 2001
From: Raghav Sharma <118168183+sharmrj@users.noreply.github.com>
Date: Wed, 4 Dec 2024 16:26:33 +0530
Subject: [PATCH 07/11] MWPW-163606 removed the /dist/ from the css paths where
it's incorrect (#3302)
* removed the /dist/ from the css paths where it's incorrect
* Cover an uncovered line in tests
---
libs/navigation/navigation.js | 8 ++++----
test/navigation/navigation.test.js | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/libs/navigation/navigation.js b/libs/navigation/navigation.js
index a4cb526218..043f5d1de3 100644
--- a/libs/navigation/navigation.js
+++ b/libs/navigation/navigation.js
@@ -84,11 +84,11 @@ export default async function loadBlock(configs, customLib) {
await import('./navigation.css');
} catch (e) {
if (theme === 'dark') {
- loadStyle(`${miloLibs}/libs/navigation/dist/base.css`, () => loadStyle(`${miloLibs}/libs/navigation/dist/dark-nav.css`));
+ loadStyle(`${miloLibs}/libs/navigation/base.css`, () => loadStyle(`${miloLibs}/libs/navigation/dark-nav.css`));
} else {
- loadStyle(`${miloLibs}/libs/navigation/dist/base.css`);
+ loadStyle(`${miloLibs}/libs/navigation/base.css`);
}
- loadStyle(`${miloLibs}/libs/navigation/dist/navigation.css`);
+ loadStyle(`${miloLibs}/libs/navigation/navigation.css`);
}
// Relative paths work just fine since they exist in the context of this file's origin
@@ -129,7 +129,7 @@ export default async function loadBlock(configs, customLib) {
try {
await import('./footer.css');
} catch (e) {
- loadStyle(`${miloLibs}/libs/navigation/dist/footer.css`);
+ loadStyle(`${miloLibs}/libs/navigation/footer.css`);
}
const { default: init } = await import('../blocks/global-footer/global-footer.js');
await bootstrapBlock(init, { ...block });
diff --git a/test/navigation/navigation.test.js b/test/navigation/navigation.test.js
index 93331eb331..1ea752635f 100644
--- a/test/navigation/navigation.test.js
+++ b/test/navigation/navigation.test.js
@@ -45,7 +45,7 @@ describe('Navigation component', async () => {
it('Renders the header block', async () => {
const onReady = stub();
- await loadBlock({ authoringPath: '/federal/dev', header: { imsClientId: 'fedsmilo', onReady }, env: 'prod' }, 'http://localhost:2000');
+ await loadBlock({ authoringPath: '/federal/dev', header: { imsClientId: 'fedsmilo', onReady }, env: 'prod', theme: 'dark' }, 'http://localhost:2000');
const el = document.getElementsByTagName('header');
expect(el).to.exist;
expect(onReady.called).to.be.true;
From d9ddd1726505eb2770468f38b94b74dd636128a9 Mon Sep 17 00:00:00 2001
From: Swati Mukherjee
Date: Wed, 4 Dec 2024 17:54:34 +0530
Subject: [PATCH 08/11] =?UTF-8?q?[MWPW-158749]=20Enhance=20Interact=20call?=
=?UTF-8?q?=20efficiency=20and=20defer=20non-essentia=E2=80=A6=20(#3258)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* move common snippets to helper, add defer logic and make interact call the 1st thing in the flow for signed out users
* toggle FPID and ECID logic, re-organise exports, dont fire if consent cookie present
* Pre merge branch (#5)
move methods utils , use async load of helper, review comments
* move try catch block as per promise
* Pre merge branch (#6)
* Test remove martech logic (#7)
fix delay logic, fix pr comments
* Test remove martech logic (#8)
change uuid logic, promise structure, alloy response logic, requestid,
* Final local branch (#9)
fix review comments, add test cases
* fix coverage for if check
* fix coverage errors
* add test case for martech helpers
* send full cookie flag added post testing round 1
---
.../personalization/personalization.js | 126 +++++-
libs/martech/helpers.js | 424 ++++++++++++++++++
libs/martech/martech.js | 77 +---
libs/utils/utils.js | 83 +++-
test/martech/helpers.test.js | 260 +++++++++++
test/utils/utils-enable-perf.test.js | 79 ++++
test/utils/utils.test.js | 20 +
7 files changed, 986 insertions(+), 83 deletions(-)
create mode 100644 libs/martech/helpers.js
create mode 100644 test/martech/helpers.test.js
create mode 100644 test/utils/utils-enable-perf.test.js
diff --git a/libs/features/personalization/personalization.js b/libs/features/personalization/personalization.js
index 9472ec413f..cbb87ba508 100644
--- a/libs/features/personalization/personalization.js
+++ b/libs/features/personalization/personalization.js
@@ -1,7 +1,9 @@
/* eslint-disable no-underscore-dangle */
/* eslint-disable no-console */
-import { createTag, getConfig, loadLink, loadScript, localizeLink } from '../../utils/utils.js';
+import {
+ createTag, getConfig, loadLink, loadScript, localizeLink, enablePersonalizationV2,
+} from '../../utils/utils.js';
import { getFederatedUrl } from '../../utils/federated.js';
/* c8 ignore start */
@@ -467,7 +469,7 @@ function getSelectedElements(sel, rootEl, forceRootEl) {
try {
els = root.querySelectorAll(modifiedSelector);
} catch (e) {
- /* eslint-disable-next-line no-console */
+ /* eslint-disable-next-line no-console */
log('Invalid selector: ', selector);
return null;
}
@@ -773,7 +775,11 @@ async function getPersonalizationVariant(manifestPath, variantNames = [], varian
let userEntitlements = [];
if (hasEntitlementTag) {
- userEntitlements = await config.entitlements();
+ if (enablePersonalizationV2()) {
+ userEntitlements = [];
+ } else {
+ userEntitlements = await config.entitlements();
+ }
}
const hasMatch = (name) => {
@@ -1111,9 +1117,7 @@ export const combineMepSources = async (persEnabled, promoEnabled, mepParam) =>
return persManifests;
};
-async function callMartech(config) {
- const { getTargetPersonalization } = await import('../../martech/martech.js');
- const { targetManifests, targetPropositions } = await getTargetPersonalization();
+function updateManifestsAndPropositions({ config, targetManifests, targetPropositions }) {
config.mep.targetManifests = targetManifests;
if (targetPropositions?.length && window._satellite) {
window._satellite.track('propositionDisplay', targetPropositions);
@@ -1124,6 +1128,97 @@ async function callMartech(config) {
}
return targetManifests;
}
+
+function roundToQuarter(num) {
+ return Math.ceil(num / 250) / 4;
+}
+
+function calculateResponseTime(responseStart) {
+ const responseTime = Date.now() - responseStart;
+ return roundToQuarter(responseTime);
+}
+
+function sendTargetResponseAnalytics(failure, responseStart, timeoutLocal, message) {
+ // temporary solution until we can decide on a better timeout value
+ const responseTime = calculateResponseTime(responseStart);
+ const timeoutTime = roundToQuarter(timeoutLocal);
+ let val = `target response time ${responseTime}:timed out ${failure}:timeout ${timeoutTime}`;
+ if (message) val += `:${message}`;
+ // eslint-disable-next-line no-underscore-dangle
+ window._satellite?.track?.('event', {
+ documentUnloading: true,
+ xdm: {
+ eventType: 'web.webinteraction.linkClicks',
+ web: {
+ webInteraction: {
+ linkClicks: { value: 1 },
+ type: 'other',
+ name: val,
+ },
+ },
+ },
+ data: { _adobe_corpnew: { digitalData: { primaryEvent: { eventInfo: { eventName: val } } } } },
+ });
+}
+
+const handleAlloyResponse = (response) => ((response.propositions || response.decisions))
+ ?.map((i) => i.items)
+ ?.flat()
+ ?.map((item) => {
+ const content = item?.data?.content;
+ if (!content || !(content.manifestLocation || content.manifestContent)) return null;
+
+ return {
+ manifestPath: content.manifestLocation || content.manifestPath,
+ manifestUrl: content.manifestLocation,
+ manifestData: content.manifestContent?.experiences?.data || content.manifestContent?.data,
+ manifestPlaceholders: content.manifestContent?.placeholders?.data,
+ manifestInfo: content.manifestContent?.info.data,
+ name: item.meta['activity.name'],
+ variantLabel: item.meta['experience.name'] && `target-${item.meta['experience.name']}`,
+ meta: item.meta,
+ };
+ })
+ ?.filter(Boolean) ?? [];
+
+async function handleMartechTargetInteraction(
+ { config, targetInteractionPromise, calculatedTimeout },
+) {
+ let targetManifests = [];
+ let targetPropositions = [];
+ if (enablePersonalizationV2() && targetInteractionPromise) {
+ try {
+ const { targetInteractionData, respTime, respStartTime } = await targetInteractionPromise;
+ sendTargetResponseAnalytics(false, respStartTime, calculatedTimeout);
+
+ const roundedResponseTime = roundToQuarter(respTime);
+ performance.clearMarks();
+ performance.clearMeasures();
+ try {
+ window.lana.log(`target response time: ${roundedResponseTime}`, { tags: 'martech', errorType: 'i' });
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error('Error logging target response time:', e);
+ }
+ targetManifests = handleAlloyResponse(targetInteractionData.result);
+ targetPropositions = targetInteractionData.result?.propositions || [];
+ } catch (err) {
+ console.log('Oops!! Interact Call didnt go through', err);
+ }
+ }
+
+ return updateManifestsAndPropositions({ config, targetManifests, targetPropositions });
+}
+
+async function callMartech(config) {
+ const { getTargetPersonalization } = await import('../../martech/martech.js');
+ const {
+ targetManifests,
+ targetPropositions,
+ } = await getTargetPersonalization({ handleAlloyResponse, sendTargetResponseAnalytics });
+ return updateManifestsAndPropositions({ config, targetManifests, targetPropositions });
+}
+
const awaitMartech = () => new Promise((resolve) => {
const listener = (event) => resolve(event.detail);
window.addEventListener(MARTECH_RETURNED_EVENT, listener, { once: true });
@@ -1132,7 +1227,8 @@ const awaitMartech = () => new Promise((resolve) => {
export async function init(enablements = {}) {
let manifests = [];
const {
- mepParam, mepHighlight, mepButton, pzn, promo, target, postLCP,
+ mepParam, mepHighlight, mepButton, pzn, promo,
+ target, targetInteractionPromise, calculatedTimeout, postLCP,
} = enablements;
const config = getConfig();
if (postLCP) {
@@ -1157,11 +1253,17 @@ export async function init(enablements = {}) {
if (pzn) loadLink(getXLGListURL(config), { as: 'fetch', crossorigin: 'anonymous', rel: 'preload' });
}
- if (target === true) manifests = manifests.concat(await callMartech(config));
- if (target === 'postlcp') callMartech(config);
- if (postLCP) {
- if (!config.mep.targetManifests) await awaitMartech();
- manifests = config.mep.targetManifests;
+ if (enablePersonalizationV2()) {
+ manifests = manifests.concat(await handleMartechTargetInteraction(
+ { config, targetInteractionPromise, calculatedTimeout },
+ ));
+ } else {
+ if (target === true) manifests = manifests.concat(await callMartech(config));
+ if (target === 'postlcp') callMartech(config);
+ if (postLCP) {
+ if (!config.mep.targetManifests) await awaitMartech();
+ manifests = config.mep.targetManifests;
+ }
}
if (!manifests || !manifests.length) return;
try {
diff --git a/libs/martech/helpers.js b/libs/martech/helpers.js
new file mode 100644
index 0000000000..25760ba57a
--- /dev/null
+++ b/libs/martech/helpers.js
@@ -0,0 +1,424 @@
+/**
+ * Generates a random UUIDv4 using cryptographically secure random values.
+ * This implementation follows the RFC 4122 specification for UUIDv4.
+ * It uses the `crypto` API for secure randomness without any bitwise operators.
+ *
+ * @returns {string} A random UUIDv4 string, e.g., 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
+ * where:
+ * - 'x' is any hexadecimal digit (0-9, a-f)
+ * - 'y' is one of 8, 9, A, or B, ensuring that the UUID conforms to version 4.
+ *
+ * @example
+ * const myUuid = generateUUIDv4();
+ * console.log(myUuid); // Outputs: 'e8b57e2f-8cb1-4d0f-804b-e1a45bce2d90'
+ */
+function generateUUIDv4() {
+ // Generate an array of 16 random values using the crypto API for better randomness
+ const randomValues = new Uint8Array(16);
+ crypto.getRandomValues(randomValues);
+
+ // Set the version (4) at the 13th position
+ randomValues[6] = (randomValues[6] % 16) + 64; // '4' for version 4
+ // Set the variant (8, 9, A, or B) at the 17th position
+ randomValues[8] = (randomValues[8] % 16) + 128; // One of 8, 9, A, or B
+
+ // Accumulate the UUID string in a separate variable (to avoid modifying the parameter directly)
+ let uuid = '';
+
+ // Convert the random values to a UUID string (xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx)
+ randomValues.forEach((byte, index) => {
+ const hex = byte.toString(16).padStart(2, '0'); // Convert byte to hex
+ if (index === 4 || index === 6 || index === 8 || index === 10) {
+ uuid += '-'; // Add dashes at appropriate positions
+ }
+ uuid += hex;
+ });
+
+ return uuid;
+}
+
+/**
+ * Determines the Adobe Target property value based on the page's region.
+ *
+ * @param {string} env - The environment (e.g., 'prod' for production, 'dev' for development).
+ * @returns {string} Adobe Target property value.
+ */
+function getTargetPropertyBasedOnPageRegion(env) {
+ const { pathname } = window.location;
+
+ if (env !== 'prod') return 'bc8dfa27-29cc-625c-22ea-f7ccebfc6231'; // Default for non-prod environments
+
+ // EMEA & LATAM
+ if (
+ pathname.search(
+ /(\/africa\/|\/be_en\/|\/be_fr\/|\/be_nl\/|\/cis_en\/|\/cy_en\/|\/dk\/|\/de\/|\/ee\/|\/es\/|\/fr\/|\/gr_en\/|\/ie\/|\/il_en\/|\/it\/|\/lv\/|\/lu_de\/|\/lu_en\/|\/lu_fr\/|\/hu\/|\/mt\/|\/mena_en\/|\/nl\/|\/no\/|\/pl\/|\/pt\/|\/ro\/|\/ch_de\/|\/si\/|\/sk\/|\/ch_fr\/|\/fi\/|\/se\/|\/ch_it\/|\/tr\/|\/uk\/|\/at\/|\/cz\/|\/bg\/|\/ru\/|\/cis_ru\/|\/ua\/|\/il_he\/|\/mena_ar\/|\/lt\/|\/sa_en\/|\/ae_en\/|\/ae_ar\/|\/sa_ar\/|\/ng\/|\/za\/|\/qa_ar\/|\/eg_en\/|\/eg_ar\/|\/kw_ar\/|\/eg_ar\/|\/qa_en\/|\/kw_en\/|\/gr_el\/|\/br\/|\/cl\/|\/la\/|\/mx\/|\/co\/|\/ar\/|\/pe\/|\/gt\/|\/pr\/|\/ec\/|\/cr\/)/,
+ ) !== -1
+ ) {
+ return '488edf5f-3cbe-f410-0953-8c0c5c323772';
+ }
+ if ( // APAC
+ pathname.search(
+ /(\/au\/|\/hk_en\/|\/in\/|\/nz\/|\/sea\/|\/cn\/|\/hk_zh\/|\/tw\/|\/kr\/|\/sg\/|\/th_en\/|\/th_th\/|\/my_en\/|\/my_ms\/|\/ph_en\/|\/ph_fil\/|\/vn_en\/|\/vn_vi\/|\/in_hi\/|\/id_id\/|\/id_en\/)/,
+ ) !== -1
+ ) {
+ return '3de509ee-bbc7-58a3-0851-600d1c2e2918';
+ }
+ // JP
+ if (pathname.indexOf('/jp/') !== -1) {
+ return 'ba5bc9e8-8fb4-037a-12c8-682384720007';
+ }
+
+ return 'bc8dfa27-29cc-625c-22ea-f7ccebfc6231'; // Default
+}
+
+/**
+ * Retrieves device-related information such as screen and viewport dimensions.
+ *
+ * @returns {Object} Object containing device and viewport information.
+ */
+function getDeviceInfo() {
+ return {
+ screenWidth: window.screen.width,
+ screenHeight: window.screen.height,
+ screenOrientation: window.innerWidth > window.innerHeight ? 'landscape' : 'portrait',
+ viewportWidth: window.innerWidth,
+ viewportHeight: window.innerHeight,
+ };
+}
+
+/**
+ * Retrieves the value of a specific cookie by its key.
+ *
+ * @param {string} key - The cookie key.
+ * @returns {string|null} The cookie value, or null if the cookie doesn't exist.
+ */
+function getCookie(key, sendFullCookie) {
+ const cookie = document.cookie.split(';')
+ .map((x) => x.trim().split('='))
+ .find(([k]) => k === key);
+
+ if (sendFullCookie) {
+ return cookie;
+ }
+ return cookie ? cookie[1] : null;
+}
+
+/**
+ * Sets a cookie with a specified expiration time (default 730 days).
+ *
+ * @param {string} key - The cookie key.
+ * @param {string} value - The cookie value.
+ * @param {Object} [options={}] - Optional settings for cookie properties.
+ * Defaults to an expiration of 730 days.
+ */
+function setCookie(key, value, options = {}) {
+ // Default expiration (24 months)
+ const expires = options.expires || 730;
+ const date = new Date();
+ date.setTime(date.getTime() + expires * 24 * 60 * 60 * 1000);
+ const expiresString = `expires=${date.toUTCString()}`;
+
+ document.cookie = `${key}=${value}; ${expiresString}; path=/`;
+}
+
+/**
+ * Retrieves the ECID (Experience Cloud ID) from the browser's cookies or
+ * generates a new FPID (First Party ID) if the ECID is not found. Returns
+ * the ID in a structured object, depending on which ID is available.
+ *
+ * @returns {Object} An object containing either the ECID or FPID.
+ * - If ECID is found, the object will be:
+ * { ECID: [{ id: string, authenticatedState: string, primary: boolean }] }
+ * - If ECID is not found, the object will be:
+ * { FPID: [{ id: string, authenticatedState: string, primary: boolean }] }
+ */
+function getOrGenerateUserId() {
+ const experienceCloudCookieName = 'AMCV_9E1005A551ED61CA0A490D45%40AdobeOrg';
+ const amcvCookieValue = getCookie(experienceCloudCookieName);
+
+ // If ECID is not found, generate and return FPID
+ if (!amcvCookieValue) {
+ const fpidValue = generateUUIDv4();
+ return {
+ FPID: [{
+ id: fpidValue,
+ authenticatedState: 'ambiguous',
+ primary: true,
+ }],
+ };
+ }
+
+ // ECID found, return structured ECID object
+ const extractedEcid = amcvCookieValue.substring(6); // Extract the ECID value from the cookie
+ return {
+ ECID: [{
+ id: extractedEcid,
+ authenticatedState: 'ambiguous',
+ primary: true,
+ }],
+ };
+}
+
+/**
+ * Retrieves the page name for analytics, modified for the current locale.
+ *
+ * @param {Object} params - The parameters.
+ * @param {Object} params.locale - The locale object containing
+ * language/region info (e.g., { ietf: 'en-US', prefix: 'us' }).
+ * @returns {string} The modified page name.
+ */
+function getPageNameForAnalytics({ locale }) {
+ const { host, pathname } = new URL(window.location.href);
+ const [modifiedPath] = pathname.split('/').filter((x) => x !== locale.prefix).join(':').split('.');
+ return `${host.replace('www.', '')}:${modifiedPath}`;
+}
+
+/**
+ * Creates the updated context for the request payload for analytics or personalization requests.
+ *
+ * @param {number} screenWidth - Screen width.
+ * @param {number} screenHeight - Screen height.
+ * @param {string} screenOrientation - Orientation of the screen.
+ * @param {number} viewportWidth - Viewport width.
+ * @param {number} viewportHeight - Viewport height.
+ * @param {string} localTime - The local time in ISO format.
+ * @param {number} timezoneOffset - The timezone offset.
+ * @returns {Object} The updated context for the request payload.
+ */
+function getUpdatedContext({
+ screenWidth, screenHeight, screenOrientation,
+ viewportWidth, viewportHeight, localTime, timezoneOffset,
+}) {
+ return {
+ device: {
+ screenHeight,
+ screenWidth,
+ screenOrientation,
+ },
+ environment: {
+ type: 'browser',
+ browserDetails: {
+ viewportWidth,
+ viewportHeight,
+ },
+ },
+ placeContext: {
+ localTime,
+ localTimezoneOffset: timezoneOffset,
+ },
+ };
+}
+
+/**
+ * Retrieves specific MarTech cookies by their keys.
+ *
+ * @returns {Array