diff --git a/.github/workflows/servicenow.yaml b/.github/workflows/servicenow.yaml index 081e2cf08c8..9ec14449fc7 100644 --- a/.github/workflows/servicenow.yaml +++ b/.github/workflows/servicenow.yaml @@ -45,7 +45,7 @@ jobs: python-version: "3.x" - name: Install dependencies run: | - python -m pip install --upgrade pip requests timedelta + python -m pip install --upgrade pip requests - name: Retrieve transaction ID from PR Comments id: retrieve-transactionId-step diff --git a/.kodiak/config.yaml b/.kodiak/config.yaml index b1cc8ad7c12..d1572af351f 100644 --- a/.kodiak/config.yaml +++ b/.kodiak/config.yaml @@ -21,7 +21,6 @@ notifications: fields: assignee: name: sghimpos - customfield_11800: MWPW-166942 # Jira epic security tickets will be assigned to. watchers: # Everyone who is an admin on your repository should be a watcher. - casalino - radu diff --git a/libs/blocks/marketo/marketo.css b/libs/blocks/marketo/marketo.css index a27c7338a36..230a258bf82 100644 --- a/libs/blocks/marketo/marketo.css +++ b/libs/blocks/marketo/marketo.css @@ -550,6 +550,8 @@ .marketo .marketo-overlay { position: absolute; + max-width: 600px; + margin: 0 auto; inset: 0; background: var(--marketo-form-overlay-background); display: flex; @@ -578,6 +580,12 @@ margin-bottom: 12px; } +.marketo .marketo-overlay .debug-info { + font-size: 14px; + text-align: center; + color: var(--marketo-form-text); +} + @media screen and (min-width: 600px) { .marketo .marketo-form-wrapper { padding: var(--spacing-xxl); diff --git a/libs/blocks/marketo/marketo.js b/libs/blocks/marketo/marketo.js index 5f874bfa816..8dd61400fb7 100644 --- a/libs/blocks/marketo/marketo.js +++ b/libs/blocks/marketo/marketo.js @@ -25,13 +25,22 @@ import { SLD, MILO_EVENTS, } from '../../utils/utils.js'; -import { replaceKey } from '../../features/placeholders.js'; +import { replaceKeyArray } from '../../features/placeholders.js'; const ROOT_MARGIN = 50; -const IFRAME_TIMEOUT = 3000; +const FAILURE_TIMEOUT = 10000; +export const LANA_MESSAGE = { + RENDER_FAILED: 'Marketo form did not render', + HANDSHAKE_FAILED: 'Marketo form handshake failed', + RENDER_RECOVERED: 'Marketo form rendered after timeout', + SUBMIT_FAILED: 'Marketo form submit failed', + MARKETO_FORMS_JS: 'Marketo form failed to load forms2.min.js', +}; const FORM_ID = 'form id'; const BASE_URL = 'marketo host'; const MUNCHKIN_ID = 'marketo munckin'; +const FORM_STATUS = 'form.status'; +const FORM_XDFRAME = 'form.xdframe'; const SUCCESS_TYPE = 'form.success.type'; const SUCCESS_CONTENT = 'form.success.content'; const SUCCESS_SECTION = 'form.success.section'; @@ -50,6 +59,30 @@ const FORM_MAP = { }; export const FORM_PARAM = 'form'; +const isVisible = (el) => !!el && (typeof el.checkVisibility === 'function' + ? el.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true }) + : !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length)); + +export const setDataLayer = (key = '', value = '') => { + window.mcz_marketoForm_pref = window.mcz_marketoForm_pref || {}; + if (!value || !key.includes('.')) return; + const keyParts = key.split('.'); + const lastKey = keyParts.pop(); + const formDataObject = keyParts.reduce((obj, part) => { + obj[part] = obj[part] || {}; + return obj[part]; + }, window.mcz_marketoForm_pref); + formDataObject[lastKey] = value; +}; + +export const setDataLayerObj = (formData) => { + Object.entries(formData).forEach(([key, value]) => setDataLayer(key, value)); +}; + +export const getDataLayer = (key = '') => key + .split('.') + .reduce((obj, part) => obj?.[part], window.mcz_marketoForm_pref); + export const formValidate = (formEl) => { formEl.classList.remove('hide-errors'); formEl.classList.add('show-warnings'); @@ -91,23 +124,6 @@ export const decorateURL = async (destination, baseURL = window.location) => { return null; }; -const setPreference = (key = '', value = '') => { - window.mcz_marketoForm_pref = window.mcz_marketoForm_pref || {}; - if (!value || !key.includes('.')) return; - const keyParts = key.split('.'); - const lastKey = keyParts.pop(); - const formDataObject = keyParts.reduce((obj, part) => { - obj[part] = obj[part] || {}; - return obj[part]; - }, window.mcz_marketoForm_pref); - formDataObject[lastKey] = value; -}; - -export const setPreferences = (formData) => { - setPreference('form.status', 'pending'); - Object.entries(formData).forEach(([key, value]) => setPreference(key, value)); -}; - const showSuccessSection = (formData) => { const show = async (sections) => { sections.forEach((section) => section.classList.remove('hide-block')); @@ -167,11 +183,90 @@ const hideSuccessSection = (formData) => { ); }; +export const debugTags = () => { + const len = document.cookie.length; + const tags = ['marketo']; + const signedIn = window.adobeIMS?.isSignedInUser(); + const frameStatus = getDataLayer(FORM_XDFRAME); + if (getDataLayer('form.id')) tags.push(`form-${getDataLayer('form.id')}`); + if (getDataLayer('program.id')) tags.push(`program-${getDataLayer('program.id')}`); + if (getDataLayer(FORM_STATUS)) tags.push(`status-${getDataLayer(FORM_STATUS)}`); + if (len >= 8192) tags.push('cookie-8k'); + else if (len >= 6144) tags.push('cookie-6k'); + else if (len >= 4096) tags.push('cookie-4k'); + tags.push(`ims-${signedIn ? 'signed-in' : 'signed-out'}`); + tags.push(frameStatus === 'ready' ? 'sync-ok' : 'sync-error'); + if (getDataLayer('form.progressive')) tags.push('progressive'); + if (getDataLayer('profile.known_visitor') === true) tags.push('known-visitor'); + tags.push(`pref-lang-${getDataLayer('profile.prefLanguage') ?? ''}`); + return tags; +}; + +const decorateOverlay = async (el, message, callback) => { + if (el.querySelector('.marketo-overlay')) return; + const [errorRefresh, tryAgain] = await replaceKeyArray(['marketo-load-error', 'marketo-try-again'], getConfig()); + const formEl = el.querySelector('form'); + if (formEl) formEl.inert = true; + const searchParams = new URLSearchParams(window.location.search); + const debugMsg = searchParams.get('preview') === '1' ? message : ''; + const errorMessage = createTag('p', { class: 'error', id: 'marketo-error-message' }, errorRefresh); + const formError = createTag('div', { class: 'error-container' }, errorMessage); + if (debugMsg) { + const debugInfo = createTag('p', { class: 'debug-info' }); + debugInfo.textContent = debugMsg; + formError.appendChild(debugInfo); + } + const retryButton = createTag('button', { class: 'retry-button' }, tryAgain); + formError.appendChild(retryButton); + const errorOverlay = createTag( + 'div', + { + class: 'marketo-overlay', + role: 'alertdialog', + 'aria-modal': 'true', + 'aria-labelledby': 'marketo-error-message', + }, + formError, + ); + + retryButton.addEventListener('click', () => { + /* c8 ignore next 3 */ + if (formEl) formEl.inert = false; + errorOverlay.remove(); + if (callback) callback(); + }); + + el.appendChild(errorOverlay); +}; + +export const logFailure = (el, msg) => { + if (el.dataset.mktoFailed) return; + const tags = debugTags(); + el.dataset.mktoFailed = 'true'; + window.lana?.log(msg, { tags: tags.join(','), severity: 'e', sampleRate: 100 }); + decorateOverlay(el, `${msg}: ${tags.join(', ')}`, () => { window.location.reload(); }); +}; + +export const formTimeout = (el, condition, message, timeout = FAILURE_TIMEOUT) => { + setTimeout(() => { + if (condition()) { + logFailure(el, message); + } + }, timeout); +}; + const toggleSuccessSection = (formData) => { showSuccessSection(formData); hideSuccessSection(formData); }; +export const formSubmit = (formEl) => { + const el = formEl.closest('.marketo'); + const testRecord = window.mkto_isTestRecord?.(); + if (testRecord && testRecord !== 'not_test') return; + formTimeout(el, () => !el.classList.contains('success'), LANA_MESSAGE.SUBMIT_FAILED); +}; + export const formSuccess = (formEl, formData) => { const el = formEl.closest('.marketo'); const parentModal = formEl?.closest('.dialog-modal'); @@ -210,83 +305,33 @@ export const formSuccess = (formEl, formData) => { if (formData?.[SUCCESS_TYPE] !== 'section') return true; toggleSuccessSection(formData); - setPreference(SUCCESS_TYPE, 'message'); + setDataLayer(SUCCESS_TYPE, 'message'); return false; }; -export const handleIframeTimeout = (el) => { - const searchParams = new URLSearchParams(window.location.search); - const config = getConfig(); - const iframe = document.querySelector('iframe[src*="/index.php/form/XDFrame"]'); - const formEl = el.querySelector('form'); - let iframeTimeout = null; +const readyForm = (form, formData) => { + const formEl = form.getFormElem().get(0); + const el = formEl.closest('.marketo'); + const isDesktop = matchMedia('(min-width: 900px)'); + el.classList.remove('loading'); const handleIframeReady = (event) => { if (event.origin !== 'https://engage.adobe.com') return; const message = JSON.parse(event.data); if (!message.mktoReady) return; - setPreference('form.status', 'ready'); - if (iframeTimeout) clearTimeout(iframeTimeout); - const errorOverlay = el.querySelector('.marketo-overlay'); - if (formEl) formEl.inert = false; - if (errorOverlay) errorOverlay.remove(); + const hadFailed = el.dataset.mktoFailed === 'true'; + setDataLayer(FORM_XDFRAME, 'ready'); + const formVisible = isVisible(formEl); + if (hadFailed && formVisible) { + window.lana?.log(LANA_MESSAGE.RENDER_RECOVERED, { tags: 'marketo,render-recovered', severity: 'i', sampleRate: 100 }); + delete el.dataset.mktoFailed; + el.querySelector('.marketo-overlay')?.remove(); + formEl.inert = false; + } window.removeEventListener('message', handleIframeReady); }; - - const decorateOverlay = async () => { - const cookieSize = document.cookie.length > 4096 ? '> 4k' : '< 4k'; - window.lana?.log(`Marketo iframe timeout - Cookie Size ${cookieSize}`, { tags: 'marketo', severity: 'e' }); - setPreference('form.status', 'error'); - if (el.querySelector('.marketo-overlay')) return; - if (!config.marketo?.showError && searchParams.get('marketoOverlay') !== 'error') return; - - const marketoErrorText = await replaceKey('marketo-load-error', config); - const marketoTryAgainText = await replaceKey('marketo-try-again', config); - if (formEl) formEl.inert = true; - - const errorMessage = createTag('p', { class: 'error', id: 'marketo-error-message' }, marketoErrorText); - const retryButton = createTag('button', { class: 'retry-button' }, marketoTryAgainText); - const formError = createTag('div', { class: 'error-container' }, [errorMessage, retryButton]); - const errorOverlay = createTag( - 'div', - { - class: 'marketo-overlay', - role: 'alertdialog', - 'aria-modal': 'true', - 'aria-labelledby': 'marketo-error-message', - }, - formError, - ); - - retryButton.addEventListener('click', () => { - const iframeSrc = iframe.src; - errorOverlay.remove(); - if (formEl) formEl.inert = false; - iframe.src = ''; - iframe.src = iframeSrc; - clearTimeout(iframeTimeout); - iframeTimeout = setTimeout(decorateOverlay, config.marketo?.iframeTimeout ?? IFRAME_TIMEOUT); - }); - - el.appendChild(errorOverlay); - }; - - /* c8 ignore next 4 */ - if (searchParams.get('marketoOverlay') === 'error') { - decorateOverlay(); - return; - } - - iframeTimeout = setTimeout(decorateOverlay, config.marketo?.iframeTimeout ?? IFRAME_TIMEOUT); window.addEventListener('message', handleIframeReady); -}; - -const readyForm = (form, formData) => { - const formEl = form.getFormElem().get(0); - const el = formEl.closest('.marketo'); - const isDesktop = matchMedia('(min-width: 900px)'); - el.classList.remove('loading'); - handleIframeTimeout(el); + formTimeout(el, () => getDataLayer(FORM_XDFRAME) !== 'ready', LANA_MESSAGE.HANDSHAKE_FAILED); formEl.addEventListener('focus', ({ target }) => { /* c8 ignore next 9 */ @@ -301,21 +346,24 @@ const readyForm = (form, formData) => { window.scrollTo(0, offsetPosition); }, true); form.onValidate(() => formValidate(formEl)); + form.onSubmit(() => formSubmit(formEl)); form.onSuccess(() => formSuccess(formEl, formData)); }; export const loadMarketo = (el, formData) => { + setDataLayer(FORM_STATUS, 'loading'); const baseURL = formData[BASE_URL]; const munchkinID = formData[MUNCHKIN_ID]; const formID = formData[FORM_ID]; const { base } = getConfig(); - loadScript(`${base}/deps/forms2.min.js`) + return loadScript(`${base}/deps/forms2.min.js`) .then(() => { const { MktoForms2 } = window; if (!MktoForms2) throw new Error('Marketo forms not loaded'); - MktoForms2.loadForm(`//${baseURL}`, munchkinID, formID); + formTimeout(el, () => !isVisible(el.querySelector('form')), LANA_MESSAGE.RENDER_FAILED); + MktoForms2.loadForm(`//${baseURL}`, munchkinID, formID, () => { setDataLayer(FORM_STATUS, 'loaded'); }); MktoForms2.whenReady((form) => { readyForm(form, formData); }); /* c8 ignore next 3 */ @@ -326,7 +374,7 @@ export const loadMarketo = (el, formData) => { .catch(() => { /* c8 ignore next 2 */ el.style.display = 'none'; - window.lana?.log(`Error loading Marketo form for ${munchkinID}_${formID}`, { tags: 'marketo', severity: 'e' }); + logFailure(el, LANA_MESSAGE.MARKETO_FORMS_JS); }); }; @@ -367,6 +415,7 @@ function decorateForm(el, formData) { } export default async function init(el) { + setDataLayer(FORM_STATUS, 'init'); const children = Array.from(el.querySelectorAll(':scope > div')); const encodedConfigDiv = children.shift(); const link = encodedConfigDiv.querySelector('a'); @@ -423,7 +472,8 @@ export default async function init(el) { if (destinationUrl) formData[SUCCESS_CONTENT] = destinationUrl; } - setPreferences(formData); + formData[FORM_STATUS] = 'decorated'; + setDataLayerObj(formData); decorateForm(el, formData); loadLink(`https://${baseURL}`, { rel: 'dns-prefetch' }); diff --git a/libs/blocks/notification/notification.css b/libs/blocks/notification/notification.css index 0fd2de8fc9d..7b55eefe81a 100644 --- a/libs/blocks/notification/notification.css +++ b/libs/blocks/notification/notification.css @@ -4,6 +4,9 @@ --min-block-size-pill: 72px; --margin-inline-pill-desktop: 80px; --margin-inline-ribbon: 30px; + --margin-inline-ribbon-mobile: 15px; + --copy-wrap-margin-mobile: 12px; + --btn-padding-mobile: 4px 9px; --max-inline-size-image: 75px; --max-inline-size-icon: 231px; --inline-size-image: auto; @@ -530,6 +533,30 @@ } } +@media screen and (max-width: 480px) { + .notification.ribbon.space-between .foreground { + margin-inline: var(--margin-inline-ribbon-mobile); + } + + .notification.ribbon.space-between .foreground .icon-area { + margin-inline-end: var(--spacing-xxs); + } + + .notification.ribbon.space-between .foreground .copy-wrap { + margin-inline-end: var(--copy-wrap-margin-mobile); + } + + .notification.ribbon.space-between [class*="heading-"] strong { + font-size: var(--type-heading-s-size); + } + + .notification.ribbon.space-between .action-area a { + font-size: var(--type-body-xxs-size); + padding: var(--btn-padding-mobile); + min-width: 0; + } +} + @media screen and (max-width: 359px) { .notification.ribbon.inline .action-area { flex-wrap: wrap; diff --git a/libs/blocks/nps-csat-form/nps-csat-form.css b/libs/blocks/nps-csat-form/nps-csat-form.css index e056589aba0..d05125d6b7f 100644 --- a/libs/blocks/nps-csat-form/nps-csat-form.css +++ b/libs/blocks/nps-csat-form/nps-csat-form.css @@ -3,7 +3,6 @@ padding: 32px; height: 404px; max-width: 576px; - width: 100%; margin: 0 auto; } diff --git a/libs/blocks/region-nav/region-nav.js b/libs/blocks/region-nav/region-nav.js index f86b0211284..83e2204d0a7 100644 --- a/libs/blocks/region-nav/region-nav.js +++ b/libs/blocks/region-nav/region-nav.js @@ -12,6 +12,13 @@ import { norm } from '../../utils/market.js'; let config; +// Commerce geo-expansion markets: no adobe.com site; picker only sets the country +// cookie and routes to US site +const GEO_EXPANSION_MARKETS = new Set([ + 'mu', 'ke', 'gh', 'tz', 'am', 'az', 'ge', 'md', 'kz', 'kg', 'tj', + 'tm', 'uz', 'tn', 'om', 'ma', 'lb', 'jo', 'iq', 'dz', 'bh', +]); + const queriedPages = []; function handleEvent({ prefix, link, callback } = {}) { @@ -53,10 +60,15 @@ export function decorateLink(link, path, localeToLanguageMap = []) { ? getLanguage(languages, mergedLocales, pathname) : getLocale(mergedLocales, pathname); const prefix = currentLocaleObj.prefix.replace('/', ''); + const geoSegment = pathname.split('/')[1]?.toLowerCase(); + const geoMarket = GEO_EXPANSION_MARKETS.has(geoSegment) ? geoSegment : null; + let { href } = link; if (href.endsWith('/')) href = href.slice(0, -1); - if (languageMap && !locales[prefix] && (languages && !languages[prefix])) { + if (geoMarket) { + href = href.replace(`/${geoMarket}`, ''); + } else if (languageMap && !locales[prefix] && (languages && !languages[prefix])) { const valueInMap = languageMap[prefix]; href = href.replace(`/${prefix}`, valueInMap ? `/${valueInMap}` : ''); } @@ -87,7 +99,7 @@ export function decorateLink(link, path, localeToLanguageMap = []) { link.addEventListener('click', (e) => { setInternational(prefix === '' ? 'us' : prefix); - const resolved = norm(prefix) || 'us'; + const resolved = geoMarket || norm(prefix) || 'us'; const market = resolved === 'la' ? 'latam' : resolved; // TODO: remove this fallback after ACOM consumes market-selector if (market) setMarket(market); if (hrefAdapted) return; diff --git a/libs/c2/styles/styles.css b/libs/c2/styles/styles.css index ee44ffb6af1..a0fe24e75d1 100644 --- a/libs/c2/styles/styles.css +++ b/libs/c2/styles/styles.css @@ -480,6 +480,12 @@ --body-font-family: adobe-clean-thai, var(--body-font-family-base); --heading-font-family: var(--body-font-family); } + + &:lang(vi-VN), + &:lang(vi), + &:lang(he) { + --heading-font-family: var(--body-font-family); + } } .dark { diff --git a/libs/mep/ace1205/split-aside-grid/split-aside-grid.css b/libs/mep/ace1205/split-aside-grid/split-aside-grid.css index cfa632480f0..407e47a3507 100644 --- a/libs/mep/ace1205/split-aside-grid/split-aside-grid.css +++ b/libs/mep/ace1205/split-aside-grid/split-aside-grid.css @@ -482,3 +482,9 @@ } } } + +@media (prefers-reduced-motion: reduce) { + .split-aside-grid * { + transition: none !important; + } +} diff --git a/libs/mep/ace1205/split-aside-grid/split-aside-grid.js b/libs/mep/ace1205/split-aside-grid/split-aside-grid.js index e2b1100bc4f..70e42093887 100644 --- a/libs/mep/ace1205/split-aside-grid/split-aside-grid.js +++ b/libs/mep/ace1205/split-aside-grid/split-aside-grid.js @@ -231,6 +231,11 @@ function setupBlock(el) { } function commitNext(progress, isNavigation) { + if (reducedMotion()) { + applyRotation(1); + return; + } + flying = true; const oldFront = slideAt(0); const width = stackWidth(); @@ -302,11 +307,13 @@ function setupBlock(el) { } function commitPrev(progress, isNavigation) { - flying = true; const incoming = slideAt(-1); - applyRotation(-1); + if (reducedMotion()) return; + + flying = true; + if (isNavigation) { animatePrev(progress, incoming); setTimeout(() => medias.forEach((slide) => clearInline(slide))); @@ -368,6 +375,10 @@ function setupBlock(el) { drag.animation = true; const dir = getDirection(drag.dx); + drag.direction = dir; + + if (reducedMotion()) return; + if (drag.direction && drag.direction !== dir) { /* Direction reversed — reset every drag-touched property back to the original slots */ medias.forEach((m) => clearInline(m, ['transform'])); @@ -382,7 +393,6 @@ function setupBlock(el) { drag.progress = progress; animatePrev(progress, incoming); } - drag.direction = dir; } function onPointerUp(e) { @@ -395,11 +405,6 @@ function setupBlock(el) { const commit = Math.abs(dx) >= SWIPE_THRESHOLD; if (!commit) { snapBack(direction); return; } - if (reducedMotion()) { - applyRotation(direction === 'next' ? 1 : -1); - medias.forEach((slide) => clearInline(slide)); - return; - } if (direction === 'next') commitNext(progress); else commitPrev(progress); } diff --git a/libs/mep/dc1198/merch-card-combo/merch-card-combo.css b/libs/mep/dc1198/merch-card-combo/merch-card-combo.css index e2e0ae36670..c9a9a9c4875 100644 --- a/libs/mep/dc1198/merch-card-combo/merch-card-combo.css +++ b/libs/mep/dc1198/merch-card-combo/merch-card-combo.css @@ -92,6 +92,7 @@ merch-card.merch-card-combo[variant="mini-compare-chart"].bullet-list [slot="bod /* UI: quantity selector row — side padding to align with card content */ merch-card.merch-card-combo[variant="mini-compare-chart"].bullet-list [slot="offers"] { padding-inline: var(--consonant-merch-spacing-xs, 16px) !important; + padding-block: 0 !important; } /* UI: section wrapper that holds all cards side by side */ diff --git a/libs/mep/dc1198/merch-card-combo/merch-card-combo.js b/libs/mep/dc1198/merch-card-combo/merch-card-combo.js index ce8bb328198..2c8e52d7291 100644 --- a/libs/mep/dc1198/merch-card-combo/merch-card-combo.js +++ b/libs/mep/dc1198/merch-card-combo/merch-card-combo.js @@ -896,24 +896,24 @@ export default async function init(el) { const style = document.createElement('style'); style.textContent = ` footer { padding-top: var(--consonant-merch-spacing-xxs, 8px) !important; } - slot[name="offers"] { padding-top: 0 !important; } @media (min-width: 768px) { - slot[name="promo-text"] { min-height: 48px !important; } + slot[name="promo-text"] { min-height: 8px !important; } } .card-subtitle-injected { display: flex; - align-items: center; + align-items: flex-start; gap: 6px; font-size: var(--consonant-merch-card-body-xs-font-size, 14px); font-weight: var(--consonant-merch-card-detail-m-font-weight, 700); font-style: normal; line-height: 1.5; min-height: 42px; - padding: 0 var(--consonant-merch-spacing-s, 24px) var(--consonant-merch-spacing-xxs, 8px) 12px; + padding: 0 var(--consonant-merch-spacing-s, 24px) 0 var(--consonant-merch-spacing-xs, 16px); } :host(.has-sparkle) .card-subtitle-injected { color: #c12f84; } :host(:not(.has-sparkle)) .card-subtitle-injected { color: #4b4b4b; } .card-subtitle-injected em { font-style: normal; } + .card-subtitle-injected::before { padding-top: 3px; } `; const subtitleEl = merchCard.querySelector('.card-subtitle'); diff --git a/test/blocks/marketo/marketo.test.html b/test/blocks/marketo/marketo.test.html index 95a7727c728..2f78e4da21a 100644 --- a/test/blocks/marketo/marketo.test.html +++ b/test/blocks/marketo/marketo.test.html @@ -85,7 +85,7 @@