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/article-feed/article-feed.js b/libs/blocks/article-feed/article-feed.js index ae7322283f..766863a6a2 100644 --- a/libs/blocks/article-feed/article-feed.js +++ b/libs/blocks/article-feed/article-feed.js @@ -303,7 +303,7 @@ async function buildFilter(type, tax, block, config) { dropdown.setAttribute('aria-labelledby', `${type}-filter-button`); dropdown.setAttribute('role', 'menu'); - const SEARCH_ICON = ` + const SEARCH_ICON = ``; const searchBar = createTag('div', { class: 'filter-search' }); diff --git a/libs/blocks/aside/aside.js b/libs/blocks/aside/aside.js index 92ea2500d4..898bc2bf9c 100644 --- a/libs/blocks/aside/aside.js +++ b/libs/blocks/aside/aside.js @@ -33,7 +33,7 @@ const blockConfig = { }, }; const FORMAT_REGEX = /^format:/i; -const closeSvg = ` +const closeSvg = `AdChoices icon`; const SUPPORTED_SOCIAL = ['facebook', 'instagram', 'twitter', 'linkedin', 'pinterest', 'discord', 'behance', 'youtube', 'weibo', 'social-media']; 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 7fe74c487d..0b370ab828 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 { @@ -212,11 +212,13 @@ class Footer { aria-expanded="false" aria-haspopup="true" role="button"> - + ${regionPickerTextElem} `; + regionPickerElem.dataset.modalPath = `${url.pathname}#_inline`; + regionPickerElem.dataset.modalHash = url.hash; const regionPickerWrapperClass = 'feds-regionPicker-wrapper'; this.elements.regionPicker = toFragment`
${regionPickerElem} @@ -230,24 +232,48 @@ class Footer { // Hash -> region selector opens a modal decorateAutoBlock(regionPickerElem); // add modal-specific attributes // TODO remove logs after finding the root cause for the region picker 404s -> MWPW-143627 + regionPickerElem.href = url.hash; if (regionPickerElem.classList[0] !== 'modal') { lanaLog({ message: `Modal block class missing from region picker pre loading the block; locale: ${locale}; regionPickerElem: ${regionPickerElem.outerHTML}`, tags: 'errorType=warn,module=global-footer', }); } - await loadBlock(regionPickerElem); // load modal logic and styles + loadStyle(`${base}/blocks/modal/modal.css`); + const { default: initModal } = await import('../modal/modal.js'); + const modal = await initModal(regionPickerElem); + + const loadRegionNav = async () => { + const block = document.querySelector('.region-nav'); + if (block && getConfig().standaloneGnav) { + // on standalone the region-nav will fail to load automatically through + // the modal calling fragment.js. In that case we will have data-failed=true + // and we should manually load region nav + // If that's not the case then we're not a standalone gnav + // and we mustn't load region-nav twice. + if (block.getAttribute('data-failed') !== 'true') return; + block.classList.add('hide'); + loadStyle(`${base}/blocks/region-nav/region-nav.css`); + const { default: initRegionNav } = await import('../region-nav/region-nav.js'); + initRegionNav(block); + // decoratePlaceholders(block, getConfig()); + block.classList.remove('hide'); + } + }; + + if (modal) await loadRegionNav(); // just in case the modal is already open + if (regionPickerElem.classList[0] !== 'modal') { lanaLog({ message: `Modal block class missing from region picker post loading the block; locale: ${locale}; regionPickerElem: ${regionPickerElem.outerHTML}`, tags: 'errorType=warn,module=global-footer', }); } - // 'decorateAutoBlock' logic replaces class name entirely, need to add it back - regionPickerElem.classList.add(regionPickerClass); regionPickerElem.addEventListener('click', () => { if (!isRegionPickerExpanded()) { regionPickerElem.setAttribute('aria-expanded', 'true'); + // wait for the modal to load before we load the region nav + window.addEventListener('milo:modal:loaded', loadRegionNav, { once: true }); } }); // Set aria-expanded to false when region modal is closed @@ -262,7 +288,8 @@ class Footer { regionSelector.href = localizeLink(regionSelector.href); decorateAutoBlock(regionSelector); // add fragment-specific class(es) this.elements.regionPicker.append(regionSelector); // add fragment after regionPickerElem - await loadBlock(regionSelector); // load fragment and replace original link + const { default: initFragment } = await import('../fragment/fragment.js'); + await initFragment(regionSelector); // load fragment and replace original link // Update aria-expanded on click regionPickerElem.addEventListener('click', (e) => { e.preventDefault(); @@ -278,7 +305,7 @@ class Footer { }); } - return this.regionPicker; + return this.elements.regionPicker; }; decorateSocial = () => { @@ -301,7 +328,7 @@ class Footer { aria-label="${platform}" daa-ll="${getAnalyticsValue(platform, index + 1)}" target="_blank"> - + @@ -331,7 +358,7 @@ class Footer { // Add Ad Choices icon const adChoicesElem = privacyContent.querySelector('a[href*="#interest-based-ads"]'); - adChoicesElem?.prepend(toFragment` + adChoicesElem?.prepend(toFragment``); 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 a1879aa779..dd8dbe3179 100644 --- a/libs/blocks/global-navigation/global-navigation.js +++ b/libs/blocks/global-navigation/global-navigation.js @@ -1,3 +1,4 @@ +/* eslint import/no-relative-packages: 0 */ /* eslint-disable no-async-promise-executor */ import { getConfig, @@ -20,7 +21,6 @@ import { isTangentToViewport, lanaLog, loadBaseStyles, - loadBlock, loadDecorateMenu, rootPath, loadStyles, @@ -222,7 +222,7 @@ const decorateProfileTrigger = async ({ avatar }) => { let keyboardNav; const setupKeyboardNav = async () => { keyboardNav = keyboardNav || new Promise(async (resolve) => { - const KeyboardNavigation = await loadBlock('./keyboard/index.js'); + const { default: KeyboardNavigation } = await import('./utilities/keyboard/index.js'); const instance = new KeyboardNavigation(); resolve(instance); }); @@ -372,6 +372,9 @@ class Gnav {
`; this.block.append(this.elements.curtain, this.elements.aside, this.elements.topnavWrapper); + // TODO: Remove with mobile redesign code + const firstLocalNavItem = this.elements.navWrapper.querySelector('.feds-nav .feds-navItem:not(.feds-navItem--section) a'); + if (firstLocalNavItem) [firstLocalNavItem.textContent] = firstLocalNavItem.textContent.split('|'); }; addChangeEventListeners = () => { @@ -425,17 +428,17 @@ class Gnav { this.block.removeEventListener('keydown', this.loadDelayed); if (this.searchPresent()) { const [ - Search, + { default: Search }, ] = await Promise.all([ - loadBlock('../features/search/gnav-search.js'), + import('./features/search/gnav-search.js'), loadStyles(rootPath('features/search/gnav-search.css')), ]); this.Search = Search; } if (!this.useUniversalNav) { - const [ProfileDropdown] = await Promise.all([ - loadBlock('../features/profile/dropdown.js'), + const [{ default: ProfileDropdown }] = await Promise.all([ + import('./features/profile/dropdown.js'), loadStyles(rootPath('features/profile/dropdown.css')), ]); this.ProfileDropdown = ProfileDropdown; @@ -540,7 +543,7 @@ class Gnav { const unavVersion = new URLSearchParams(window.location.search).get('unavVersion') || '1.3'; await Promise.all([ loadScript(`https://${environment}.adobeccstatic.com/unav/${unavVersion}/UniversalNav.js`), - loadStyles(`https://${environment}.adobeccstatic.com/unav/${unavVersion}/UniversalNav.css`), + loadStyles(`https://${environment}.adobeccstatic.com/unav/${unavVersion}/UniversalNav.css`, true), ]); const getChildren = () => { @@ -910,7 +913,7 @@ class Gnav { const menuLogic = await loadDecorateMenu(); - menuLogic.decorateMenu({ + await menuLogic.decorateMenu({ item, template, type: itemType, @@ -1021,7 +1024,7 @@ class Gnav { const breadcrumbsElem = this.block.querySelector('.breadcrumbs'); // Breadcrumbs are not initially part of the nav, need to decorate the links if (breadcrumbsElem) decorateLinks(breadcrumbsElem); - const createBreadcrumbs = await loadBlock('../features/breadcrumbs/breadcrumbs.js'); + const { default: createBreadcrumbs } = await import('./features/breadcrumbs/breadcrumbs.js'); this.elements.breadcrumbsWrapper = await createBreadcrumbs(breadcrumbsElem); return this.elements.breadcrumbsWrapper; }; @@ -1091,5 +1094,6 @@ export default async function init(block) { const mepMartech = mep?.martech || ''; block.setAttribute('daa-lh', `gnav|${getExperienceName()}${mepMartech}`); if (isDarkMode()) block.classList.add('feds--dark'); + block.classList.add('ready'); return gnav; } diff --git a/libs/blocks/global-navigation/utilities/getUserEntitlements.js b/libs/blocks/global-navigation/utilities/getUserEntitlements.js index 1166146179..b9812236b6 100644 --- a/libs/blocks/global-navigation/utilities/getUserEntitlements.js +++ b/libs/blocks/global-navigation/utilities/getUserEntitlements.js @@ -1,3 +1,4 @@ +/* eslint import/no-relative-packages: 0 */ /* eslint-disable camelcase */ import { getConfig } from '../../../utils/utils.js'; diff --git a/libs/blocks/global-navigation/utilities/getUserEventHistory.js b/libs/blocks/global-navigation/utilities/getUserEventHistory.js index e1fcc23fee..25937e34a2 100644 --- a/libs/blocks/global-navigation/utilities/getUserEventHistory.js +++ b/libs/blocks/global-navigation/utilities/getUserEventHistory.js @@ -1,3 +1,4 @@ +/* eslint import/no-relative-packages: 0 */ /* eslint-disable no-promise-executor-return, no-async-promise-executor */ import { getConfig } from '../../../utils/utils.js'; diff --git a/libs/blocks/global-navigation/utilities/utilities.js b/libs/blocks/global-navigation/utilities/utilities.js index 3848fdc601..ed7fcb3491 100644 --- a/libs/blocks/global-navigation/utilities/utilities.js +++ b/libs/blocks/global-navigation/utilities/utilities.js @@ -1,3 +1,4 @@ +/* eslint import/no-relative-packages: 0 */ import { getConfig, getMetadata, loadStyle, loadLana, decorateLinks, localizeLink, } from '../../../utils/utils.js'; @@ -26,16 +27,16 @@ export const selectors = { }; export const icons = { - brand: '', - company: '', - search: '', - home: '', + brand: '', + company: '', + search: '', + home: '', }; export const darkIcons = { ...icons, - brand: '', - company: '', + brand: '', + company: '', }; export const lanaLog = ({ message, e = '', tags = 'errorType=default' }) => { @@ -134,7 +135,9 @@ export function rootPath(path) { return url; } -export function loadStyles(url) { +export function loadStyles(url, override = false) { + const { standaloneGnav } = getConfig(); + if (standaloneGnav && !override) return; loadStyle(url, (e) => { if (e === 'error') { lanaLog({ @@ -155,6 +158,8 @@ export function isDarkMode() { // since they can be independent of each other. // CSS imports were not used due to duplication of file include export async function loadBaseStyles() { + const { standaloneGnav } = getConfig(); + if (standaloneGnav) return; if (isDarkMode()) { new Promise((resolve) => { loadStyle(rootPath('base.css'), resolve); }) .then(() => loadStyles(rootPath('dark-nav.css'))); @@ -164,10 +169,6 @@ export async function loadBaseStyles() { } } -export function loadBlock(path) { - return import(path).then((module) => module.default); -} - let cachedDecorateMenu; export async function loadDecorateMenu() { if (cachedDecorateMenu) return cachedDecorateMenu; @@ -177,15 +178,12 @@ export async function loadDecorateMenu() { resolve = _resolve; }); - const [{ decorateMenu, decorateLinkGroup }] = await Promise.all([ - loadBlock('./menu/menu.js'), + const [menu] = await Promise.all([ + import('./menu/menu.js'), loadStyles(rootPath('utilities/menu/menu.css')), ]); - resolve({ - decorateMenu, - decorateLinkGroup, - }); + resolve(menu.default); return cachedDecorateMenu; } diff --git a/libs/blocks/gnav/gnav-appLauncher.js b/libs/blocks/gnav/gnav-appLauncher.js index c36f224ca2..6b907f9c0a 100644 --- a/libs/blocks/gnav/gnav-appLauncher.js +++ b/libs/blocks/gnav/gnav-appLauncher.js @@ -1,6 +1,6 @@ import { createTag } from '../../utils/utils.js'; -const WAFFLE_ICON = ''; +const WAFFLE_ICON = ''; function decorateAppsMenu(profileEl, appsDom, toggle) { const appsNavItem = createTag('div', { class: 'gnav-navitem app-launcher has-menu', 'da-ll': 'App Launcher' }); diff --git a/libs/blocks/gnav/gnav.js b/libs/blocks/gnav/gnav.js index 10ebe8b701..f3d8237afc 100644 --- a/libs/blocks/gnav/gnav.js +++ b/libs/blocks/gnav/gnav.js @@ -13,10 +13,10 @@ import { analyticsGetLabel, } from '../../martech/attributes.js'; -const COMPANY_IMG = ''; -const BRAND_IMG = ''; -const BRAND_LOGO_AS_TEXT = ''; -const SEARCH_ICON = ''; +const COMPANY_IMG = ''; +const BRAND_IMG = ''; +const BRAND_LOGO_AS_TEXT = ''; +const SEARCH_ICON = ''; const SEARCH_DEBOUNCE_MS = 300; export const IS_OPEN = 'is-open'; const SEARCH_TYPE_CONTEXTUAL = 'contextual'; diff --git a/libs/blocks/modal/modal.js b/libs/blocks/modal/modal.js index 5ba3f3a0c8..09539695d0 100644 --- a/libs/blocks/modal/modal.js +++ b/libs/blocks/modal/modal.js @@ -4,7 +4,7 @@ import { createTag, getMetadata, localizeLink, loadStyle, getConfig } from '../. import { decorateSectionAnalytics } from '../../martech/attributes.js'; const FOCUSABLES = 'a:not(.hide-video), button, input, textarea, select, details, [tabindex]:not([tabindex="-1"]'; -const CLOSE_ICON = ` +const CLOSE_ICON = ` diff --git a/libs/martech/helpers.js b/libs/martech/helpers.js index 25760ba57a..8ac87e40c6 100644 --- a/libs/martech/helpers.js +++ b/libs/martech/helpers.js @@ -352,7 +352,10 @@ function updateAMCVCookie(ECID) { */ export const loadAnalyticsAndInteractionData = async ({ locale, env, calculatedTimeout }) => { const value = getCookie('kndctr_9E1005A551ED61CA0A490D45_AdobeOrg_consent', true); - if (value?.[1] === 'general' && value?.[2] === 'out') { + const isRejectedDecodedURI = value?.[2] === undefined && decodeURIComponent(value?.[1]) === 'general=out'; + const isRejectedURI = value?.[1] === 'general' && value?.[2] === 'out'; + + if (isRejectedDecodedURI || isRejectedURI) { return Promise.reject(new Error('Consent Cookie doesnt allow interact')); } diff --git a/libs/mep/dc0994/aside/aside.js b/libs/mep/dc0994/aside/aside.js index 2283e0e1d9..16d4f5d459 100644 --- a/libs/mep/dc0994/aside/aside.js +++ b/libs/mep/dc0994/aside/aside.js @@ -33,7 +33,7 @@ const blockConfig = { }, }; const FORMAT_REGEX = /^format:/i; -const closeSvg = ` +const closeSvg = `

+ +

+ Text +

+

+ Text | Aria label +

+

+ Text | Other text | Aria label +

+

+ Text|Other text|Aria label +

+

+ + + Text + +

+

+ + + Text | Aria label + +

diff --git a/test/utils/utils.test.js b/test/utils/utils.test.js index 88a39e6e1d..f908644013 100644 --- a/test/utils/utils.test.js +++ b/test/utils/utils.test.js @@ -212,6 +212,39 @@ describe('Utils', () => { }); }); + describe('Aria label appendment', () => { + it('appends aria label if defined', () => { + const theText = 'Text'; + const theAriaLabel = 'Aria label'; + + const noAriaLabelElem = document.querySelector('.aria-label-none'); + expect(noAriaLabelElem.getAttribute('aria-label')).to.be.null; + expect(noAriaLabelElem.innerText).to.equal(theText); + + const simpleAriaLabelElem = document.querySelector('.aria-label-simple'); + expect(simpleAriaLabelElem.getAttribute('aria-label')).to.equal(theAriaLabel); + expect(simpleAriaLabelElem.innerText).to.equal(theText); + + const pipedAriaLabelElem = document.querySelector('.aria-label-piped'); + expect(pipedAriaLabelElem.getAttribute('aria-label')).to.equal(theAriaLabel); + expect(pipedAriaLabelElem.innerText).to.equal(`${theText} | Other text`); + + const noSpacePipedAriaLabelElem = document.querySelector('.aria-label-piped--no-space'); + expect(noSpacePipedAriaLabelElem.getAttribute('aria-label')).to.equal(theAriaLabel); + expect(noSpacePipedAriaLabelElem.innerText).to.equal(`${theText}|Other text`); + + const iconNoAriaLabelElem = document.querySelector('.aria-label-icon-none'); + expect(iconNoAriaLabelElem.getAttribute('aria-label')).to.be.null; + expect(iconNoAriaLabelElem.querySelector('.icon')).to.exist; + expect(iconNoAriaLabelElem.innerText).to.equal(theText); + + const iconAriaLabelElem = document.querySelector('.aria-label-icon-simple'); + expect(iconAriaLabelElem.getAttribute('aria-label')).to.equal(theAriaLabel); + expect(iconAriaLabelElem.querySelector('.icon')).to.exist; + expect(iconAriaLabelElem.innerText).to.equal(theText); + }); + }); + describe('Fragments', () => { it('fully unwraps a fragment', () => { const fragments = document.querySelectorAll('.link-block.fragment');