diff --git a/404.html b/404.html index 314a436a3..551d3cead 100644 --- a/404.html +++ b/404.html @@ -13,7 +13,8 @@ const redirectMap = new Map( [ - ['ae_ar', '/'], + ['ara' ,'/' ], + ['ae_ar', '/ara/'], ['ae_en', '/'], ['africa', '/'], ['ar', '/es/'], @@ -40,7 +41,7 @@ ['de-DE','/de/'], ['ec', '/es/'], ['ee', '/'], - ['eg_ar', '/'], + ['eg_ar', '/ara/'], ['eg_en', '/'], ['es-ES', '/es/'], ['fi-FI', '/fi/'], @@ -61,7 +62,7 @@ ['it-IT', '/it/'], ['ja-JP', '/jp/'], ['ko-KR', '/kr/'], - ['kw_ar', '/'], + ['kw_ar', '/ara/'], ['kw_en', '/'], ['la', '/es/'], ['lt', '/'], @@ -69,7 +70,7 @@ ['lu_de', '/de/'], ['lu_fr', '/fr/'], ['lv', '/'], - ['mena_ar', '/'], + ['mena_ar', '/ara/'], ['mena_en', '/'], ['mt', '/'], ['mx', '/es/'], @@ -86,11 +87,11 @@ ['pr', '/es/'], ['pt', '/br/'], ['pt-BR', '/br/'], - ['qa_ar', '/'], + ['qa_ar', '/ara/'], ['qa_en', '/'], ['ro', '/'], ['ru', '/'], - ['sa_ar', '/'], + ['sa_ar', '/ara/'], ['sa_en', '/'], ['sea', '/'], ['sg', '/'], diff --git a/express/code/blocks/ax-columns/ax-columns.css b/express/code/blocks/ax-columns/ax-columns.css index bca7533ee..e36547bc9 100644 --- a/express/code/blocks/ax-columns/ax-columns.css +++ b/express/code/blocks/ax-columns/ax-columns.css @@ -117,7 +117,7 @@ main .section .ax-columns.enterprise .column h2 { } .section:not(:first-child):not(.xxxl-spacing-static, .xxl-spacing-static, .xl-spacing-static, .xxxl-spacing, .xxl-spacing, .xl-spacing, -.l-spacing, .m-spacing, .s-spacing, .xs-spacing, .xxs-spacing) .ax-columns:first-child { +.l-spacing, .m-spacing, .s-spacing, .xs-spacing, .xxs-spacing) .ax-columns:first-child:not(.v2) { padding-top: 60px; } @@ -132,6 +132,36 @@ main .section .ax-columns.enterprise .column h2 { padding-top: 0; } +/* Authorable variant: equal top & bottom vertical spacing. + 32px (--spacing-500) on mobile/tablet, 64px (--spacing-800) on desktop (>=1200px). + Opt-in via the `v2` block class — self-contained so it does not rely on the + section spacing-variant override chain above. */ +.ax-columns.v2 { + --ax-columns-v2-spacing: var(--spacing-500); + max-width: 599px; + padding-top: var(--ax-columns-v2-spacing); + padding-bottom: var(--ax-columns-v2-spacing); +} + +.ax-columns.v2 > div { + gap : 16px; +} + +.ax-columns.v2 .column { + padding-left: 0; + padding-right: 0; +} + +.ax-columns.v2 .column, +.ax-columns.v2 .column.column-picture { + padding-left: 0; + padding-right: 0; +} + +.ax-columns.v2 .column-picture img { + border-radius: 16px; +} + .ax-columns.top.center, .ax-columns.fullsize.center, .ax-columns.light, @@ -141,7 +171,7 @@ main .section .ax-columns.enterprise .column h2 { padding-right: 0; } -.section:has(.ax-columns:not(.top.center, .fullsize, .light, .dark, .numbered, .centered, .color)):not(.xxxl-spacing-static, .xxl-spacing-static, .xl-spacing-static, .xxxl-spacing, .xxl-spacing, .xl-spacing, +.section:has(.ax-columns:not(.top.center, .fullsize, .light, .dark, .numbered, .centered, .color, .v2)):not(.xxxl-spacing-static, .xxl-spacing-static, .xl-spacing-static, .xxxl-spacing, .xxl-spacing, .xl-spacing, .l-spacing, .m-spacing, .s-spacing, .xs-spacing, .xxs-spacing)>.ax-columns:last-child, .section:has(.ax-columns:not(.top.center, .fullsize, .light, .dark, .numbered, .centered, .color)):not(.xxxl-spacing-static, .xxl-spacing-static, .xl-spacing-static, .xxxl-spacing, .xxl-spacing, .xl-spacing, .l-spacing, .m-spacing, .s-spacing, .xs-spacing, .xxs-spacing)>.content:last-child { @@ -390,9 +420,12 @@ main .section .ax-columns.enterprise .column h2 { display: flex; flex-direction: column; justify-content: center; +} +.section .ax-columns:not(.v2)>div { padding: var(--spacing-600) 0; } + .section .ax-columns.marquee>div { gap: var(--spacing-500); padding: 0; @@ -546,7 +579,7 @@ main .section .ax-columns.enterprise .column h2 { .ax-columns .column h4, .ax-columns .column h5, .ax-columns .column h6 { - text-align: left; + text-align: start; } .ax-columns.center .column, @@ -1198,7 +1231,7 @@ main .ax-columns.highlight.dark .columns-video:last-of-type { .section .ax-columns>div { flex-direction: row; - text-align: left; + text-align: start; align-items: center; } @@ -1465,6 +1498,11 @@ main .ax-columns.highlight.dark .columns-video:last-of-type { max-width: var(--block-lg-max-width); } + .ax-columns.v2 { + --ax-columns-v2-spacing: var(--spacing-800); + + } + .ax-columns.marquee .button-container.free-plan-container { display: flex; align-items: start; @@ -1950,3 +1988,16 @@ main .ax-columns .express-logo.icon.marquee-eyebrow-logo-large { margin-top: var(--spacing-500); } } + +main .ax-columns a.button.fill { + background-color: var(--Background-Primary-Default); + border-color: var(--Background-Primary-Default); + border-radius: 999px; + color: var(--color-white); +} + +main .ax-columns a.button.fill:any-link:hover, +main .ax-columns a.button.fill:any-link:active { + background-color: var(--color-info-accent-hover); + border-color: var(--color-info-accent-hover); +} diff --git a/express/code/blocks/banner-bg/banner-bg.css b/express/code/blocks/banner-bg/banner-bg.css index e2380406e..58f5498d3 100644 --- a/express/code/blocks/banner-bg/banner-bg.css +++ b/express/code/blocks/banner-bg/banner-bg.css @@ -152,7 +152,7 @@ font-size: var(--body-font-size-l); line-height: 1; padding: 13px var(--spacing-400); - border-radius: var(--spacing-400); + border-radius: var(--border-radius-max); background-color: var(--color-content-neutral); border: var(--border-width-2) solid var(--color-content-neutral); color: var(--color-white); diff --git a/express/code/blocks/discover-cards/discover-cards.css b/express/code/blocks/discover-cards/discover-cards.css index 44cbfb815..73d01bb51 100644 --- a/express/code/blocks/discover-cards/discover-cards.css +++ b/express/code/blocks/discover-cards/discover-cards.css @@ -135,6 +135,7 @@ main .section .discover-cards { justify-content: flex-end; gap: var(--spacing-100); align-items: center; + max-width: 1700px; } .discover-cards.flip .gallery-control { padding-top: var(--spacing-600); @@ -143,10 +144,10 @@ main .section .discover-cards { display: none; } .discover-cards .cards-container .card .text-content { - text-align: left; + text-align: start; } .discover-cards .cards-container .card .text-content .header { - text-align: left; + text-align: start; font-size: var(--ax-heading-l-size); font-weight: var(--heading-font-weight); margin-top: 17px; @@ -161,27 +162,28 @@ main .section .discover-cards { height: 147px; margin-top: var(--spacing-80); } -.discover-cards .card .button-container { - text-align: left; -} -.section .discover-cards .card .button-container a { + .discover-cards .card :is(.button-container, .action-area) { + text-align: start; + } + + .section .discover-cards .card :is(.button-container, .action-area) a { background-color: var(--color-gray-800-variant); color: var(--color-white); border: none; margin-top: var(--spacing-500); - margin-left: 0; + margin-inline-start: 0; } .discover-cards .cards-container.gallery { - scroll-padding-left: var(--spacing-350); + scroll-padding-inline-start: var(--spacing-350); } .discover-cards .cards-container .card:first-child { - margin-left: var(--spacing-350); + margin-inline-start: var(--spacing-350); } @media (min-width: 600px) { .discover-cards .center-title { - padding-bottom: var(--spacing-100); + padding-bottom: var(--spacing-500); font-size: var(--heading-font-size-l); } @@ -204,7 +206,7 @@ main .section .discover-cards { width: auto; } .discover-cards .cards-container .card .text-content { - text-align: left; + text-align: start; width: 351px; margin-left: auto; margin-right: auto; @@ -217,10 +219,10 @@ main .section .discover-cards { height: 198px; } .discover-cards .cards-container.gallery { - scroll-padding-left: 60px; + scroll-padding-inline-start: 60px; } .discover-cards .cards-container .card:first-child { - margin-left: 60px; + margin-inline-start: var(--spacing-300); } .discover-cards.flip .center-title { font-size: var(--heading-font-size-m); @@ -230,10 +232,8 @@ main .section .discover-cards { padding-bottom: var(--spacing-800); } .discover-cards.flip .cards-container.gallery { - padding-left: var(--spacing-500); - padding-right: var(--spacing-500); - scroll-padding-left: var(--spacing-500); - scroll-padding-right: var(--spacing-500); + padding-inline: var(--spacing-500); + scroll-padding-inline: var(--spacing-500); justify-content: center; } .section:has(.discover-cards.v2) { @@ -252,11 +252,14 @@ main .section .discover-cards { .discover-cards .cards-container { margin: auto; } + .discover-cards .gallery-control { + margin: 0 auto; + } .discover-cards .cards-container.gallery { - scroll-padding-left: 100px; + scroll-padding-inline-start: 100px; } .discover-cards .cards-container .card:first-child { - margin-left: 100px; + margin-inline-start: 100px; } } @@ -339,6 +342,10 @@ main .section .discover-cards { stroke: var(--color-gray-300); } +html[dir="rtl"] .discover-cards .gallery-control button { + transform: scaleX(-1); +} + .discover-cards.flip .flip-card-inner { position: relative; width: 100%; diff --git a/express/code/blocks/discover-cards/discover-cards.js b/express/code/blocks/discover-cards/discover-cards.js index 1f9066902..6172599a9 100644 --- a/express/code/blocks/discover-cards/discover-cards.js +++ b/express/code/blocks/discover-cards/discover-cards.js @@ -205,7 +205,7 @@ export default async function decorate(block) { } else { cardDivs.forEach((element) => { const img = element.querySelector('picture img'); - const textHeader = element.querySelector('h4'); + const textHeader = element.querySelector('h1, h2, h3, h4, h5, h6'); const textBody = element.querySelector('p'); if (textHeader && textBody) { textHeader.classList.add('header'); diff --git a/express/code/blocks/gen-ai-cards/gen-ai-cards.css b/express/code/blocks/gen-ai-cards/gen-ai-cards.css index 66ea4fcca..e1d5e6b13 100644 --- a/express/code/blocks/gen-ai-cards/gen-ai-cards.css +++ b/express/code/blocks/gen-ai-cards/gen-ai-cards.css @@ -81,7 +81,7 @@ .gen-ai-cards .gen-ai-cards-heading-section h2 { - text-align: left; + text-align: start; margin-bottom: var(--spacing-100); font-size: var(--heading-font-size-m); line-height: 130%; @@ -89,7 +89,7 @@ } .gen-ai-cards .gen-ai-cards-heading-section p { - text-align: left; + text-align: start; margin: 0; } @@ -129,6 +129,11 @@ scroll-padding-left: var(--spacing-350); } +html[dir="rtl"] .gen-ai-cards.homepage .carousel-container { + scroll-padding-left: unset; + scroll-padding-right: var(--spacing-350); +} + .gen-ai-cards .card { height: 415px; box-sizing: border-box; @@ -166,7 +171,7 @@ } .gen-ai-cards .card .text-wrapper { - text-align: left; + text-align: start; } .gen-ai-cards .card .text-wrapper p { @@ -317,11 +322,19 @@ main .gen-ai-cards.homepage .card .media-wrapper { } .gen-ai-cards.homepage .carousel-container .carousel-element.card:nth-child(2) { - margin-left: 20px; + margin-inline-start: 20px; +} + +.gen-ai-cards.homepage .carousel-container .carousel-element.card:nth-last-child(2) { + margin-inline-end: 20px; } .gen-ai-cards.homepage.v2 .carousel-container .carousel-element.card:nth-child(2) { - margin-left: 16px; + margin-inline-start: 16px; +} + +.gen-ai-cards.homepage.v2 .carousel-container .carousel-element.card:nth-last-child(2) { + margin-inline-end: 16px; } .gen-ai-cards .card .gen-ai-input-form .gen-ai-submit { @@ -398,10 +411,6 @@ main .gen-ai-cards.homepage .carousel-container .carousel-platform.right-fader:n mask-image: none; } -main .gen-ai-cards.homepage .carousel-container .carousel-platform.left-fader { - padding-right: var(--spacing-350); -} - main .gen-ai-cards.homepage .carousel-container .carousel-fader-left { align-items: end; top: 45px; @@ -417,6 +426,28 @@ main .gen-ai-cards.homepage .carousel-container .carousel-fader-right { pointer-events: none; } +/* RTL: the widget's float/absolute-position math for the arrow icon assumes + ltr, so force it regardless of page direction to keep the icon centered + in its circle, then mirror the pair to the left (reading end). carousel-fader-left + always scrolls physically left and carousel-fader-right always scrolls physically + right (their names track scroll direction, not screen side), so carousel-fader-left + must stay the leftmost of the pair to keep its "<" pointing further left, away + from carousel-fader-right, instead of back toward it. */ +html[dir="rtl"] .gen-ai-cards .carousel-fader-left, +html[dir="rtl"] .gen-ai-cards .carousel-fader-right { + direction: ltr; +} + +html[dir="rtl"] main .gen-ai-cards.homepage .carousel-container .carousel-fader-left { + right: auto; + left: 15px; +} + +html[dir="rtl"] main .gen-ai-cards.homepage .carousel-container .carousel-fader-right { + right: auto; + left: 42px; +} + .gen-ai-cards.homepage .gen-ai-cards-heading-section p { font-size: var(--ax-body-s-size); } @@ -432,7 +463,7 @@ main .gen-ai-cards.homepage .carousel-container .carousel-fader-right { .gen-ai-cards .gen-ai-cards-heading-section h2, .gen-ai-cards .gen-ai-cards-heading-section p { - text-align: left; + text-align: start; max-width: none; } @@ -449,7 +480,7 @@ main .gen-ai-cards.homepage .carousel-container .carousel-fader-right { .gen-ai-cards.homepage.spotlight .gen-ai-cards-heading-section h2, .gen-ai-cards.homepage.spotlight .gen-ai-cards-heading-section p { - text-align: left; + text-align: start; max-width: none; } @@ -498,6 +529,12 @@ main .gen-ai-cards.homepage .carousel-container .carousel-fader-right.arrow-hidd margin: 0px; } +/* RTL: the prev/next chevrons point a fixed physical direction, but scrolling + forward moves toward the left instead of the right, so flip them to match. */ +html[dir="rtl"] .gen-ai-cards .gallery-control svg { + transform: rotate(180deg); +} + .gen-ai-cards .card .beta-tag { color: var(--Content-Accent-default, #5258E4); font-size: var(--font-size-beta); @@ -526,7 +563,7 @@ main .gen-ai-cards.homepage .carousel-container .carousel-fader-right.arrow-hidd .gen-ai-cards .gen-ai-cards-heading-section h3, .gen-ai-cards .gen-ai-cards-heading-section p { - text-align: left; + text-align: start; } .gen-ai-cards .gen-ai-cards-heading-section .gen-ai-cards-link { @@ -556,16 +593,25 @@ main .gen-ai-cards.homepage .carousel-container .carousel-fader-right.arrow-hidd --heading-max-width: 600px; } - .gen-ai-cards.homepage .carousel-container .carousel-element.card:nth-child(2) { - margin-left: var(--spacing-350); + .gen-ai-cards.homepage .carousel-container .carousel-element.card:nth-child(2) { + margin-inline-start: var(--spacing-350); + } + + .gen-ai-cards.homepage .carousel-container .carousel-element.card:nth-last-child(2) { + margin-inline-end: var(--spacing-350); } .section:has(.gen-ai-cards.homepage.v2) { padding-top: var(--spacing-500); padding-bottom: var(--spacing-500); } + .gen-ai-cards.homepage.v2 .carousel-container .carousel-element.card:nth-child(2) { - margin-left: var(--spacing-500); + margin-inline-start: var(--spacing-500); + } + + .gen-ai-cards.homepage.v2 .carousel-container .carousel-element.card:nth-last-child(2) { + margin-inline-end: var(--spacing-500); } } @@ -606,7 +652,11 @@ main .gen-ai-cards.homepage .carousel-container .carousel-fader-right.arrow-hidd } .gen-ai-cards.homepage .carousel-container .carousel-element.card:nth-child(2) { - margin-left: var(--spacing-300); + margin-inline-start: var(--spacing-300); + } + + .gen-ai-cards.homepage .carousel-container .carousel-element.card:nth-last-child(2) { + margin-inline-end: var(--spacing-300); } .gen-ai-cards .carousel-container .carousel-fader-left, @@ -627,7 +677,7 @@ main .gen-ai-cards.homepage .carousel-container .carousel-fader-right.arrow-hidd } .gen-ai-cards.homepage .card .text-wrapper { - text-align: left; + text-align: start; } main .gen-ai-cards.homepage .gen-ai-cards-heading { diff --git a/express/code/blocks/grid-marquee/grid-marquee.css b/express/code/blocks/grid-marquee/grid-marquee.css index ced53e020..109d6b2b7 100644 --- a/express/code/blocks/grid-marquee/grid-marquee.css +++ b/express/code/blocks/grid-marquee/grid-marquee.css @@ -6,6 +6,16 @@ text-align: center; } +/* Anti-FOUC: hide only the raw, un-decorated video source authored under each + card face (mp4 link, or MEP-inlined if auto-video-decoration ran + first) so it never paints before toCard() removes it. Scoped to the element + itself, not gated on .foreground existing, so a JS failure can't leave the + whole block - including the authored LCP headline - hidden forever. */ +.grid-marquee > div > div:first-child > p > a[href*=".mp4"], +.grid-marquee > div > div:first-child > p > video { + visibility: hidden; +} + .grid-marquee .express-logo { @@ -45,6 +55,17 @@ main .section .grid-marquee h1 { border-color: var(--color-black); } +/* adobe-clean-arabic's ascent/descent metrics differ from the Latin body font, + so the line-height-based centering used elsewhere pushes Arabic CTA text + above the button's vertical midline. Center on the flex box instead, which + is font-metric agnostic. */ +:lang(ar) .grid-marquee .headline a.button { + display: inline-flex; + align-items: center; + justify-content: center; + line-height: normal; +} + .grid-marquee .background { width: 100% } @@ -59,6 +80,7 @@ main .section .grid-marquee h1 { .grid-marquee .background img { position: absolute; top: 50%; + left: 50%; object-fit: fill; width: 250%; transform: translate(-50%, -35%); @@ -94,7 +116,7 @@ main .section .grid-marquee h1 { border-radius: var(--spacing-100); border: 1px solid #fff; background: linear-gradient(170.96deg, rgba(255, 255, 255, 0.6) 18.95%, rgba(255, 255, 255, 0.2) 99.73%); - text-align: left; + text-align: start; display: flex; color: initial; } @@ -183,7 +205,8 @@ main .section .grid-marquee h1 { background: none; } -.grid-marquee .drawer video { +.grid-marquee .drawer video, +.grid-marquee .drawer .video-container img { border-radius: var(--spacing-100); width: 328px; z-index: 0; @@ -247,7 +270,7 @@ main .section .grid-marquee h1 { } .grid-marquee .drawer [role='tablist'] { - padding: 0 0 var(--spacing-100) var(--spacing-300); + padding: 0 var(--spacing-300) var(--spacing-100); } .grid-marquee .drawer [role='tab'] { @@ -277,7 +300,7 @@ main .section .grid-marquee h1 { font-size: 1rem; border: initial; padding: initial; - text-align: left; + text-align: start; margin: initial; } @@ -306,7 +329,8 @@ main .section .grid-marquee h1 { .grid-marquee .ratings img { height: 35px; - padding-left: var(--spacing-200); + width: auto; + padding-inline-start: var(--spacing-200); margin: 0; } @@ -340,8 +364,7 @@ main .section .grid-marquee.pricing-page h1 { } .grid-marquee .headline { max-width: 1200px; - padding-left: var(--spacing-500); - padding-right: var(--spacing-500); + padding-inline: var(--spacing-500); } .grid-marquee .headline .ctas { display: flex; @@ -355,6 +378,10 @@ main .section .grid-marquee.pricing-page h1 { margin: 0; } + .grid-marquee.cta-alignment .headline a.button { + padding-top: var(--spacing-100); + } + .grid-marquee .cards-container { gap: 8px; } @@ -375,7 +402,8 @@ main .section .grid-marquee.pricing-page h1 { .grid-marquee .drawer .title-row { width: 480px; } - .grid-marquee .drawer video { + .grid-marquee .drawer video, + .grid-marquee .drawer .video-container img { width: 480px; } .grid-marquee .drawer [role='tablist'] { @@ -424,6 +452,10 @@ main .section .grid-marquee.pricing-page h1 { border-radius: 50px; line-height: 23.4px; } + + .grid-marquee.cta-alignment .headline a.button { + padding-top: var(--spacing-300); + } .grid-marquee .cards-container { grid-template-columns: repeat(auto-fit, minmax(288px, 1fr)); width: 1200px; @@ -458,6 +490,7 @@ main .section .grid-marquee.pricing-page h1 { .grid-marquee .drawer .content { display: block; + overflow: visible; } .grid-marquee .drawer.hide { @@ -475,13 +508,14 @@ main .section .grid-marquee.pricing-page h1 { padding: 0 0 12px 0; } - .grid-marquee .drawer video { + .grid-marquee .drawer video, + .grid-marquee .drawer .video-container img { width: var(--d-img-w); height: initial; } .grid-marquee .drawer div[role='tablist'] { - padding: 0 0 16px 16px; + padding: 0 16px 16px; display: block; } diff --git a/express/code/blocks/grid-marquee/grid-marquee.js b/express/code/blocks/grid-marquee/grid-marquee.js index 08387b2ee..7cc5a2d01 100644 --- a/express/code/blocks/grid-marquee/grid-marquee.js +++ b/express/code/blocks/grid-marquee/grid-marquee.js @@ -81,18 +81,20 @@ async function decorateDrawer(videoSrc, poster, titleText, panels, panelsFrag, d } }); - const video = createTag('video', { - playsinline: '', - muted: '', - loop: '', - preload: 'metadata', - title: titleText, - poster, - }, ``); - const videoWrapper = createTag('button', { class: 'video-container' }, video); + const media = videoSrc + ? createTag('video', { + playsinline: '', + muted: '', + loop: '', + preload: 'metadata', + title: titleText, + poster, + }, ``) + : createTag('img', { src: poster, alt: titleText }); + const videoWrapper = createTag('button', { class: 'video-container' }, media); // link video to first anchor videoWrapper.addEventListener('click', () => anchors[0]?.click()); - videoWrapper.setAttribute('title', anchors[0]?.title); + if (anchors[0]?.title) videoWrapper.setAttribute('title', anchors[0].title); content.append(titleRow, videoWrapper, panelsFrag); drawer.append(content); @@ -183,8 +185,20 @@ function toCard(drawer) { const panelsFrag = new DocumentFragment(); panelsFrag.append(...panels); panels.forEach((panel) => panel.classList.add('panel')); - const videoAnchor = face.querySelector('a'); - videoAnchor?.remove(); + // The drawer video is authored in the face as an . But if + // Express's auto-video decoration ran first (e.g. inside an MEP replacePage + // fragment where #_dnb wasn't applied to the link), it arrives as an inlined + // instead. Capture the source from whichever form is present and + // remove BOTH so the face video can never duplicate the drawer video. + const faceAnchor = face.querySelector('a'); + const faceVideo = face.querySelector('video'); + const videoSrc = faceAnchor?.href + || faceVideo?.querySelector('source')?.src + || faceVideo?.currentSrc + || faceVideo?.getAttribute('src') + || faceVideo?.dataset?.videoSource; + faceAnchor?.remove(); + faceVideo?.remove(); // Use createTag like hero-marquee does (now available) const card = createTag('button', { @@ -195,7 +209,7 @@ function toCard(drawer) { }, [face, drawer]); face.classList.add('face'); - const lazyCB = () => decorateDrawer(videoAnchor.href, getPosterSrc(face.querySelector('img')), titleText, panels, panelsFrag, drawer); + const lazyCB = () => decorateDrawer(videoSrc, getPosterSrc(face.querySelector('img')), titleText, panels, panelsFrag, drawer); addCardInteractions(card, drawer, lazyCB); drawer.classList.add('drawer', 'hide'); drawer.id = `drawer-${titleText}`; @@ -212,6 +226,7 @@ async function makeRating( starsPlaceholder, playStoreLabelPlaceholder, appleStoreLabelPlaceholder, + config, ) { const ratings = ratingPlaceholder?.split(';') || []; const link = ratings[2]?.trim(); @@ -222,9 +237,11 @@ async function makeRating( const storeTypeIndex = [APPLE, GOOGLE].indexOf(store); const [score, cnt] = ratings[storeTypeIndex].split(',').map((str) => str.trim()); const ariaLabel = store === APPLE ? appleStoreLabelPlaceholder : playStoreLabelPlaceholder; + const { locale: { region } } = config; + // ToDo: Add support for all regions and remove Hardcoded Arabic region const storeLink = createTag('a', { href: link, - }, getIconElementDeprecated(`${store}-store`)); + }, getIconElementDeprecated(`${store}-store${region === 'ara' ? `-${region}` : ''}`)); storeLink.setAttribute('aria-label', ariaLabel); const { default: trackBranchParameters } = await import('../../scripts/branchlinks.js'); await trackBranchParameters([storeLink]); @@ -247,6 +264,7 @@ async function makeRatings( starsPlaceholder, playStoreLabelPlaceholder, appleStoreLabelPlaceholder, + config, ) { const ratings = createTag('div', { class: 'ratings' }); const userAgent = getMobileOperatingSystem(); @@ -257,6 +275,7 @@ async function makeRatings( starsPlaceholder, playStoreLabelPlaceholder, appleStoreLabelPlaceholder, + config, ); appleElement && ratings.append(appleElement); } @@ -267,6 +286,7 @@ async function makeRatings( starsPlaceholder, playStoreLabelPlaceholder, appleStoreLabelPlaceholder, + config, ); googleElement && ratings.append(googleElement); } @@ -366,14 +386,15 @@ export default async function init(el) { if (el.classList.contains('ratings')) { const { replaceKey } = await import(`${getLibs()}/features/placeholders.js`); const { getConfig } = await import(`${getLibs()}/utils/utils.js`); + const config = getConfig(); const [ratingPlaceholder, starsPlaceholder, playStoreLabelPlaceholder, appleStoreLabelPlaceholder] = await Promise.all([ - replaceKey('app-store-ratings', getConfig()), - replaceKey('app-store-stars', getConfig()), - replaceKey('app-store-ratings-play-store', getConfig()), - replaceKey('app-store-ratings-apple-store', getConfig()), + replaceKey('app-store-ratings', config), + replaceKey('app-store-stars', config), + replaceKey('app-store-ratings-play-store', config), + replaceKey('app-store-ratings-apple-store', config), ]); const ratingsElement = await makeRatings( @@ -381,6 +402,7 @@ export default async function init(el) { starsPlaceholder, playStoreLabelPlaceholder, appleStoreLabelPlaceholder, + config, ); foreground.append(ratingsElement); } @@ -433,14 +455,15 @@ export default async function init(el) { setTimeout(async () => { const { replaceKey } = await import(`${getLibs()}/features/placeholders.js`); const { getConfig } = await import(`${getLibs()}/utils/utils.js`); + const config = getConfig(); const [ratingPlaceholder, starsPlaceholder, playStoreLabelPlaceholder, appleStoreLabelPlaceholder] = await Promise.all([ - replaceKey('app-store-ratings', getConfig()), - replaceKey('app-store-stars', getConfig()), - replaceKey('app-store-ratings-play-store', getConfig()), - replaceKey('app-store-ratings-apple-store', getConfig()), + replaceKey('app-store-ratings', config), + replaceKey('app-store-stars', config), + replaceKey('app-store-ratings-play-store', config), + replaceKey('app-store-ratings-apple-store', config), ]); const ratingsElement = await makeRatings( @@ -448,6 +471,7 @@ export default async function init(el) { starsPlaceholder, playStoreLabelPlaceholder, appleStoreLabelPlaceholder, + config, ); ratingsPlaceholder.replaceWith(ratingsElement); }, 1000); diff --git a/express/code/blocks/list/list.css b/express/code/blocks/list/list.css index af4ad38a1..44f6df8e2 100644 --- a/express/code/blocks/list/list.css +++ b/express/code/blocks/list/list.css @@ -2,7 +2,7 @@ main .section:not([data-padding='none']):not(.xxxl-spacing-static, .xxl-spacing- .l-spacing, .m-spacing, .s-spacing, .xs-spacing, .xxs-spacing) .list-wrapper:first-child, main .section:not([data-padding='none']):not(.xxxl-spacing-static, .xxl-spacing-static, .xl-spacing-static, .xxxl-spacing, .xxl-spacing, .xl-spacing, .l-spacing, .m-spacing, .s-spacing, .xs-spacing, .xxs-spacing) .section-metadata:first-child + .list-wrapper { - padding-top: var(--spacing-785); + padding-top: var(--spacing-600); } main .list { @@ -15,7 +15,7 @@ main .list { } .section .banner + .list-wrapper { - padding-top: var(--spacing-785); + padding-top: var(--spacing-600); } main .list .item { @@ -75,7 +75,7 @@ main .list > div > div > :last-child { main .list { list-style: none; - text-align: left; + text-align: start; box-sizing: border-box; } @@ -113,3 +113,21 @@ main .list ol li::before { padding: 0px var(--spacing-400) var(--spacing-600) var(--spacing-400); } } + +main .list.edge-aligned { + max-width: unset; + margin: 0 auto; + display: block; +} + +@media (min-width: 768px) { + main .list.edge-aligned { + padding: 0px var(--spacing-500) var(--spacing-600) var(--spacing-500); + } +} + +@media (min-width: 1200px) { + main .list.edge-aligned { + max-width: 1200px; + } +} diff --git a/express/code/blocks/logo-row/logo-row.css b/express/code/blocks/logo-row/logo-row.css index 5b3569e91..3a791208e 100644 --- a/express/code/blocks/logo-row/logo-row.css +++ b/express/code/blocks/logo-row/logo-row.css @@ -30,8 +30,7 @@ main .logo-row .block-row { main .logo-row .text-column { margin: auto auto var(--spacing-300) auto; text-align: center; - padding-left: var(--spacing-300); - padding-right: var(--spacing-300); + padding-inline: var(--spacing-300); } main .logo-row .text-content { @@ -45,8 +44,7 @@ main .logo-row .image-column { flex-wrap: wrap; gap: var(--border-radius-10); justify-content: center; - padding-left: var(--spacing-300); - padding-right: var(--spacing-300); + padding-inline: var(--spacing-300); margin: auto; } @@ -83,11 +81,11 @@ main .logo-row .brand-image { main .logo-row .text-column { margin: auto auto var(--spacing-300) auto; - padding-right: initial; + padding-inline-end: initial; max-width: 300px; } main .logo-row .text-column > * { - text-align: left; + text-align: start; font-size: 20px; } @@ -101,9 +99,9 @@ main .logo-row .brand-image { @media (min-width: 1280px) { main .logo-row .text-column { margin: auto 0 16px auto; - padding-right: 40px; + padding-inline-end: 40px; max-width: 320px; - text-align: left; + text-align: start; } main .logo-row .block-row { @@ -111,7 +109,7 @@ main .logo-row .brand-image { } main .logo-row .text-column > * { - text-align: left; + text-align: start; } main .logo-row .image-column { max-width: unset; diff --git a/express/code/blocks/mobile-fork-button-dismissable/mobile-fork-button-dismissable.css b/express/code/blocks/mobile-fork-button-dismissable/mobile-fork-button-dismissable.css index 0456b62ab..4dd0ff7df 100644 --- a/express/code/blocks/mobile-fork-button-dismissable/mobile-fork-button-dismissable.css +++ b/express/code/blocks/mobile-fork-button-dismissable/mobile-fork-button-dismissable.css @@ -36,10 +36,15 @@ main .mobile-fork-button a.mweb-close { box-shadow: 0px 0px 12px 0px #00000029; } +html[dir="rtl"] main .mobile-fork-button a.mweb-close { + right: unset; + left: -8px; +} + main .mobile-gating-text { height: fit-content; margin: auto; - text-align: left; + text-align: start; flex-grow: 1; font-weight: 400; font-size: var(--mfb-font-size); diff --git a/express/code/blocks/quotes/quotes.css b/express/code/blocks/quotes/quotes.css index 27f71fa5c..5eebac630 100644 --- a/express/code/blocks/quotes/quotes.css +++ b/express/code/blocks/quotes/quotes.css @@ -137,7 +137,7 @@ margin: 0; font-size: 22px; font-weight: var(--heading-font-weight); - text-align: left; + text-align: start; line-height: initial; } @@ -396,12 +396,12 @@ main .quotes.singular .quote-details { margin: 0 16px; font-size: 22px; font-weight: 800; - text-align: left; + text-align: start; line-height: initial; } main .quotes.singular .desktop-container .quote-details { - margin-left: 30px; + margin-inline-start: 30px; } main .quotes.singular .author-description { @@ -416,10 +416,7 @@ main .quotes.singular .mobile-container .author-panel { font-weight: 600; display: flex; align-items: center; -} - -main .quotes.singular .mobile-container .author-panel .author-photo { - margin-right: 15px; + gap: 15px; } /* Ratings styles */ @@ -856,4 +853,4 @@ main .quotes.carousel .ratings button.selected .icon.icon-star { margin: 0; line-height: 20px; font-size: var(--body-font-size-m); -} \ No newline at end of file +} diff --git a/express/code/blocks/wayfinder/wayfinder.css b/express/code/blocks/wayfinder/wayfinder.css index 1477ffce9..a2e61bfd5 100644 --- a/express/code/blocks/wayfinder/wayfinder.css +++ b/express/code/blocks/wayfinder/wayfinder.css @@ -6,17 +6,15 @@ body main .section:has(.wayfinder) { .wayfinder { border: 2px solid var(--color-black); border-radius: 20px; - padding: var(--spacing-300) 0 var(--spacing-300) 0; - margin-left: var(--spacing-350); - margin-right: var(--spacing-350); - margin-top: 60px; + padding-block: var(--spacing-300); + margin-inline: var(--spacing-350); + margin-block-start: 60px; text-align: center; max-width: 1328px; } .wayfinder.template-page { - margin-top: 0; - margin-bottom: var(--spacing-300); + margin-block: 0 var(--spacing-300); } main .wayfinder h1, main .wayfinder h2, main .wayfinder h3, main .wayfinder h4, main .wayfinder h5, main .wayfinder h6 { @@ -25,7 +23,8 @@ main .wayfinder h1, main .wayfinder h2, main .wayfinder h3, main .wayfinder h4, font-style: normal; font-weight: 700; line-height: 130%; /* 20.8px */ - margin: var(--spacing-300) 0 var(--spacing-50) 0; + margin-block: var(--spacing-300) var(--spacing-50); + margin-inline: 0; } .wayfinder.narrow { @@ -57,7 +56,8 @@ main .wayfinder h1, main .wayfinder h2, main .wayfinder h3, main .wayfinder h4, .wayfinder .cta-row .button { height: fit-content; - margin: var(--spacing-100); + margin-block: var(--spacing-100); + margin-inline: var(--spacing-100); background-color: var(--color-black); border-color: var(--color-black); } @@ -71,7 +71,8 @@ main .wayfinder h1, main .wayfinder h2, main .wayfinder h3, main .wayfinder h4, } .wayfinder .text-row p { - margin: auto; + margin-block: auto; + margin-inline: auto; font-size: 16px; } @@ -83,8 +84,7 @@ main .wayfinder h1, main .wayfinder h2, main .wayfinder h3, main .wayfinder h4, background-color: var(--color-info-accent); color: var(--color-white); border: none; - padding-bottom: var(--spacing-100); - padding-top: var(--spacing-100); + padding-block: var(--spacing-100); } .wayfinder .cta-row em .button { @@ -99,12 +99,10 @@ main .wayfinder h1, main .wayfinder h2, main .wayfinder h3, main .wayfinder h4, background-color: none; border: none; text-underline-offset: 5px; - margin-left: var(--spacing-0); - margin-right: var(--spacing-0); + margin-inline: var(--spacing-0); text-decoration: underline; - padding-left: var(--spacing-100); - padding-right: var(--spacing-100); - margin-top: 11px; + padding-inline: var(--spacing-100); + margin-block-start: 11px; } .wayfinder .cta-row { @@ -119,7 +117,8 @@ main .wayfinder h1, main .wayfinder h2, main .wayfinder h3, main .wayfinder h4, .wayfinder .cta-row > div { display: flex; width: max-content; - margin: auto; + margin-block: auto; + margin-inline: auto; } @media (min-width: 768px) { @@ -130,7 +129,8 @@ main .wayfinder h1, main .wayfinder h2, main .wayfinder h3, main .wayfinder h4, font-style: normal; font-weight: 700; line-height: 130%; /* 20.8px */ - margin: var(--spacing-300) 0; + margin-block: var(--spacing-300); + margin-inline: 0; } .wayfinder { @@ -138,11 +138,13 @@ main .wayfinder h1, main .wayfinder h2, main .wayfinder h3, main .wayfinder h4, flex-direction: column; justify-content: center; align-items: center; - padding: 10px; + padding-block: 10px; + padding-inline: 10px; } .wayfinder .text-row { - margin: initial; + margin-block: initial; + margin-inline: initial; font-size: 16px; } @@ -155,7 +157,7 @@ main .wayfinder h1, main .wayfinder h2, main .wayfinder h3, main .wayfinder h4, display: flex; position: relative; top: var(--spacing-50); - margin-left: 10px; + margin-inline-start: 10px; overflow-x: scroll; } } diff --git a/express/code/icons/adobe-express-logo-rtl.svg b/express/code/icons/adobe-express-logo-rtl.svg new file mode 100644 index 000000000..de7ea581f --- /dev/null +++ b/express/code/icons/adobe-express-logo-rtl.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/express/code/icons/apple-store-ara.svg b/express/code/icons/apple-store-ara.svg new file mode 100755 index 000000000..be60cf52a --- /dev/null +++ b/express/code/icons/apple-store-ara.svg @@ -0,0 +1,34 @@ + + Download_on_the_App_Store_Badge_AR_RGB_blk_102417 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/express/code/icons/google-store-ara.svg b/express/code/icons/google-store-ara.svg new file mode 100644 index 000000000..a22686a8e --- /dev/null +++ b/express/code/icons/google-store-ara.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/express/code/scripts/branchlinks.js b/express/code/scripts/branchlinks.js index 0f68895ca..d14175e88 100644 --- a/express/code/scripts/branchlinks.js +++ b/express/code/scripts/branchlinks.js @@ -161,8 +161,11 @@ export async function getTrackingAppendedURL(url, options = {}) { setParams('cgen', cgen); if (placement) setParams('placement', placement); const { locale: { ietf, region } } = getConfig(); - setParams('locale', ietf); - setParams('contentRegion', region === 'uk' ? 'gb' : region); + setParams('locale', ietf === 'ar' ? 'ar-SA' : ietf); + let contentRegion = region; + if (region === 'uk') contentRegion = 'gb'; + if (region === 'ara') contentRegion = 'sa'; + setParams('contentRegion', contentRegion); if (sKwcId) { const sKwcIdParameters = sKwcId.split('!'); diff --git a/express/code/scripts/scripts.js b/express/code/scripts/scripts.js index 2b427ce1f..4a741a085 100644 --- a/express/code/scripts/scripts.js +++ b/express/code/scripts/scripts.js @@ -91,6 +91,7 @@ const CONFIG = { faasCloseModalAfterSubmit: 'on', locales: { '': { ietf: 'en-US', tk: 'jdq5hay.css' }, + ara: { ietf: 'ar', tk: 'cbp4pzm.css', dir: 'rtl' }, br: { ietf: 'pt-BR', tk: 'inq1xob.css' }, // eslint-disable-next-line max-len // TODO check that this ietf is ok to use everywhere. It's different in the old project zh-Hans-CN diff --git a/express/code/scripts/widgets/carousel.js b/express/code/scripts/widgets/carousel.js index f77e46233..a45b885f3 100644 --- a/express/code/scripts/widgets/carousel.js +++ b/express/code/scripts/widgets/carousel.js @@ -12,7 +12,28 @@ function isAtRightmostScroll(element) { return element.scrollLeft + element.clientWidth >= element.scrollWidth - 10; } -function initToggleTriggers(parent) { +// scrollLeft's origin (0) is always where the platform's first flex child sits: +// the physical left edge in ltr, but the physical right edge in rtl (the flex +// row auto-mirrors). So the "far" end of the scrollable range - the boundary +// isAtRightmostScroll checks for ltr - is the *most negative* scrollLeft in rtl. +export function isAtScrollEnd(element, isRTL) { + if (isRTL) return element.scrollLeft <= -(element.scrollWidth - element.clientWidth) + 10; + return isAtRightmostScroll(element); +} + +// carousel-left-trigger is always the platform's first flex child, so it sits +// at scrollLeft's origin (0) - the physical left edge in ltr, but the physical +// right edge in rtl. carousel-fader-left/-right always scroll the same +// physical direction regardless of dir, so which one the origin trigger +// governs has to flip under rtl to match where it actually sits. +export function getFaderRoles(isRTL) { + return { + origin: isRTL ? 'right' : 'left', + end: isRTL ? 'left' : 'right', + }; +} + +export function initToggleTriggers(parent) { if (!parent) return; const isInHiddenSection = () => { @@ -35,12 +56,21 @@ function initToggleTriggers(parent) { const rightTrigger = parent.querySelector('.carousel-right-trigger'); const platform = parent.querySelector('.carousel-platform'); - // If flex container has a gap, add negative margins to compensate + const isRTL = window.getComputedStyle(platform).direction === 'rtl'; + const { origin, end } = getFaderRoles(isRTL); + const originControl = origin === 'left' ? leftControl : rightControl; + const endControl = end === 'left' ? leftControl : rightControl; + const originFaderClass = `${origin}-fader`; + const endFaderClass = `${end}-fader`; + + // If flex container has a gap, add negative margins to compensate. Logical + // properties so this doesn't need its own isRTL branch - the browser maps + // inline-end/-start to the correct physical side for either direction. const gap = window.getComputedStyle(platform, null).getPropertyValue('gap'); if (gap !== 'normal') { const gapInt = parseInt(gap.replace('px', ''), 10); - leftTrigger.style.marginRight = `-${gapInt + 1}px`; - rightTrigger.style.marginLeft = `-${gapInt + 1}px`; + leftTrigger.style.marginInlineEnd = `-${gapInt + 1}px`; + rightTrigger.style.marginInlineStart = `-${gapInt + 1}px`; } // intersection observer to toggle right arrow and gradient @@ -50,38 +80,38 @@ function initToggleTriggers(parent) { entries.forEach((entry) => { if (entry.target === leftTrigger) { if (entry.isIntersecting) { - leftControl.classList.add('arrow-hidden'); - platform.classList.remove('left-fader'); + originControl.classList.add('arrow-hidden'); + platform.classList.remove(originFaderClass); } else { - leftControl.classList.remove('arrow-hidden'); - platform.classList.add('left-fader'); + originControl.classList.remove('arrow-hidden'); + platform.classList.add(originFaderClass); } } if (entry.target === rightTrigger) { - if (entry.isIntersecting || isAtRightmostScroll(platform)) { - rightControl.classList.add('arrow-hidden'); - platform.classList.remove('right-fader'); + if (entry.isIntersecting || isAtScrollEnd(platform, isRTL)) { + endControl.classList.add('arrow-hidden'); + platform.classList.remove(endFaderClass); } else { - rightControl.classList.remove('arrow-hidden'); - platform.classList.add('right-fader'); + endControl.classList.remove('arrow-hidden'); + platform.classList.add(endFaderClass); } } }); }; // Also handle scroll events to ensure proper state updates - const updateRightArrowState = () => { - if (isAtRightmostScroll(platform)) { - rightControl.classList.add('arrow-hidden'); - platform.classList.remove('right-fader'); + const updateEndArrowState = () => { + if (isAtScrollEnd(platform, isRTL)) { + endControl.classList.add('arrow-hidden'); + platform.classList.remove(endFaderClass); } else { - rightControl.classList.remove('arrow-hidden'); - platform.classList.add('right-fader'); + endControl.classList.remove('arrow-hidden'); + platform.classList.add(endFaderClass); } }; - platform.addEventListener('scroll', throttle(updateRightArrowState, 100)); + platform.addEventListener('scroll', throttle(updateEndArrowState, 100)); const options = { threshold: 0, root: parent }; const slideObserver = new IntersectionObserver(onSlideIntersect, options); diff --git a/express/code/styles/styles.css b/express/code/styles/styles.css index fb6b597d7..b971c044b 100644 --- a/express/code/styles/styles.css +++ b/express/code/styles/styles.css @@ -27,6 +27,28 @@ src: local('Trebuchet MS'), local('TrebuchetMS'), local('Arial'), local('Helvetica'); } +/* Arabic fallback chain: Trebuchet MS has no Arabic glyph coverage, so use Arial first */ +@font-face { + font-family: 'adobe-clean-fallback-ar'; + font-display: swap; + size-adjust: 92.1%; + ascent-override: 88.67%; + descent-override: 51.75%; + line-gap-override: 0%; + src: local('Arial'), local('Helvetica'); +} + +@font-face { + font-family: 'adobe-clean-fallback-ar'; + font-display: swap; + size-adjust: 103.5%; + font-weight: 900; + ascent-override: 88.67%; + descent-override: 51.75%; + line-gap-override: 0%; + src: local('Arial'), local('Helvetica'); +} + :root { --color-white: #fff; @@ -137,7 +159,7 @@ --heading-font-size-ml : 2rem; /* 32px */ - + --block-sm-max-width: 375px; --block-md-max-width: 830px; --block-lg-max-width: 1024px; @@ -167,7 +189,7 @@ --SN-Radius-corner-radius-full: 2000px; --S2-Buttons-Gen-AI: linear-gradient(96deg, #D73220 0%, #D92361 33%, #7155FA 100%); - + --S2-Buttons-Premium: linear-gradient(95.85deg, #b539c8, #7155fa 66%, #3b63fb); --S2-Buttons-Premium-Hover: linear-gradient(96deg, #9C28AF 0%, #6338EE 66%, #274DEA 100%); --S2-Buttons-Premium-Down: linear-gradient(96deg, #871B9A 0%, #5424DB 66%, #1D3ECF 100%); @@ -203,7 +225,7 @@ /* ===== EXPRESS TYPOGRAPHY SYSTEM (ax- namespace) ===== */ /* Based on Figma design system values */ - + /* Express Heading Typography Variables */ --ax-heading-xxxl-size: 44px; --ax-heading-xxxl-lh: 55px; @@ -389,7 +411,7 @@ --Grid-container-padding-side: var(--spacing-300); --Grid-grid-gutter: var(--spacing-100); --section-padding-xl: var(--spacing-600); - + } @media (min-width: 600px) { @@ -408,7 +430,7 @@ :root { --ax-grid-margin: var(--spacing-600); --ax-grid-gutter: var(--Grid-grid-gutter); - + /* Express Typography Desktop Overrides */ --ax-heading-xxxl-size: 80px; --ax-heading-xxxl-lh: 88px; @@ -491,6 +513,10 @@ body[data-device="mobile"] { 'Trebuchet MS', sans-serif; } +:root:lang(ar) { + --body-font-family: adobe-clean-arabic, 'Adobe Clean', adobe-clean, adobe-arabic, myriad-arabic, 'adobe-clean-fallback-ar', 'Arial', sans-serif +} + :is(p):lang(ja) { line-break: normal; } @@ -858,7 +884,7 @@ main a.button.gradient.disabled:any-link { background: var(--color-gray-200); pointer-events: none; } - + main a.button>svg { float: left; display: inline; @@ -908,7 +934,7 @@ main h5 { main p, main ul, main ol { font-size: var(--body-font-size-xl); - line-height: var(--body-line-height); + line-height: var(--body-line-height); } main a:any-link { @@ -1017,7 +1043,7 @@ main .section.secondary { main .section.panel { - margin: 0 var(--spacing-600) var(--spacing-900) var(--spacing-600); + margin: 0 var(--spacing-600) var(--spacing-900) var(--spacing-600); padding: var(--spacing-600) var(--spacing-500); border-radius: 16px; border: 2px solid var(--color-white); @@ -1058,7 +1084,7 @@ main .section.max-width-900 { max-width: 900px; margin-inline: auto; } - + } /* Align link-list with template container when in modular-search */ diff --git a/head.html b/head.html index 0aecb14e2..2ec9351ea 100644 --- a/head.html +++ b/head.html @@ -9,7 +9,7 @@ const flagMeta = document.querySelector('meta[name="head-loaded"]'); const headAlreadyLoaded = flagMeta.content === 'true'; - const LOCALES = ['ae_ar', 'ae_en', 'africa', 'ar', 'at', 'au', 'be_en', 'be_fr', 'be_nl', 'bg', 'br', 'ca_fr', 'ca', 'ch_de', 'ch_fr', 'ch_it', 'cl', 'cn', 'co', 'cr', 'cy_en', 'cz', 'de', 'dk', 'ec', 'ee', 'eg_ar', 'eg_en', 'el', 'es', 'fi', 'fr', 'gr_el', 'gr_en', 'gt', 'hk_en', 'hk_zh', 'hu', 'id_en', 'id_id', 'ie', 'il_en', 'il_he', 'in_hi', 'in', 'it', 'jp', 'kr', 'kw_ar', 'kw_en', 'la', 'lt', 'lu_de', 'lu_en', 'lu_fr', 'lv', 'mena_ar', 'mena_en', 'mt', 'mx', 'my_en', 'my_ms', 'ng', 'nl', 'no', 'nz', 'pe', 'ph_en', 'ph_fil', 'pl', 'pr', 'pt', 'qa_ar', 'qa_en', 'ro', 'ru', 'sa_ar', 'sa_en', 'se', 'sg', 'si', 'sk', 'th_en', 'th_th', 'tr', 'tw', 'ua', 'uk', 'vn_en', 'vn_vi', 'za']; + const LOCALES = ['ae_ar', 'ae_en', 'africa', 'ar', 'ara', 'at', 'au', 'be_en', 'be_fr', 'be_nl', 'bg', 'br', 'ca_fr', 'ca', 'ch_de', 'ch_fr', 'ch_it', 'cl', 'cn', 'co', 'cr', 'cy_en', 'cz', 'de', 'dk', 'ec', 'ee', 'eg_ar', 'eg_en', 'el', 'es', 'fi', 'fr', 'gr_el', 'gr_en', 'gt', 'hk_en', 'hk_zh', 'hu', 'id_en', 'id_id', 'ie', 'il_en', 'il_he', 'in_hi', 'in', 'it', 'jp', 'kr', 'kw_ar', 'kw_en', 'la', 'lt', 'lu_de', 'lu_en', 'lu_fr', 'lv', 'mena_ar', 'mena_en', 'mt', 'mx', 'my_en', 'my_ms', 'ng', 'nl', 'no', 'nz', 'pe', 'ph_en', 'ph_fil', 'pl', 'pr', 'pt', 'qa_ar', 'qa_en', 'ro', 'ru', 'sa_ar', 'sa_en', 'se', 'sg', 'si', 'sk', 'th_en', 'th_th', 'tr', 'tw', 'ua', 'uk', 'vn_en', 'vn_vi', 'za']; (() => { function buildLink(language, url) { diff --git a/test/blocks/grid-marquee/grid-marquee.test.js b/test/blocks/grid-marquee/grid-marquee.test.js index d9c7b86fd..25cf7f36b 100644 --- a/test/blocks/grid-marquee/grid-marquee.test.js +++ b/test/blocks/grid-marquee/grid-marquee.test.js @@ -15,6 +15,7 @@ const { default: decorateHero } = imports[2]; const oldAuthoring = await readFile({ path: './mocks/old-authoring.html' }); const newAuthoring = await readFile({ path: './mocks/new-authoring.html' }); const newAuthoringPicture = await readFile({ path: './mocks/new-authoring-picture.html' }); +const newAuthoringStaticImage = await readFile({ path: './mocks/new-authoring-static-image.html' }); describe('Grid Marquee - Legacy vs New Authoring', () => { let originalRAF; @@ -149,4 +150,32 @@ describe('Grid Marquee - Legacy vs New Authoring', () => { expect(video.poster).to.include('/express/code/img/favicons/favicon-32.png'); expect(video.poster).to.not.include('format='); }); + + it('Card authored with a static image (no video anchor) opens the drawer with an image instead of a video', async () => { + document.body.innerHTML = newAuthoringStaticImage; + const gm = document.querySelector('.grid-marquee'); + await decorateGrid(gm); + + const card = gm.querySelector('.card'); + expect(card).to.exist; + card.dispatchEvent(new MouseEvent('mouseenter')); + + const waitForVideoContainer = async (root, timeoutMs = 2000) => { + const start = performance.now(); + let container = root.querySelector('.drawer .video-container'); + while (!container?.firstElementChild && performance.now() - start < timeoutMs) { + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { setTimeout(resolve, 20); }); + container = root.querySelector('.drawer .video-container'); + } + if (!container?.firstElementChild) throw new Error('Timeout waiting for drawer video-container'); + return container; + }; + + const videoContainer = await waitForVideoContainer(gm); + expect(videoContainer.querySelector('video')).to.not.exist; + const img = videoContainer.querySelector('img'); + expect(img).to.exist; + expect(img.src).to.not.be.empty; + }); }); diff --git a/test/blocks/grid-marquee/mocks/new-authoring-static-image.html b/test/blocks/grid-marquee/mocks/new-authoring-static-image.html new file mode 100644 index 000000000..bbb591bd5 --- /dev/null +++ b/test/blocks/grid-marquee/mocks/new-authoring-static-image.html @@ -0,0 +1,43 @@ + + + + + + The quick and easy create-anything app. + Make stunning social posts, images, videos, flyers, and more. + + Start 30-day free trial + Get Adobe Express Free + + + + + + + + + + + + + + + + + + + + + + + + Create anything, quickly and easily + + + Start from scratch + Start from your media + + + + + diff --git a/test/scripts/widgets/carousel.test.js b/test/scripts/widgets/carousel.test.js new file mode 100644 index 000000000..1982078e3 --- /dev/null +++ b/test/scripts/widgets/carousel.test.js @@ -0,0 +1,70 @@ +import { expect } from '@esm-bundle/chai'; +import { isAtScrollEnd, getFaderRoles } from '../../../express/code/scripts/widgets/carousel.js'; + +// initToggleTriggers itself is driven by IntersectionObserver callbacks, +// which are unreliable to assert on directly in a full parallel test run +// (they get throttled/never fire when the tab isn't focused - the same class +// of flakiness documented in how-to-cards/gallery.test.js). The two pieces of +// logic that actually contained the RTL bug - which fader-control/gradient a +// trigger drives, and where the far end of the scroll range is - are pure +// functions carousel.js exports, so test those directly instead of racing +// the observer through a real DOM. + +describe('carousel widget - getFaderRoles', () => { + it('ltr: the left trigger (scroll origin) drives the left fader, the right trigger drives the right fader', () => { + expect(getFaderRoles(false)).to.deep.equal({ origin: 'left', end: 'right' }); + }); + + it('rtl: the left trigger (still the scroll origin, now rendered on the physical right) drives the right fader', () => { + expect(getFaderRoles(true)).to.deep.equal({ origin: 'right', end: 'left' }); + }); +}); + +describe('carousel widget - isAtScrollEnd', () => { + // A platform with 1000px of scrollable content in a 400px viewport, so the + // far end of the range is +/-600 depending on direction. + const clientWidth = 400; + const scrollWidth = 1000; + + describe('ltr', () => { + it('is false at the scroll origin', () => { + expect(isAtScrollEnd({ scrollLeft: 0, scrollWidth, clientWidth }, false)).to.be.false; + }); + + it('is false part-way through the scroll range', () => { + expect(isAtScrollEnd({ scrollLeft: 300, scrollWidth, clientWidth }, false)).to.be.false; + }); + + it('is true once scrolled to the far (rightmost) end', () => { + expect(isAtScrollEnd({ scrollLeft: 600, scrollWidth, clientWidth }, false)).to.be.true; + }); + + it('is true within the 10px tolerance of the far end', () => { + expect(isAtScrollEnd({ scrollLeft: 595, scrollWidth, clientWidth }, false)).to.be.true; + }); + }); + + describe('rtl', () => { + it('is false at the scroll origin (0, the physical right/start)', () => { + expect(isAtScrollEnd({ scrollLeft: 0, scrollWidth, clientWidth }, true)).to.be.false; + }); + + it('is false part-way through the scroll range', () => { + expect(isAtScrollEnd({ scrollLeft: -300, scrollWidth, clientWidth }, true)).to.be.false; + }); + + it('is true once scrolled to the far (leftmost) end - the most negative scrollLeft', () => { + expect(isAtScrollEnd({ scrollLeft: -600, scrollWidth, clientWidth }, true)).to.be.true; + }); + + it('is true within the 10px tolerance of the far end', () => { + expect(isAtScrollEnd({ scrollLeft: -595, scrollWidth, clientWidth }, true)).to.be.true; + }); + + it('does not flag the far ltr-style boundary as the end', () => { + // Regression guard: before the rtl branch existed, this used the ltr + // formula unconditionally, which this positive scrollLeft would satisfy. + expect(isAtScrollEnd({ scrollLeft: 600, scrollWidth, clientWidth }, true)).to.be.false; + }); + }); +});
Make stunning social posts, images, videos, flyers, and more.
+ Start 30-day free trial + Get Adobe Express Free +
+ + + +
Create anything, quickly and easily
Start from scratch
Start from your media