Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2dbeea8
feat: fixed min-height for different variants
saurabhsircar11 Jun 1, 2026
4de5e25
feat: handled height for tabs
saurabhsircar11 Jun 1, 2026
6e03b03
feat: fixed height for tabs panel
saurabhsircar11 Jun 1, 2026
4c39c35
feat: variant wise tokens
saurabhsircar11 Jun 2, 2026
071cbbf
fix: fix for legacy modal
saurabhsircar11 Jun 2, 2026
2f20c0d
feat: fixed for page in layouts also
saurabhsircar11 Jun 2, 2026
fc9f7b8
fix: code review fix
saurabhsircar11 Jun 2, 2026
d6f116e
feat: shorten comments
saurabhsircar11 Jun 2, 2026
1e45505
Merge branch 'stage' of github.com:adobecom/da-express-milo into MWPW…
saurabhsircar11 Jun 2, 2026
3285eaa
fix: removed locked height and switched to min-height for modals
saurabhsircar11 Jun 3, 2026
9381578
fix(susi-light): reserve simplified modal wrapper at 400px
saurabhsircar11 Jun 3, 2026
356ed26
fix(susi-light): bump modal wrapper reserves from peak measurements
saurabhsircar11 Jun 3, 2026
54e463e
fix: added scrolls if the content is to big
saurabhsircar11 Jun 3, 2026
7f24fa0
Merge branch 'stage' of github.com:adobecom/da-express-milo into MWPW…
saurabhsircar11 Jun 3, 2026
9693396
Merge branch 'stage' of github.com:adobecom/da-express-milo into MWPW…
saurabhsircar11 Jun 9, 2026
b368e9f
fix: code review fix
saurabhsircar11 Jun 9, 2026
3686190
fix: switched to min-height for tabs
saurabhsircar11 Jun 9, 2026
4ef9a82
fix(nala): assert tab panel min-height reserve instead of switch delta
saurabhsircar11 Jun 9, 2026
645c493
Merge branch 'stage' of github.com:adobecom/da-express-milo into MWPW…
saurabhsircar11 Jun 22, 2026
38e43e7
Merge branch 'stage' of github.com:adobecom/da-express-milo into MWPW…
saurabhsircar11 Jun 24, 2026
9c3d446
MWPW-178300: dedupe susi-light CLS height tokens to single source of …
saurabhsircar11 Jun 24, 2026
348f532
Merge branch 'stage' of github.com:adobecom/da-express-milo into MWPW…
saurabhsircar11 Jul 22, 2026
b819e4c
MWPW-178300: lower legacy-edu-express reserve to 520px per intl measu…
saurabhsircar11 Jul 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 50 additions & 9 deletions express/code/blocks/susi-light/susi-light.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
/* CLS fallback floors. Per-profile heights live in the JS (single source of truth). */
:root {
--susi-modal-width: 370px;
--susi-tabs-width: 400px;
--susi-min-height: 462px;
--susi-tabs-panel-height: 521px;
--susi-tabs-wrapper-standard: 458px;
--susi-tabs-wrapper-edu-express: 367px;
--susi-wrapper-standard: 484px;
--susi-wrapper-edu-express: 400px;
--susi-wrapper-simplified: 400px;
--susi-standard-padding: 24px;
}

Expand Down Expand Up @@ -115,30 +121,62 @@
background-color: var(--color-white);
}

.susi-light.tabs [role='tabpanel'] {
.susi-light.tabs .susi-tab-panels {
position: relative;
width: var(--susi-tabs-width);
max-width: 95vw;
min-height: var(--susi-tabs-panel-height);
flex-shrink: 0;
box-sizing: border-box;
}

.susi-light.tabs [role='tabpanel'] {
display: flex;
flex-direction: column;
align-items: stretch;
width: 100%;
min-height: var(--susi-tabs-panel-height);
box-sizing: border-box;
}

.susi-light.tabs [role='tabpanel'].hide {
display: none;
}

/* CLS optiimzations */
/* TODO: makes this dynamic based on # of social logins */
.susi-light.tabs [role='tabpanel'].standard .susi-wrapper {
min-height: 457.5px;
flex: 0 0 auto;
min-height: var(--susi-tabs-wrapper-standard);
}

.susi-light.tabs [role='tabpanel'].edu-express .susi-wrapper {
min-height: 366.5px;
flex: 0 0 auto;
min-height: var(--susi-tabs-wrapper-edu-express);
}

.susi-light.tabs [role='tabpanel'] .footer {
flex-shrink: 0;
margin-top: auto;
}

/* CLS: height set by applyModalWrapperReserve */
.susi-light:not(.tabs):has(.susi-layout) .susi-wrapper {
min-height: var(--susi-modal-wrapper-height, var(--susi-wrapper-standard));
}

.susi-light.student .susi-wrapper {
min-height: 421.5px;
width: var(--susi-modal-width);
}

.susi-light.simplified .susi-wrapper {
min-height: var(--susi-modal-wrapper-height, var(--susi-wrapper-simplified));
}

.susi-light:not(:has(.susi-layout)) > susi-sentry-light {
display: block;
width: 100%;
min-height: var(--susi-modal-wrapper-height, var(--susi-wrapper-edu-express));
}

.susi-light.color .susi-wrapper {
width: 100%;
}
Expand Down Expand Up @@ -176,12 +214,15 @@
/* modal styles */
.dialog-modal:has(.susi-light) {
width: var(--susi-modal-width);
min-height: var(--susi-min-height);
}

.dialog-modal:has(.susi-light.tabs) {
width: var(--susi-tabs-width);
min-height: var(--susi-min-height);
}

.dialog-modal:has(.susi-light:not(.tabs) .susi-layout) .susi-wrapper {
flex: 0 0 auto;
min-height: var(--susi-modal-wrapper-height, var(--susi-wrapper-standard));
}

.dialog-modal .susi-light .express-logo {
Expand Down
117 changes: 112 additions & 5 deletions express/code/blocks/susi-light/susi-light.js
Original file line number Diff line number Diff line change
Expand Up @@ -349,11 +349,116 @@ async function buildStudent(el, locale, imsClientId, noRedirect) {

// each tab wraps susi component with custom logo + footer
let tabsId = 0;

const TABS_PANEL_HEIGHTS = {
standard: 508,
'edu-express': 521,
};
const TABS_PANEL_BUFFER = 0;
const TABS_PANEL_FALLBACK = 521;

// Per-variant tab wrapper min-heights are owned by CSS
// (--susi-tabs-wrapper-* in susi-light.css), applied statically per tabpanel.

export function resolveTabsPanelMinHeight(variants) {
const heights = variants
.filter(Boolean)
.map((v) => TABS_PANEL_HEIGHTS[v] ?? TABS_PANEL_HEIGHTS.standard);
if (!heights.length) return TABS_PANEL_FALLBACK;
return Math.ceil(Math.max(...heights) + TABS_PANEL_BUFFER);
}

/** CLS: reserve tab panel slot from authoring row 2 variants. */
export function applyTabsReserveFromAuthoring(el, variants) {
el.style.setProperty('--susi-tabs-panel-height', `${resolveTabsPanelMinHeight(variants)}px`);
}

const MODAL_FLAVORS = ['b2b', 'edu', 'student'];

function readClientIdFromAuthoring(el, imsClientId) {
const cells = el.querySelectorAll(':scope > div > div');
return cells[1]?.textContent?.trim() || imsClientId || 'AdobeExpressWeb';
}

function resolveFlavorModalWrapperProfile(el, flavor, clientId) {
if (el.classList.contains('email-only')) return `${flavor}-email-only`;
if (flavor === 'b2b' && el.classList.contains('email-first')) return 'b2b-email-first';
if (clientId === 'AdobeExpressWeb_HED' || clientId?.endsWith('_HED')) return `${flavor}-hed`;
if (clientId === 'AdobeExpressWeb_Business' || clientId?.endsWith('_Business')) {
return `${flavor}-business`;
}
return `${flavor}-default`;
}

/** Legacy: no flavor class, bare susi-sentry-light. */
function resolveLegacyModalWrapperProfile(clientId) {
if (clientId === 'AdobeExpressWeb_HED' || clientId?.endsWith('_HED')) return 'legacy-edu-hed';
if (clientId === 'AdobeExpressWeb_Business' || clientId?.endsWith('_Business')) {
return 'edu-business';
}
return 'legacy-edu-express';
}

/** Profile id matches fragment names, e.g. b2b-hed, edu-business. */
export function resolveModalWrapperProfile(el, clientId) {
if (el.classList.contains('tabs')) return null;
if (el.classList.contains('simplified')) return 'simplified';
const flavor = MODAL_FLAVORS.find((c) => el.classList.contains(c));
if (flavor) return resolveFlavorModalWrapperProfile(el, flavor, clientId);
return resolveLegacyModalWrapperProfile(clientId);
}

/** Measured px per profile — mirrors :root --susi-wrapper-* tokens. */
const MODAL_WRAPPER_FALLBACK_PX = {
'b2b-default': 484,
'b2b-hed': 291,
'b2b-business': 393,
'b2b-email-first': 409,
'b2b-email-only': 294,
'edu-default': 484,
'edu-hed': 333,
'edu-business': 393,
'student-default': 462,
'student-hed': 422,
'student-business': 422,
'student-email-only': 294,
'legacy-edu-express': 520,
'legacy-edu-hed': 477,
simplified: 400,
};

export function resolveModalWrapperHeight(el, clientId) {
const profile = resolveModalWrapperProfile(el, clientId);
if (!profile) return null;
return MODAL_WRAPPER_FALLBACK_PX[profile] ?? 484;
}

/** CLS: set --susi-modal-wrapper-height before SUSI hydrates. */
export function applyModalWrapperReserve(el, clientId) {
const profile = resolveModalWrapperProfile(el, clientId);
if (!profile) return;
const height = resolveModalWrapperHeight(el, clientId);
if (!height) return;
el.dataset.susiWrapperProfile = profile;
el.style.setProperty('--susi-modal-wrapper-height', `${height}px`);
}

function setTabPanelActive(panel, isActive) {
panel.classList.toggle('hide', !isActive);
panel.setAttribute('aria-hidden', String(!isActive));
if (isActive) {
panel.removeAttribute('inert');
} else {
panel.setAttribute('inert', '');
}
}

async function buildSUSITabs(el, locale, imsClientId, noRedirect) {
const rows = [...el.children];
const title = rows[0].textContent?.trim();
const tabNames = [...rows[1].querySelectorAll('div')].map((div) => div.textContent);
const variants = [...rows[2].querySelectorAll('div')].map((div) => div.textContent?.trim().toLowerCase());
applyTabsReserveFromAuthoring(el, variants);
const redirectUrls = [...rows[3].querySelectorAll('div')].map((div) => div.textContent?.trim());
const client_ids = [...rows[4].querySelectorAll('div')].map((div) => div.textContent?.trim() || (imsClientId ?? 'AdobeExpressWeb'));
const footers = rows[5] ? [...rows[5].querySelectorAll('div')] : [];
Expand Down Expand Up @@ -405,7 +510,7 @@ async function buildSUSITabs(el, locale, imsClientId, noRedirect) {
const id = sanitizeId(`${tabName}-${tabsId}`);
panel.setAttribute('aria-labelledby', `tab-${id}`);
panel.id = `panel-${id}`;
i > 0 && panel.classList.add('hide');
setTabPanelActive(panel, i === 0);
const tab = createTag('button', {
role: 'tab',
'aria-selected': i === 0,
Expand All @@ -415,15 +520,15 @@ async function buildSUSITabs(el, locale, imsClientId, noRedirect) {
tab.addEventListener('click', () => {
tabList.querySelector('[aria-selected=true]')?.setAttribute('aria-selected', false);
tab.setAttribute('aria-selected', true);
panels.forEach((p) => {
p !== panel ? p.classList.add('hide') : p.classList.remove('hide');
});
panels.forEach((p) => setTabPanelActive(p, p === panel));
});
tabList.append(tab);
return panel;
});
const titleDiv = createTag('div', { class: 'title' }, title);
layout.append(createLogo(), titleDiv, tabList, ...panels);
const tabPanelsSlot = createTag('div', { class: 'susi-tab-panels' });
panels.forEach((panel) => tabPanelsSlot.append(panel));
layout.append(createLogo(), titleDiv, tabList, tabPanelsSlot);
return layout;
}

Expand Down Expand Up @@ -518,6 +623,8 @@ export default async function init(el) {
const locale = getConfig().locale.ietf.toLowerCase();
const { imsClientId } = getConfig();
const noRedirect = el.classList.contains('no-redirect');
const clientId = readClientIdFromAuthoring(el, imsClientId);
applyModalWrapperReserve(el, clientId);

/**
* customize can be used to add custom logic to the susi-light component
Expand Down
38 changes: 38 additions & 0 deletions nala/blocks/susi-light/susi-light.page.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,44 @@ class SusiLightBlock {
constructor(page, selector = '.susi-light', nth = 0) {
this.page = page;
this.block = page.locator(selector).nth(nth);
this.layout = this.block.locator('.susi-layout');
this.tabPanels = this.block.locator('.susi-tab-panels');
this.tabs = this.block.locator('[role="tab"]');
this.susiComponent = this.block.locator('susi-sentry-light');
}

async waitForSusiReady(timeout = 30000) {
await this.susiComponent.first().waitFor({ state: 'visible', timeout });
}

async waitForActiveSusiReady(timeout = 30000) {
await this.block.locator('[role="tabpanel"]:not(.hide) susi-sentry-light')
.waitFor({ state: 'visible', timeout });
}

async getLayoutHeight() {
const box = await this.layout.boundingBox();
return box?.height ?? 0;
}

async getTabPanelsHeight() {
const box = await this.tabPanels.boundingBox();
return box?.height ?? 0;
}

async getTabsPanelReservePx() {
const value = await this.block.evaluate((el) => el.style.getPropertyValue('--susi-tabs-panel-height').trim());
return parseInt(value, 10) || 0;
}

async getTabPanelsMinHeightPx() {
return this.tabPanels.evaluate((el) => parseFloat(getComputedStyle(el).minHeight) || 0);
}

async clickTab(index) {
await this.tabs.nth(index).click();
await this.susiComponent.nth(index).waitFor({ state: 'attached', timeout: 30000 });
}
}

module.exports = SusiLightBlock;
21 changes: 21 additions & 0 deletions nala/blocks/susi-light/susi-light.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -127,5 +127,26 @@ test.describe('SusiLightBlock Test Suite', () => {
await test.step('step-4: SEO validation', async () => {
await runSeoChecks({ page, feature: features[1], skipSeoTest: false });
});

await test.step('step-5: Tab panel min-height reserve (CLS)', async () => {
await block.waitForSusiReady();
const susiCount = await block.susiComponent.count();
if (susiCount < 2) {
test.skip(true, 'SUSI CDN unavailable — expected susi-sentry-light in both tab panels');
}

const reservePx = await block.getTabsPanelReservePx();
expect(reservePx).toBeGreaterThan(0);
expect(await block.getTabPanelsMinHeightPx()).toBe(reservePx);
expect(await block.getTabPanelsHeight()).toBeGreaterThanOrEqual(reservePx);

await block.clickTab(1);
await block.waitForActiveSusiReady();
expect(await block.getTabPanelsHeight()).toBeGreaterThanOrEqual(reservePx);

await block.clickTab(0);
await block.waitForActiveSusiReady();
expect(await block.getTabPanelsHeight()).toBeGreaterThanOrEqual(reservePx);
});
});
});
Loading
Loading