diff --git a/libs/blocks/aside/aside.css b/libs/blocks/aside/aside.css index bc8ed99fa5..bfe474836b 100644 --- a/libs/blocks/aside/aside.css +++ b/libs/blocks/aside/aside.css @@ -567,7 +567,7 @@ } .aside.promobar .action-area .con-button { - flex-shrink: 0; + white-space: nowrap; } .aside.promobar .promo-text.desktop-up, diff --git a/libs/blocks/global-footer/global-footer.js b/libs/blocks/global-footer/global-footer.js index 7818d81b4f..306650c536 100644 --- a/libs/blocks/global-footer/global-footer.js +++ b/libs/blocks/global-footer/global-footer.js @@ -66,7 +66,7 @@ class Footer { observer.disconnect(); this.decorateContent(); }, CONFIG.delays.decoration); - }, 'Error in global footer init', 'errorType=error,module=global-footer'); + }, 'Error in global footer init', 'global-footer', 'error'); decorateContent = () => logErrorFor(async () => { // Fetch footer content @@ -114,7 +114,7 @@ class Footer { this.block.setAttribute('daa-lh', `gnav|${getExperienceName()}|footer${mepMartech}`); this.block.append(this.elements.footer); - }, 'Failed to decorate footer content', 'errorType=error,module=global-footer'); + }, 'Failed to decorate footer content', 'global-footer', 'error'); loadMenuLogic = async () => { this.menuLogic = this.menuLogic || new Promise(async (resolve) => { @@ -154,7 +154,8 @@ class Footer { lanaLog({ message: 'Issue with loadIcons', e: `${file.statusText} url: ${file.url}`, - tags: 'errorType=info,module=global-footer', + tags: 'global-footer', + errorType: 'info', }); } const content = await file.text(); @@ -199,7 +200,7 @@ class Footer { try { url = new URL(regionSelector.href); } catch (e) { - lanaLog({ message: `Could not create URL for region picker; href: ${regionSelector.href}`, tags: 'errorType=error,module=global-footer' }); + lanaLog({ message: `Could not create URL for region picker; href: ${regionSelector.href}`, tags: 'global-footer', errorType: 'error' }); return this.elements.regionPicker; } @@ -229,14 +230,7 @@ class Footer { if (url.hash !== '') { // 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', - }); - } loadStyle(`${base}/blocks/modal/modal.css`); const { default: initModal } = await import('../modal/modal.js'); const modal = await initModal(regionPickerElem); @@ -261,12 +255,6 @@ class Footer { 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', - }); - } regionPickerElem.addEventListener('click', () => { if (!isRegionPickerExpanded()) { regionPickerElem.setAttribute('aria-expanded', 'true'); diff --git a/libs/blocks/global-navigation/base.css b/libs/blocks/global-navigation/base.css index 768701dd84..f475e6f75b 100644 --- a/libs/blocks/global-navigation/base.css +++ b/libs/blocks/global-navigation/base.css @@ -27,8 +27,8 @@ --feds-background-link--hover: #e9e9e9; --feds-borderColor-link: #e9e9e9; --feds-color-link: #292929; - --feds-color-blue-link: #3b63fb; - --feds-color-link--hover: #274dea; + --feds-color-blue-link: #274dea; + --feds-color-link--hover: #1d3ecf; --feds-color-navLink-description: #505050; --feds-color-link-breadcrumbs: #707070; /* Footer */ diff --git a/libs/blocks/global-navigation/features/breadcrumbs/breadcrumbs.js b/libs/blocks/global-navigation/features/breadcrumbs/breadcrumbs.js index 2a5741b486..1f00fb7bcf 100644 --- a/libs/blocks/global-navigation/features/breadcrumbs/breadcrumbs.js +++ b/libs/blocks/global-navigation/features/breadcrumbs/breadcrumbs.js @@ -81,7 +81,7 @@ const createWithBase = async (el) => { element.querySelector('ul')?.prepend(...base.querySelectorAll('li')); return createBreadcrumbs(element); } catch (e) { - lanaLog({ e, message: 'Breadcrumbs failed fetching base', tags: 'errorType=info,module=gnav-breadcrumbs' }); + lanaLog({ e, message: 'Breadcrumbs failed fetching base', tags: 'gnav-breadcrumbs', errorType: 'info' }); return null; } }; @@ -110,7 +110,7 @@ export default async function init(el) { setBreadcrumbSEO(breadcrumbsEl); return breadcrumbsEl; } catch (e) { - lanaLog({ e, message: 'Breadcrumbs failed rendering', tags: 'errorType=error,module=gnav-breadcrumbs' }); + lanaLog({ e, message: 'Breadcrumbs failed rendering', tags: 'gnav-breadcrumbs', errorType: 'error' }); return null; } } diff --git a/libs/blocks/global-navigation/features/profile/dropdown.js b/libs/blocks/global-navigation/features/profile/dropdown.js index ca9e843cf8..cdd0d8f9fe 100644 --- a/libs/blocks/global-navigation/features/profile/dropdown.js +++ b/libs/blocks/global-navigation/features/profile/dropdown.js @@ -55,7 +55,7 @@ class ProfileDropdown { this.sections = sections; this.openOnInit = openOnInit; this.localMenu = rawElem.querySelector('h5')?.parentElement; - logErrorFor(this.init.bind(this), 'ProfileDropdown.init()', 'errorType=error,module=gnav-profile'); + logErrorFor(this.init.bind(this), 'ProfileDropdown.init()', 'gnav-profile', 'error'); } async init() { diff --git a/libs/blocks/global-navigation/global-navigation.css b/libs/blocks/global-navigation/global-navigation.css index 8e3a8b52ec..785f2d78e4 100644 --- a/libs/blocks/global-navigation/global-navigation.css +++ b/libs/blocks/global-navigation/global-navigation.css @@ -986,12 +986,13 @@ header.new-nav .feds-nav > section.feds-navItem > .feds-popup .title .breadcrumb box-sizing: inherit; } header.new-nav .feds-nav > section.feds-navItem > .feds-popup .title h7 { - height: 25px; + min-height: 25px; font-size: 28px; font-weight: 700; line-height: 25px; padding: 8px 0 24px; box-sizing: inherit; + white-space: break-spaces; } header.new-nav .feds-nav > section.feds-navItem > .feds-popup .tabs { @@ -1058,6 +1059,10 @@ header.new-nav .feds-nav > section.feds-navItem > .feds-popup .tab-content a { font-weight: 700; } +header.new-nav .feds-nav > section.feds-navItem > .feds-popup .tab-content a.feds-navLink:not(:has(div)) { + white-space: break-spaces; +} + header.new-nav .feds-nav > section.feds-navItem > .feds-popup .tab-content .feds-navLink-title { font-weight: 700; white-space: break-spaces; diff --git a/libs/blocks/global-navigation/global-navigation.js b/libs/blocks/global-navigation/global-navigation.js index 53ab0e77e6..d5d336ed7d 100644 --- a/libs/blocks/global-navigation/global-navigation.js +++ b/libs/blocks/global-navigation/global-navigation.js @@ -115,8 +115,8 @@ export const CONFIG = { trace: () => {}, debug: () => {}, info: () => {}, - warn: (e) => lanaLog({ message: 'Profile Menu warning', e, tags: 'errorType=warn,module=universalnav' }), - error: (e) => lanaLog({ message: 'Profile Menu error', e, tags: 'errorType=error,module=universalnav' }), + warn: (e) => lanaLog({ message: 'Profile Menu warning', e, tags: 'universalnav', errorType: 'warn' }), + error: (e) => lanaLog({ message: 'Profile Menu error', e, tags: 'universalnav', errorType: 'error' }), }, }, ...getConfig().unav?.profile?.config, @@ -186,7 +186,7 @@ export const LANGMAP = { // signIn, decorateSignIn and decorateProfileTrigger can be removed if IMS takes over the profile const signIn = (options = {}) => { if (typeof window.adobeIMS?.signIn !== 'function') { - lanaLog({ message: 'IMS signIn method not available', tags: 'errorType=warn,module=gnav' }); + lanaLog({ message: 'IMS signIn method not available', tags: 'gnav', errorType: 'warn' }); return; } window.adobeIMS.signIn(options); @@ -222,7 +222,7 @@ const decorateSignIn = async ({ rawElem, decoratedElem }) => { signIn(SIGNIN_CONTEXT); }); } else { - lanaLog({ message: 'Sign in link not found in dropdown.', tags: 'errorType=warn,module=gnav' }); + lanaLog({ message: 'Sign in link not found in dropdown.', tags: 'gnav', errorType: 'warn' }); } decoratedElem.append(dropdownElem); @@ -386,7 +386,7 @@ class Gnav { document.addEventListener('click', (e) => closeOnClickOutside(e, this.isLocalNav(), this.elements.navWrapper)); isDesktop.addEventListener('change', closeAllDropdowns); - }, 'Error in global navigation init', 'errorType=error,module=gnav'); + }, 'Error in global navigation init', 'gnav', 'error'); ims = async () => (window.adobeIMS?.initialized ? this.imsReady() : loadIms() .then(() => this.imsReady()) @@ -395,7 +395,7 @@ class Gnav { window.addEventListener('onImsLibInstance', () => this.imsReady()); return; } - lanaLog({ message: 'GNAV: Error with IMS', e, tags: 'errorType=info,module=gnav' }); + lanaLog({ message: 'GNAV: Error with IMS', e, tags: 'gnav', errorType: 'info' }); })); decorateProductEntryCTA = () => { @@ -429,7 +429,7 @@ class Gnav { const localNavItems = this.elements.navWrapper.querySelector('.feds-nav').querySelectorAll('.feds-navItem:not(.feds-navItem--section, .feds-navItem--mobile-only)'); const firstElem = localNavItems[0]?.querySelector('a'); if (!firstElem) { - lanaLog({ message: 'GNAV: Incorrect authoring of localnav found.', tags: 'errorType=info,module=gnav' }); + lanaLog({ message: 'GNAV: Incorrect authoring of localnav found.', tags: 'gnav', errorType: 'info' }); return; } const [title, navTitle = ''] = this.getOriginalTitle(firstElem); @@ -437,7 +437,8 @@ class Gnav { if (!localNav) { lanaLog({ message: 'GNAV: Localnav does not include \'localnav\' in its name.', - tags: 'errorType=info,module=gnav', + tags: 'gnav', + errorType: 'info', }); localNav = toFragment`
`; this.block.after(localNav); @@ -574,7 +575,7 @@ class Gnav { resolve(); } catch (e) { - lanaLog({ message: 'GNAV: Error within loadDelayed', e, tags: 'errorType=warn,module=gnav' }); + lanaLog({ message: 'GNAV: Error within loadDelayed', e, tags: 'gnav', errorType: 'warn' }); resolve(); } }); @@ -593,7 +594,12 @@ class Gnav { await task(); } } catch (e) { - lanaLog({ message: 'GNAV: issues within onReady', e, tags: 'errorType=info,module=gnav' }); + lanaLog({ + e, + tags: 'gnav', + errorType: 'info', + message: `GNAV: issues within imsReady - ${this.useUniversalNav ? 'decorateUniversalNav' : 'decorateProfile'}`, + }); } }; @@ -619,7 +625,8 @@ class Gnav { lanaLog({ message: 'GNAV: decorateProfile has failed to fetch profile data', e: `${profileData.statusText} url: ${profileData.url}`, - tags: 'errorType=info,module=gnav', + tags: 'gnav', + errorType: 'info', }); return; } @@ -884,7 +891,7 @@ class Gnav { if (this.isToggleExpanded()) setHamburgerPadding(); }; - toggle.addEventListener('click', () => logErrorFor(onToggleClick, 'Toggle click failed', 'errorType=error,module=gnav')); + toggle.addEventListener('click', () => logErrorFor(onToggleClick, 'Toggle click failed', 'gnav', 'error')); const onDeviceChange = () => { if (isDesktop.matches) { @@ -897,7 +904,7 @@ class Gnav { } }; - isDesktop.addEventListener('change', () => logErrorFor(onDeviceChange, 'Toggle logic failed on device change', 'errorType=error,module=gnav')); + isDesktop.addEventListener('change', () => logErrorFor(onDeviceChange, 'Toggle logic failed on device change', 'gnav', 'error')); return toggle; }; @@ -1148,7 +1155,7 @@ class Gnav { decorateLocalNavItems(item, template); } } - }, 'Decorate dropdown failed', 'errorType=info,module=gnav'); + }, 'Decorate dropdown failed', 'gnav', 'info'); template.addEventListener('click', decorateDropdown); decorationTimeout = setTimeout(decorateDropdown, CONFIG.delays.mainNavDropdowns); @@ -1313,14 +1320,15 @@ export default async function init(block) { setDisableAEDState(); } const content = await fetchAndProcessPlainHtml({ url }); - setAsyncDropdownCount(content.querySelectorAll('.large-menu').length); if (!content) { const error = new Error('Could not create global navigation. Content not found!'); - error.tags = 'errorType=error,module=gnav'; + error.tags = 'gnav'; error.url = url; - lanaLog({ message: error.message, error, tags: 'errorType=error,module=gnav' }); + error.errorType = 'error'; + lanaLog({ message: error.message, ...error }); throw error; } + setAsyncDropdownCount(content.querySelectorAll('.large-menu').length); const gnav = new Gnav({ content, block, diff --git a/libs/blocks/global-navigation/utilities/keyboard/index.js b/libs/blocks/global-navigation/utilities/keyboard/index.js index 593b2b7843..be57cf21d8 100644 --- a/libs/blocks/global-navigation/utilities/keyboard/index.js +++ b/libs/blocks/global-navigation/utilities/keyboard/index.js @@ -82,7 +82,7 @@ class KeyboardNavigation { } this.desktop = window.matchMedia('(min-width: 900px)'); } catch (e) { - lanaLog({ message: 'Keyboard Navigation failed to load', e, tags: 'errorType=error,module=gnav-keyboard' }); + lanaLog({ message: 'Keyboard Navigation failed to load', e, tags: 'gnav-keyboard', errorType: 'error' }); } } @@ -93,7 +93,7 @@ class KeyboardNavigation { const { default: LnavNavigation } = await import('./localNav.js'); return new LnavNavigation(); } catch (e) { - lanaLog({ message: 'Keyboard Navigation failed to load for LNAV', e, tags: 'errorType=info,module=gnav-keyboard' }); + lanaLog({ message: 'Keyboard Navigation failed to load for LNAV', e, tags: 'gnav-keyboard', errorType: 'info' }); return null; } })(); @@ -153,7 +153,7 @@ class KeyboardNavigation { default: break; } - }, `KeyboardNavigation index failed. ${e.code}`, 'errorType=error,module=gnav-keyboard')); + }, `KeyboardNavigation index failed. ${e.code}`, 'gnav-keyboard', 'error')); }); }; } diff --git a/libs/blocks/global-navigation/utilities/keyboard/mainNav.js b/libs/blocks/global-navigation/utilities/keyboard/mainNav.js index 3fb6e2ce60..ac26b4f0ed 100644 --- a/libs/blocks/global-navigation/utilities/keyboard/mainNav.js +++ b/libs/blocks/global-navigation/utilities/keyboard/mainNav.js @@ -109,7 +109,7 @@ class MainNavItem { default: break; } - }, `mainNav key failed ${e.code}`, 'errorType=error,module=gnav-keyboard')); + }, `mainNav key failed ${e.code}`, 'gnav-keyboard', 'error')); } getState = () => { diff --git a/libs/blocks/global-navigation/utilities/keyboard/mobilePopup.js b/libs/blocks/global-navigation/utilities/keyboard/mobilePopup.js index 21eb889572..8a8b3f141f 100644 --- a/libs/blocks/global-navigation/utilities/keyboard/mobilePopup.js +++ b/libs/blocks/global-navigation/utilities/keyboard/mobilePopup.js @@ -247,7 +247,7 @@ class Popup { const element = getOpenPopup(); if (!e.target.closest(selectors.popup) || !element || this.desktop.matches) return; this.handleKeyDown({ e, element, popupEl: element, isFooter: false }); - }, `popup key failed ${e.code}`, 'errorType=error,module=gnav-keyboard')); + }, `popup key failed ${e.code}`, 'gnav-keyboard', 'error')); document.querySelector(selectors.globalFooter) ?.addEventListener('keydown', (e) => logErrorFor(() => { @@ -274,7 +274,7 @@ class Popup { element, isFooter: true, }); - }, `footer key failed ${e.code}`, 'errorType=error,module=gnav-keyboard')); + }, `footer key failed ${e.code}`, 'gnav-keyboard', 'error')); }; } diff --git a/libs/blocks/global-navigation/utilities/keyboard/popup.js b/libs/blocks/global-navigation/utilities/keyboard/popup.js index 7744981977..536f8f67e1 100644 --- a/libs/blocks/global-navigation/utilities/keyboard/popup.js +++ b/libs/blocks/global-navigation/utilities/keyboard/popup.js @@ -149,14 +149,14 @@ class Popup { const element = getOpenPopup(); if (!e.target.closest(selectors.popup) || !element || !this.desktop.matches) return; this.handleKeyDown({ e, element, isFooter: false }); - }, `popup key failed ${e.code}`, 'errorType=error,module=gnav-keyboard')); + }, `popup key failed ${e.code}`, 'gnav-keyboard', 'error')); document.querySelector(selectors.globalFooter) ?.addEventListener('keydown', (e) => logErrorFor(() => { if (!this.desktop.matches) return; const element = e.target.closest(selectors.globalFooter); this.handleKeyDown({ e, element, isFooter: true }); - }, `footer key failed ${e.code}`, 'errorType=error,module=gnav-keyboard')); + }, `footer key failed ${e.code}`, 'gnav-keyboard', 'error')); }; } diff --git a/libs/blocks/global-navigation/utilities/menu/menu.js b/libs/blocks/global-navigation/utilities/menu/menu.js index 004dc54bb2..92ece7b24b 100644 --- a/libs/blocks/global-navigation/utilities/menu/menu.js +++ b/libs/blocks/global-navigation/utilities/menu/menu.js @@ -382,6 +382,6 @@ const decorateMenu = (config) => logErrorFor(async () => { } config.template?.append(menuTemplate); -}, 'Decorate menu failed', 'errorType=info,module=gnav-menu'); +}, 'Decorate menu failed', 'gnav-menu', 'info'); export default { decorateMenu, decorateLinkGroup }; diff --git a/libs/blocks/global-navigation/utilities/utilities.js b/libs/blocks/global-navigation/utilities/utilities.js index 39cc1427a8..5b73184467 100644 --- a/libs/blocks/global-navigation/utilities/utilities.js +++ b/libs/blocks/global-navigation/utilities/utilities.js @@ -47,20 +47,21 @@ export const darkIcons = { company: '', }; -export const lanaLog = ({ message, e = '', tags = 'errorType=default' }) => { +export const lanaLog = ({ message, e = '', tags = 'default', errorType }) => { const url = getMetadata('gnav-source'); window.lana.log(`${message} | gnav-source: ${url} | href: ${window.location.href} | ${e.reason || e.error || e.message || e}`, { clientId: 'feds-milo', sampleRate: 1, tags, + errorType, }); }; -export const logErrorFor = async (fn, message, tags) => { +export const logErrorFor = async (fn, message, tags, errorType) => { try { await fn(); } catch (e) { - lanaLog({ message, e, tags }); + lanaLog({ message, e, tags, errorType }); } }; @@ -151,7 +152,8 @@ export function loadStyles(url, override = false) { lanaLog({ message: 'GNAV: Error in loadStyles', e: `error loading style: ${url}`, - tags: 'errorType=info,module=utilities', + tags: 'utilities', + errorType: 'info', }); } }); @@ -364,7 +366,8 @@ export async function fetchAndProcessPlainHtml({ url, shouldDecorateLinks = true lanaLog({ message: 'Error in fetchAndProcessPlainHtml', e: `${res.statusText} url: ${res.url}`, - tags: 'errorType=info,module=utilities', + tags: 'utilities', + errorType: 'info', }); return null; } @@ -402,7 +405,8 @@ export async function fetchAndProcessPlainHtml({ url, shouldDecorateLinks = true lanaLog({ message: 'Error in fetchAndProcessPlainHtml', e, - tags: 'errorType=info,module=utilities', + tags: 'utilities', + errorType: 'info', }); }); } diff --git a/libs/blocks/library-config/lists/blocks.js b/libs/blocks/library-config/lists/blocks.js index 19df5a7bcf..621564b493 100644 --- a/libs/blocks/library-config/lists/blocks.js +++ b/libs/blocks/library-config/lists/blocks.js @@ -116,7 +116,8 @@ export function getHtml(container, path) { const isBlock = element.nodeName === 'DIV' && element.className; const content = isBlock ? getTable(element) : element.outerHTML; - return `${acc}${content}`; + const brTop = !acc || acc.endsWith('
') ? '' : BLOCK_SPACING; + return `${acc}${brTop}${content}${BLOCK_SPACING}`; }, ''); } @@ -283,7 +284,7 @@ export default async function loadBlocks(blocks, list, query, type) { const containerHtml = getHtml(container, block.path); e.target.classList.add('copied'); setTimeout(() => { e.target.classList.remove('copied'); }, 3000); - const blob = new Blob([`${BLOCK_SPACING}${containerHtml}${BLOCK_SPACING}`], { type: 'text/html' }); + const blob = new Blob([containerHtml], { type: 'text/html' }); createCopy(blob); }); item.append(name, copy); diff --git a/libs/blocks/mobile-app-banner/mobile-app-banner.js b/libs/blocks/mobile-app-banner/mobile-app-banner.js index 62f68b7086..b98354776d 100644 --- a/libs/blocks/mobile-app-banner/mobile-app-banner.js +++ b/libs/blocks/mobile-app-banner/mobile-app-banner.js @@ -16,7 +16,7 @@ async function getKey(product) { async function getECID() { let ecid = null; if (window.alloy) { - await window.alloy('getIdentity').then((data) => { + await window.alloy_getIdentity.then((data) => { ecid = data?.identity?.ECID; }).catch((err) => window.lana.log(`Error fetching ECID: ${err}`, { tags: 'mobile-app-banner' })); } diff --git a/libs/blocks/table/table.js b/libs/blocks/table/table.js index b1e8988bab..3a7a6704e3 100644 --- a/libs/blocks/table/table.js +++ b/libs/blocks/table/table.js @@ -171,6 +171,34 @@ async function setAriaLabelForIcons(el) { }); } +function setTooltipListeners(el) { + document.addEventListener('keydown', (event) => { + if (event.key === 'Escape') { + el.querySelectorAll('.milo-tooltip').forEach((tooltip) => { + tooltip.classList.add('hide-tooltip'); + }); + } + }); + + el.querySelectorAll('.milo-tooltip').forEach((tooltip) => { + tooltip.addEventListener('mouseenter', () => { + tooltip.classList.remove('hide-tooltip'); + }); + + tooltip.addEventListener('mouseleave', () => { + tooltip.classList.add('hide-tooltip'); + }); + + tooltip.addEventListener('focus', () => { + tooltip.classList.remove('hide-tooltip'); + }); + + tooltip.addEventListener('blur', () => { + tooltip.classList.add('hide-tooltip'); + }); + }); +} + function handleHighlight(table) { const isHighlightTable = table.classList.contains('highlight'); const firstRow = table.querySelector('.row-1'); @@ -622,6 +650,7 @@ export default function init(el) { isDecorated = true; setAriaLabelForIcons(el); + setTooltipListeners(el); }; window.addEventListener(MILO_EVENTS.DEFERRED, () => { diff --git a/libs/features/branch-quick-links/branch-quick-links.js b/libs/features/branch-quick-links/branch-quick-links.js index 153b5df0b8..6977926cc9 100644 --- a/libs/features/branch-quick-links/branch-quick-links.js +++ b/libs/features/branch-quick-links/branch-quick-links.js @@ -25,12 +25,12 @@ async function decorateQuickLink(a, hasConsent) { if (!window.alloy) return; let ecid = null; try { - const data = await window.alloy('getIdentity'); + const data = await window.alloy_getIdentity; ecid = data?.identity?.ECID; } catch (e) { window.lana.log(`Error fetching ECID: ${e}`, { tags: 'branch-quick-links' }); } - if (hasConsent && !a.href.includes('ecid')) { + if (ecid && hasConsent && !a.href.includes('ecid')) { a.href = a.href.concat(`?ecid=${ecid}`); } window.location.href = a.href; diff --git a/libs/features/icons/icons.css b/libs/features/icons/icons.css index 22b7ec30f2..b38d2fb2f5 100644 --- a/libs/features/icons/icons.css +++ b/libs/features/icons/icons.css @@ -107,6 +107,11 @@ display: block; } +.milo-tooltip.hide-tooltip::before, +.milo-tooltip.hide-tooltip::after { + display: none; +} + @media (max-width: 600px) { .milo-tooltip::before { max-width: 180px; diff --git a/libs/navigation/bootstrapper.js b/libs/navigation/bootstrapper.js index 2b76e09dd1..423132413d 100644 --- a/libs/navigation/bootstrapper.js +++ b/libs/navigation/bootstrapper.js @@ -21,6 +21,7 @@ export default async function bootstrapBlock(initBlock, blockConfig) { if (blockConfig.targetEl === 'header') { setNavLayout(); const metaTags = [ + { key: 'gnavSource', name: 'gnav-source' }, { key: 'unavComponents', name: 'universal-nav' }, { key: 'redirect', name: 'adobe-home-redirect' }, { key: 'mobileGnavV2', name: 'mobile-gnav-v2' }, diff --git a/libs/navigation/navigation.js b/libs/navigation/navigation.js index 132900a057..33e86a2d3e 100644 --- a/libs/navigation/navigation.js +++ b/libs/navigation/navigation.js @@ -103,7 +103,10 @@ export default async function loadBlock(configs, customLib) { } // 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([ + const [ + { default: bootstrapBlock }, + { default: locales }, + { setConfig, getConfig }] = await Promise.all([ import('./bootstrapper.js'), import('../utils/locales.js'), import('../utils/utils.js'), @@ -117,7 +120,7 @@ export default async function loadBlock(configs, customLib) { pathname: `/${locale}`, miloLibs: `${miloLibs}/libs`, locales: configs.locales || locales, - contentRoot: authoringPath || footer.authoringPath, + contentRoot: authoringPath || footer?.authoringPath, stageDomainsMap: getStageDomainsMap(stageDomainsMap), origin: `https://main--federal--adobecom.aem.${env === 'prod' ? 'live' : 'page'}`, allowedOrigins: [...allowedOrigins, `https://main--federal--adobecom.aem.${env === 'prod' ? 'live' : 'page'}`], @@ -126,12 +129,15 @@ export default async function loadBlock(configs, customLib) { setConfig(clientConfig); for await (const block of blockConfig) { const configBlock = configs[block.key]; + const config = getConfig(); + const gnavSource = `${config?.locale?.contentRoot}/gnav`; try { if (configBlock) { if (block.key === 'header') { const { default: init } = await import('../blocks/global-navigation/global-navigation.js'); await bootstrapBlock(init, { ...block, + gnavSource, unavComponents: configBlock.unav?.unavComponents, redirect: configBlock.redirect, layout: configBlock.layout, @@ -153,6 +159,11 @@ export default async function loadBlock(configs, customLib) { } } catch (e) { configBlock.onError?.(e); + window.lana.log(`${e.message} | gnav-source: ${gnavSource} | href: ${window.location.href}`, { + clientId: 'feds-milo', + tags: 'standalone-gnav', + errorType: e.errorType, + }); } } } diff --git a/libs/styles/styles.css b/libs/styles/styles.css index 14d2d4627f..728b8e88e2 100644 --- a/libs/styles/styles.css +++ b/libs/styles/styles.css @@ -9,8 +9,8 @@ --feds-localnav-height: 40px; /* Colors */ - --link-color: rgb(59, 99, 251); - --link-hover-color: rgb(39, 77, 234); + --link-color: rgb(39, 77, 234); + --link-hover-color: rgb(29, 62, 207); --link-color-dark: rgb(59, 99, 251); --link-hover-color-dark: rgb(39, 77, 234); --background-color: #fff; diff --git a/nala/features/mas/ccd/masccd.spec.js b/nala/features/mas/ccd/masccd.spec.js index 4797c68d83..5ef50055cd 100644 --- a/nala/features/mas/ccd/masccd.spec.js +++ b/nala/features/mas/ccd/masccd.spec.js @@ -32,6 +32,7 @@ module.exports = { description: 'Save over 65% on Photoshop and more than 20 apps for the first year. Plus get the first month on us when purchase by Sep 2', price: 'US$19.99/mo', strikethroughPrice: 'US$59.99/mo', + abmLabel: 'Annual, paid monthly', cta: 'Buy now', offerid: '951DCCB08194F40B9C79951675547DF5', linkText: 'See terms', diff --git a/nala/features/mas/ccd/masccd.test.js b/nala/features/mas/ccd/masccd.test.js index 678d74b3c5..2f6ece89f6 100644 --- a/nala/features/mas/ccd/masccd.test.js +++ b/nala/features/mas/ccd/masccd.test.js @@ -83,7 +83,7 @@ test.describe('CCD Merchcard feature test suite', () => { }); }); - // @MAS-CCD-suggested-strikethrough : CCD suggested card with eyebrow, legal link and strikethrough price + // @MAS-CCD-suggested-strikethrough : CCD suggested card with eyebrow, legal link, strikethrough price and ABM price label test(`${features[1].name},${features[1].tags}`, async ({ page, baseURL }) => { const testPage = `${baseURL}${features[1].path}${miloLibs}`; const { data } = features[1]; @@ -113,6 +113,7 @@ test.describe('CCD Merchcard feature test suite', () => { await expect(await CCD.getCardPrice(data.id, 'suggested')).toBeVisible(); await expect(await CCD.getCardPrice(data.id, 'suggested')).toContainText(data.price); await expect(await CCD.getCardPrice(data.id, 'suggested')).toContainText(data.strikethroughPrice); + await expect(await CCD.getCardPrice(data.id, 'suggested')).toContainText(data.abmLabel); await expect(await CCD.getCardCTA(data.id, 'suggested')).toBeVisible(); await expect(await CCD.getCardCTA(data.id, 'suggested')).toHaveAttribute('class', /primary/); await expect(await CCD.getCardCTA(data.id, 'suggested')).toContainText(data.cta); diff --git a/test/blocks/global-footer/global-footer.test.js b/test/blocks/global-footer/global-footer.test.js index 8e8ed697d8..a664230e4f 100644 --- a/test/blocks/global-footer/global-footer.test.js +++ b/test/blocks/global-footer/global-footer.test.js @@ -355,7 +355,7 @@ describe('global footer', () => { }; const logMessage = 'test message'; - const logTags = 'errorType=error,module=global-footer'; + const logTags = 'global-footer'; await logErrorFor(erroneousFunction, logMessage, logTags); expect(window.lana.log.calledOnce).to.be.true; diff --git a/test/blocks/global-navigation/features/breadcrumbs.test.js b/test/blocks/global-navigation/features/breadcrumbs.test.js index 95235c705f..2ae8d1066c 100644 --- a/test/blocks/global-navigation/features/breadcrumbs.test.js +++ b/test/blocks/global-navigation/features/breadcrumbs.test.js @@ -33,6 +33,15 @@ const breadcrumbWithCase = () => toFragment`
`; +const breadcrumbWithEmptyList = () => toFragment` + +`; + export const assertBreadcrumb = ({ breadcrumb, length }) => { expect(breadcrumb.querySelector('nav')).to.exist; expect(breadcrumb.querySelector('ul').children.length).to.equal(length); @@ -85,6 +94,12 @@ describe('breadcrumbs', () => { expect(breadcrumb.querySelector('ul li:last-of-type').innerText.trim().replace(/^\//, '').trim()).to.equal('Custom Title'); }); + it('should fail to load when list is empty', async () => { + document.head.innerHTML = ''; + const breadcrumb = await breadcrumbs(breadcrumbWithEmptyList()); + expect(breadcrumb).to.be.null; + }); + it('should create a breadcrumb SEO element', async () => { await breadcrumbs(breadcrumbMock()); const script = document.querySelector('script'); diff --git a/test/blocks/global-navigation/global-navigation.test.js b/test/blocks/global-navigation/global-navigation.test.js index f7352f2d5b..5cb31342c5 100644 --- a/test/blocks/global-navigation/global-navigation.test.js +++ b/test/blocks/global-navigation/global-navigation.test.js @@ -102,7 +102,7 @@ describe('global navigation', () => { }); window.adobeIMS = { isSignedInUser: () => true }; await gnav.imsReady(); - expect(window.lana.log.getCalls().find((c) => c.args[0].includes('issues within onReady'))).to.exist; + expect(window.lana.log.getCalls().find((c) => c.firstArg.includes('issues within imsReady'))).to.exist; window.adobeIMS = ogIms; }); diff --git a/test/blocks/global-navigation/utilities/utilities.test.js b/test/blocks/global-navigation/utilities/utilities.test.js index b3972be8ac..39fe835c60 100644 --- a/test/blocks/global-navigation/utilities/utilities.test.js +++ b/test/blocks/global-navigation/utilities/utilities.test.js @@ -17,7 +17,7 @@ import { dropWhile, } from '../../../../libs/blocks/global-navigation/utilities/utilities.js'; import { setConfig, getConfig } from '../../../../libs/utils/utils.js'; -import { createFullGlobalNavigation, config } from '../test-utilities.js'; +import { createFullGlobalNavigation, config, mockRes } from '../test-utilities.js'; import mepInBlock from '../mocks/mep-config.js'; import { getFedsPlaceholderConfig } from '../../../../libs/utils/federated.js'; @@ -38,6 +38,16 @@ describe('global navigation utilities', () => { }); }); + it('fetchAndProcessPlainHtml with failed fetch call should return null fragment', () => { + sinon.stub(window, 'fetch').callsFake((url) => { + if (url.includes('/old/navigations')) return mockRes({ payload: null, ok: false, status: 400 }); + return null; + }); + fetchAndProcessPlainHtml({ url: '/old/navigations' }).then((fragment) => { + expect(fragment).to.be.null; + }); + }); + it('toFragment', () => { expect(toFragment).to.exist; const fragment = toFragment`
test
`; diff --git a/test/blocks/library-config/mocks/blocks/chart/docx.html b/test/blocks/library-config/mocks/blocks/chart/docx.html index 957326f441..0269a8aa3c 100644 --- a/test/blocks/library-config/mocks/blocks/chart/docx.html +++ b/test/blocks/library-config/mocks/blocks/chart/docx.html @@ -1,3 +1,3 @@
chart (area, green, border)

Lorem ipsum dolor sit amet, consectetuer adipiscing elit.

-
Revenue dollars: 2020 vs 2021 forecasted vs 2021 actuals.
https://main--milo--adobecom.hlx.page/docs/library/blocks/chart_data/areachart.json
Footnote lorem ipsum dolor sit amet, consectetuer adipiscing elit.
+ Revenue dollars: 2020 vs 2021 forecasted vs 2021 actuals.https://main--milo--adobecom.hlx.page/docs/library/blocks/chart_data/areachart.jsonFootnote lorem ipsum dolor sit amet, consectetuer adipiscing elit.
diff --git a/test/blocks/library-config/mocks/blocks/container/docx-container.html b/test/blocks/library-config/mocks/blocks/container/docx-container.html index 752c26d474..526fa2120e 100644 --- a/test/blocks/library-config/mocks/blocks/container/docx-container.html +++ b/test/blocks/library-config/mocks/blocks/container/docx-container.html @@ -1,15 +1,15 @@ -
carousel (lightbox)
Carousel lightbox
section-metadata
stylexxl spacing

---

Avocado surprise

+
carousel (lightbox)
Carousel lightbox

section-metadata
stylexxl spacing

---


Avocado surprise


-

section-metadata
carouselCarousel lightbox
styleCenter

---

Cabage surprise

+


section-metadata
carouselCarousel lightbox
styleCenter

---


Cabage surprise


-

section-metadata
carouselCarousel lightbox
styleCenter

---

+


section-metadata
carouselCarousel lightbox
styleCenter

---


diff --git a/test/blocks/library-config/mocks/blocks/container/docx-single-block.html b/test/blocks/library-config/mocks/blocks/container/docx-single-block.html index 169dcc33bd..c34ca96620 100644 --- a/test/blocks/library-config/mocks/blocks/container/docx-single-block.html +++ b/test/blocks/library-config/mocks/blocks/container/docx-single-block.html @@ -1 +1 @@ -
carousel (container0)
Carousel container
+
carousel (container0)
Carousel container

diff --git a/test/blocks/library-config/mocks/blocks/marquee/docx.html b/test/blocks/library-config/mocks/blocks/marquee/docx.html index ff240b8b7f..df5dba1a1a 100644 --- a/test/blocks/library-config/mocks/blocks/marquee/docx.html +++ b/test/blocks/library-config/mocks/blocks/marquee/docx.html @@ -25,4 +25,4 @@

Heading XL Marquee standard med - +
diff --git a/test/blocks/library-config/mocks/blocks/text/docx.html b/test/blocks/library-config/mocks/blocks/text/docx.html index 2d9e67ce1d..6f195254b2 100644 --- a/test/blocks/library-config/mocks/blocks/text/docx.html +++ b/test/blocks/library-config/mocks/blocks/text/docx.html @@ -2,4 +2,4 @@

Text

Kick things off with hundreds of premium and free presets you can access with your Lightroom subscription.

Learn more Explore the premium collection

- +
diff --git a/test/blocks/table/mocks/body.html b/test/blocks/table/mocks/body.html index 6fb93feb80..2d15dff0fc 100644 --- a/test/blocks/table/mocks/body.html +++ b/test/blocks/table/mocks/body.html @@ -510,7 +510,7 @@

Add a business stamp

-

+

diff --git a/test/blocks/table/table.test.js b/test/blocks/table/table.test.js index 6b0d400cda..6810a28ca0 100644 --- a/test/blocks/table/table.test.js +++ b/test/blocks/table/table.test.js @@ -127,5 +127,25 @@ describe('table and tablemetadata', () => { expect(selectElement.getAttribute('aria-label')).to.equal(ariaLabel); }); }); + + it('should show and hide tooltip on hover, focus, and Escape key', async () => { + const tooltip = document.querySelector('.milo-tooltip'); + expect(tooltip).to.exist; + + tooltip.dispatchEvent(new Event('mouseenter')); + expect(tooltip.classList.contains('hide-tooltip')).to.be.false; + + tooltip.dispatchEvent(new Event('mouseleave')); + expect(tooltip.classList.contains('hide-tooltip')).to.be.true; + + tooltip.dispatchEvent(new Event('focus')); + expect(tooltip.classList.contains('hide-tooltip')).to.be.false; + + tooltip.dispatchEvent(new Event('blur')); + expect(tooltip.classList.contains('hide-tooltip')).to.be.true; + + await sendKeys({ press: 'Escape' }); + expect(tooltip.classList.contains('hide-tooltip')).to.be.true; + }); }); }); diff --git a/test/features/icons/icons.test.js b/test/features/icons/icons.test.js index 2c847a4eae..669551a807 100644 --- a/test/features/icons/icons.test.js +++ b/test/features/icons/icons.test.js @@ -14,7 +14,7 @@ document.body.innerHTML = await readFile({ path: './mocks/body.html' }); let icons; -describe('Icon Suppprt', () => { +describe('Icon Support', () => { let paramsGetStub; before(() => { @@ -27,15 +27,14 @@ describe('Icon Suppprt', () => { }); before(async () => { + document.body.innerHTML = await readFile({ path: './mocks/body.html' }); icons = document.querySelectorAll('span.icon'); await loadIcons(icons, config); - await loadIcons(icons, config); // Test duplicate icon not created if run twice }); it('Handles tooltip- prefix correctly', async () => { const tooltipIcon = createTag('span', { class: 'icon icon-tooltip-info' }); await loadIcons([tooltipIcon], config); - const svgIcon = tooltipIcon.querySelector(':scope svg'); expect(svgIcon).to.exist; @@ -45,27 +44,123 @@ describe('Icon Suppprt', () => { it('Fetches successfully with cache control enabled', async () => { const otherIcons = [createTag('span', { class: 'icon icon-play' })]; + document.body.appendChild(otherIcons[0]); + await loadIcons(otherIcons, config); + expect(otherIcons[0].querySelector('svg')).to.exist; }); - it('Replaces span.icon', async () => { + it('Renders an SVG after loading the icons', () => { const selector = icons[0].querySelector(':scope svg'); expect(selector).to.exist; }); it('No duplicate icon', async () => { + await loadIcons(icons, config); const svgs = icons[0].querySelectorAll(':scope svg'); expect(svgs.length).to.equal(1); }); - it('Creates default tooltip', async () => { + it('Creates default tooltip (right-aligned)', () => { const tooltip = document.querySelector('.milo-tooltip.right'); expect(tooltip).to.exist; expect(tooltip.dataset.tooltip).to.equal('This is my tooltip text.'); }); - it('Creates top tooltip', async () => { + it('Creates top tooltip', () => { const tooltip = document.querySelector('.milo-tooltip.top'); expect(tooltip).to.exist; }); }); + +describe('Tooltip', () => { + let iconTooltip; + let wrapper; + let normalIcon; + + beforeEach(() => { + iconTooltip = createTag('span', { class: 'icon icon-tooltip' }); + wrapper = createTag('em', {}, 'top|This is a tooltip text.'); + wrapper.appendChild(iconTooltip); + document.body.appendChild(wrapper); + + normalIcon = createTag('span', { class: 'icon icon-play' }); + document.body.appendChild(normalIcon); + }); + + afterEach(() => { + document.body.innerHTML = ''; + }); + + it('Should only decorate icons with "icon-tooltip" class', async () => { + await loadIcons([iconTooltip, normalIcon], config); + + expect(iconTooltip.dataset.tooltip).to.equal('This is a tooltip text.'); + expect(iconTooltip.classList.contains('milo-tooltip')).to.be.true; + expect(normalIcon.dataset.tooltip).to.be.undefined; + expect(normalIcon.classList.contains('milo-tooltip')).to.be.false; + }); + + it('Tooltip should not be visible by default', async () => { + await loadIcons([iconTooltip], config); + + const tooltipContent = document.querySelector('.milo-tooltip[data-tooltip]'); + expect(tooltipContent).to.exist; + expect(tooltipContent.style.display).to.equal(''); + expect(tooltipContent.style.visibility).to.equal(''); + }); + + it('Tooltip should become visible on focus', async () => { + document.body.appendChild(wrapper); + await loadIcons([iconTooltip], config); + const tooltip = document.querySelector('.milo-tooltip'); + expect(tooltip).to.exist; + expect(tooltip.dataset.tooltip).to.equal('This is a tooltip text.'); + expect(tooltip.getAttribute('role')).to.equal('button'); + expect(tooltip.className).to.contain('top'); + }); + + it('Creates a tooltip without default alignment (left)', () => { + const customTooltip = createTag('span', { class: 'icon icon-tooltip milo-tooltip left', 'data-tooltip': 'Left-aligned tooltip' }); + document.body.appendChild(customTooltip); + + loadIcons([customTooltip], config); + + const tooltip = document.querySelector('.milo-tooltip.left'); + expect(tooltip).to.exist; + expect(tooltip.dataset.tooltip).to.equal('Left-aligned tooltip'); + }); + + it('Should replace wrapper with icon', async () => { + await loadIcons([iconTooltip], config); + + expect(document.body.contains(wrapper)).to.be.false; + expect(document.body.contains(iconTooltip)).to.be.true; + }); + + it('Should assign correct default icon class and name', async () => { + await loadIcons([iconTooltip], config); + expect(iconTooltip.classList.contains('icon-info')).to.be.true; + expect(iconTooltip.dataset.name).to.be.undefined; + }); + + it('Should not assign default icon class if already defined', async () => { + const customIcon = createTag('span', { class: 'icon icon-warning' }); + document.body.appendChild(customIcon); + + await loadIcons([customIcon], config); + + expect(customIcon.classList.contains('icon-warning')).to.be.true; + expect(customIcon.classList.contains('icon-info')).to.be.false; + }); + + it('Should not add tooltip if data-tooltip is missing', async () => { + const noTooltipIcon = createTag('span', { class: 'icon icon-tooltip' }); + document.body.appendChild(noTooltipIcon); + + await loadIcons([noTooltipIcon], config); + + expect(noTooltipIcon.classList.contains('milo-tooltip')).to.be.false; + expect(noTooltipIcon.dataset.tooltip).to.be.undefined; + }); +}); diff --git a/test/navigation/navigation.test.js b/test/navigation/navigation.test.js index 1ea752635f..6ac70edb17 100644 --- a/test/navigation/navigation.test.js +++ b/test/navigation/navigation.test.js @@ -18,7 +18,7 @@ describe('Navigation component', async () => { return null; }); - setConfig({ miloLibs, contentRoot: '/federal/dev' }); + setConfig({ miloLibs }); }); afterEach(() => { @@ -26,29 +26,44 @@ describe('Navigation component', async () => { }); it('Renders the footer block', async () => { - await loadBlock({ authoringPath: '/federal/dev', footer: { privacyId: '12343' }, env: 'qa' }, 'http://localhost:2000'); + await loadBlock({ authoringPath: '/federal/dev', footer: { privacyId: '12343', onReady: 'dede' }, env: 'qa' }, 'http://localhost:2000'); const el = document.getElementsByTagName('footer'); expect(el).to.exist; }); - it('Renders the footer block should not load when config is not passed', async () => { + it('Renders the header block', async () => { + const onReady = stub(); + await loadBlock({ authoringPath: '/federal/dev', header: { imsClientId: 'fedsmilo', onReady, layout: 'fullWidth', noBorder: 'true' }, env: 'prod', theme: 'dark' }, 'http://localhost:2000'); + const el = document.getElementsByTagName('header'); + expect(el).to.exist; + expect(onReady.called).to.be.true; + }); + + it('Renders the localnav if isLocalNav key is passed', async () => { + const onReady = stub(); + await loadBlock({ authoringPath: '/federal/dev', header: { imsClientId: 'fedsmilo', onReady, isLocalNav: true, jarvis: { id: '1.1' } }, env: 'prod', theme: 'dark' }, 'http://localhost:2000'); + const el = document.querySelector('.feds-localnav'); + expect(el).to.exist; + }); + + it('Should not render the footer block when config is not passed', async () => { try { - const onError = stub(); - await loadBlock({ authoringPath: '/federal/dev-new', env: 'qa', footer: { privacyId: '12343' }, header: { onError } }, 'http://localhost:2000'); + await loadBlock({ env: 'qa', authoringPath: '/federal/dev', footer: {} }, 'http://localhost:2000'); const el = document.getElementsByTagName('footer'); expect(el).to.not.exist; - expect(onError.called).to.be.true; } catch (e) { // handle error } }); - it('Renders the header block', async () => { - const onReady = stub(); - 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; + it('Should not render the header block when config is not passed', async () => { + try { + await loadBlock({ env: 'qa', footer: { privacyId: '12343' } }, 'http://localhost:2000'); + const el = document.getElementsByTagName('header'); + expect(el).to.not.exist; + } catch (e) { + // handle error + } }); it('Does not render either header or footer if not found in configs', async () => { @@ -60,15 +75,6 @@ describe('Navigation component', async () => { expect(footer).to.be.empty; }); - it('Does not render either header or footer if configs is not passed', async () => { - document.body.innerHTML = await readFile({ path: './mocks/body.html' }, 'http://localhost:2000'); - await loadBlock(); - const header = document.getElementsByTagName('header'); - const footer = document.getElementsByTagName('footer'); - expect(header).to.be.empty; - expect(footer).to.be.empty; - }); - it('Renders the footer block with authoringpath passed in footer', async () => { await loadBlock({ footer: { privacyId: '12343', authoringPath: '/federal/dev' }, env: 'qa' }, 'http://localhost:2000'); const el = document.getElementsByTagName('footer');