From 548bd7e4069ac1d69d3122767945ac1d624da31d Mon Sep 17 00:00:00 2001 From: Diana Pazheva Date: Tue, 7 Jul 2026 19:29:03 +0300 Subject: [PATCH 1/6] fix(ui5-illustrated-message): vertical responsiveness corrected --- .../cypress/specs/IllustratedMessage.cy.tsx | 203 ++++++++++++++---- packages/fiori/src/IllustratedMessage.ts | 125 +++++++---- .../fiori/src/themes/IllustratedMessage.css | 34 ++- 3 files changed, 251 insertions(+), 111 deletions(-) diff --git a/packages/fiori/cypress/specs/IllustratedMessage.cy.tsx b/packages/fiori/cypress/specs/IllustratedMessage.cy.tsx index b89a2107509f0..df17fa6c05e14 100644 --- a/packages/fiori/cypress/specs/IllustratedMessage.cy.tsx +++ b/packages/fiori/cypress/specs/IllustratedMessage.cy.tsx @@ -156,92 +156,153 @@ describe("IllustratedMessage 'design' property", () => { }); }); -describe("Vertical responsiveness", () => { - it("content with auto design shrinks to fit the parent container", () => { - const expectedMedia = "dialog"; +describe("Width-based responsiveness (auto height)", () => { + it("media is base when container width is 100px", () => { + cy.mount( +
+ +
+ ); + cy.get("[ui5-illustrated-message]") + .should("have.attr", "media", IllustratedMessage.MEDIA.BASE); + }); + + it("media is dot when container width is 200px", () => { cy.mount( -
+
); cy.get("[ui5-illustrated-message]") - .shadow() - .find(".ui5-illustrated-message-root") - .should(($contents) => { - const scrollHeight = $contents[0].scrollHeight; - const offsetHeight = $contents[0].offsetHeight; + .should("have.attr", "media", IllustratedMessage.MEDIA.DOT); + }); - expect(scrollHeight).to.be.lessThan(300); - expect(scrollHeight).to.equal(offsetHeight); - }); + it("media is spot when container width is 300px", () => { + cy.mount( +
+ +
+ ); cy.get("[ui5-illustrated-message]") - .should("have.prop", "media", expectedMedia); + .should("have.attr", "media", IllustratedMessage.MEDIA.SPOT); }); - it("content with auto design expands to fit the parent container", () => { - const expectedMedia = "scene"; + it("media is dialog when container width is 500px", () => { + cy.mount( +
+ +
+ ); + cy.get("[ui5-illustrated-message]") + .should("have.attr", "media", IllustratedMessage.MEDIA.DIALOG); + }); + + it("media is scene when container width is 800px", () => { cy.mount( -
+
); + cy.get("[ui5-illustrated-message]") + .should("have.attr", "media", IllustratedMessage.MEDIA.SCENE); + }); + + it("shows image with unconstrained height when container has auto height", () => { + cy.mount( +
+ +
+ ); + + cy.get("div") + .invoke("css", "height", "auto"); + cy.get("[ui5-illustrated-message]") .shadow() - .find(".ui5-illustrated-message-root") - .should(($contents) => { - const scrollHeight = $contents[0].scrollHeight; - const offsetHeight = $contents[0].offsetHeight; - expect(scrollHeight).to.be.lessThan(500); - expect(scrollHeight).to.equal(offsetHeight); + .find(".ui5-illustrated-message-illustration svg") + .should("have.css", "height", "160px"); + }); +}); + +describe("Height-based responsiveness (restricted height, wide container)", () => { + it("media is scene when container height is 600px", () => { + cy.mount( +
+ +
+ ); + + cy.get("[ui5-illustrated-message]") + .should("have.attr", "media", IllustratedMessage.MEDIA.SCENE); + + cy.get("[ui5-illustrated-message]") + .shadow() + .find(".ui5-illustrated-message-inner") + .should(($el) => { + expect($el[0].scrollHeight).to.be.lte($el[0].offsetHeight + 1); }); - + }); + + it("media shrinks to dialog when container height is 350px", () => { + cy.mount( +
+ +
+ ); + + cy.get("[ui5-illustrated-message]") + .should("have.attr", "media", IllustratedMessage.MEDIA.DIALOG); + cy.get("[ui5-illustrated-message]") - .should("have.prop", "media", expectedMedia); + .shadow() + .find(".ui5-illustrated-message-inner") + .should(($el) => { + expect($el[0].scrollHeight).to.be.lte($el[0].offsetHeight + 1); + }); }); - it("content with fixed design fits the parent container", () => { + it("media shrinks to spot when container height is 250px", () => { cy.mount( -
- +
+
); - cy.get("div") - .invoke("css", "height", "250px"); + cy.get("[ui5-illustrated-message]") + .should("have.attr", "media", IllustratedMessage.MEDIA.SPOT); cy.get("[ui5-illustrated-message]") .shadow() - .find(".ui5-illustrated-message-root") - .should(($contents) => { - const scrollHeight = $contents[0].scrollHeight; - const offsetHeight = $contents[0].offsetHeight; - expect(scrollHeight).to.be.lessThan(250); - expect(scrollHeight).to.equal(offsetHeight); + .find(".ui5-illustrated-message-inner") + .should(($el) => { + expect($el[0].scrollHeight).to.be.lte($el[0].offsetHeight + 1); }); }); - it("shows image with unconstrained height when container has auto height", () => { + it("media shrinks to dot when container height is 180px", () => { cy.mount( -
+
); - cy.get("div") - .invoke("css", "height", "auto"); + cy.get("[ui5-illustrated-message]") + .should("have.attr", "media", IllustratedMessage.MEDIA.DOT); cy.get("[ui5-illustrated-message]") .shadow() - .find(".ui5-illustrated-message-illustration svg") - .should("have.css", "height", "160px"); + .find(".ui5-illustrated-message-inner") + .should(($el) => { + expect($el[0].scrollHeight).to.be.lte($el[0].offsetHeight + 1); + }); }); - it("Illustration visible, when container fit content height", () => { + it("illustration is visible when container height fits content", () => { cy.mount(
@@ -257,7 +318,7 @@ describe("Vertical responsiveness", () => { }); }); - it("Illustration visible, when IM slotted and container has fixed height", () => { + it("illustration is visible when IM is slotted and container has fixed height", () => { cy.mount( @@ -274,6 +335,62 @@ describe("Vertical responsiveness", () => { }); }); +describe("Height-based responsiveness (restricted height, dialog-width container)", () => { + it("media is dialog when container height is 500px", () => { + cy.mount( +
+ +
+ ); + + cy.get("[ui5-illustrated-message]") + .should("have.attr", "media", IllustratedMessage.MEDIA.DIALOG); + + cy.get("[ui5-illustrated-message]") + .shadow() + .find(".ui5-illustrated-message-inner") + .should(($el) => { + expect($el[0].scrollHeight).to.be.lte($el[0].offsetHeight + 1); + }); + }); + + it("media shrinks to spot when container height is 250px", () => { + cy.mount( +
+ +
+ ); + + cy.get("[ui5-illustrated-message]") + .should("have.attr", "media", IllustratedMessage.MEDIA.SPOT); + + cy.get("[ui5-illustrated-message]") + .shadow() + .find(".ui5-illustrated-message-inner") + .should(($el) => { + expect($el[0].scrollHeight).to.be.lte($el[0].offsetHeight + 1); + }); + }); + + it("media shrinks to dot when container height is 180px", () => { + cy.mount( +
+ +
+ ); + + cy.get("[ui5-illustrated-message]") + .should("have.attr", "media", IllustratedMessage.MEDIA.DOT); + + cy.get("[ui5-illustrated-message]") + .shadow() + .find(".ui5-illustrated-message-inner") + .should(($el) => { + expect($el[0].scrollHeight).to.be.lte($el[0].offsetHeight + 1); + }); + }); +}); + describe("Dot design resource handling", () => { it("uses substitute Spot illustration", () => { cy.mount( diff --git a/packages/fiori/src/IllustratedMessage.ts b/packages/fiori/src/IllustratedMessage.ts index 73652773cd9d4..daaef74cc1b3c 100644 --- a/packages/fiori/src/IllustratedMessage.ts +++ b/packages/fiori/src/IllustratedMessage.ts @@ -1,5 +1,5 @@ import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; -import type { Slot, DefaultSlot } from "@ui5/webcomponents-base/dist/UI5Element.js"; +import type { Slot, DefaultSlot, ChangeInfo } from "@ui5/webcomponents-base/dist/UI5Element.js"; import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; import property from "@ui5/webcomponents-base/dist/decorators/property.js"; import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js"; @@ -277,7 +277,10 @@ class IllustratedMessage extends UI5Element { static i18nBundle: I18nBundle; _lastKnownOffsetWidthForMedia: Record; _lastKnownOffsetHeightForMedia: Record; + _contentHeightForMedia: Record; _lastKnownMedia: string; + _lastWidth: number | undefined; + _lastHeight: number | undefined; _handleResize: ResizeObserverCallback; constructor() { @@ -287,6 +290,8 @@ class IllustratedMessage extends UI5Element { // this will store the last known offsetWidth of the IllustratedMessage DOM node for a given media (e.g. "Spot") this._lastKnownOffsetWidthForMedia = {}; this._lastKnownOffsetHeightForMedia = {}; + // this will store the last known height of the inner content of the IllustratedMessage (illustration + title + subtitle + actions) for a given media (e.g. "Spot") + this._contentHeightForMedia = {}; // this will store the last known media, in order to detect if IllustratedMessage has been hidden by expand/collapse container this._lastKnownMedia = "base"; } @@ -378,48 +383,77 @@ class IllustratedMessage extends UI5Element { ResizeHandler.deregister(this, this._handleResize); } + onInvalidation(changeInfo: ChangeInfo) { + if ( + (changeInfo.type === "property" && ["name", "titleText", "subtitleText"].includes(changeInfo.name)) || + (changeInfo.type === "slot" && ["title", "subtitle", "default"].includes(changeInfo.name)) + ) { + this._contentHeightForMedia = {}; + } + } + handleResize() { - if (this.design !== IllustrationMessageDesign.Auto) { - this._adjustHeightToFitContainer(); + if (this.design === IllustrationMessageDesign.Auto) { + this._cacheContentHeight(); + this._applyMedia(); + } + } + + _applyMedia() { + const currentWidth = this.offsetWidth; + const currentHeight = this.offsetHeight; + + const isWidthChanged = this._lastWidth !== currentWidth; + const isHeightChanged = this._lastHeight !== currentHeight; + this._lastWidth = currentWidth; + this._lastHeight = currentHeight; + + let newMedia = this._getMediaForSize(currentWidth); + + const lastKnownOffsetWidth = this._lastKnownOffsetWidthForMedia[newMedia]; + const lastKnownOffsetHeight = this._lastKnownOffsetHeightForMedia[newMedia]; + this._lastKnownOffsetWidthForMedia[newMedia] = currentWidth; + this._lastKnownOffsetHeightForMedia[newMedia] = currentHeight; + // prevents infinite resizing, when same width is detected for the same media, + // excluding the case in which, the control is placed inside expand/collapse container + if (isWidthChanged && !isHeightChanged && lastKnownOffsetWidth && currentWidth === lastKnownOffsetWidth + && this._lastKnownOffsetWidthForMedia[this._lastKnownMedia] !== 0 && currentHeight === lastKnownOffsetHeight + && this._lastKnownOffsetHeightForMedia[this._lastKnownMedia] !== 0) { return; } - this._applyMedia(); - window.requestAnimationFrame(this._adjustHeightToFitContainer.bind(this)); + this.media = newMedia; + this._lastKnownMedia = newMedia; } - _applyMedia(heightChange?: boolean) { - const currOffsetWidth = this.offsetWidth, - currOffsetHeight = this.offsetHeight; - - const design = heightChange ? currOffsetHeight : currOffsetWidth, - oBreakpounts = heightChange ? IllustratedMessage.BREAKPOINTS_HEIGHT : IllustratedMessage.BREAKPOINTS; - let newMedia = ""; - - if (design <= oBreakpounts.BASE) { - newMedia = IllustratedMessage.MEDIA.BASE; - } else if (design <= oBreakpounts.DOT) { - newMedia = IllustratedMessage.MEDIA.DOT; - } else if (design <= oBreakpounts.SPOT) { - newMedia = IllustratedMessage.MEDIA.SPOT; - } else if (design <= oBreakpounts.DIALOG) { - newMedia = IllustratedMessage.MEDIA.DIALOG; + _getMediaForSize(width: number): string { + let media = ""; + let mediaIndex = -1; + if (width <= IllustratedMessage.BREAKPOINTS.BASE) { + media = IllustratedMessage.MEDIA.BASE; + } else if (width <= IllustratedMessage.BREAKPOINTS.DOT) { + media = IllustratedMessage.MEDIA.DOT; + } else if (width <= IllustratedMessage.BREAKPOINTS.SPOT) { + media = IllustratedMessage.MEDIA.SPOT; + } else if (width <= IllustratedMessage.BREAKPOINTS.DIALOG) { + media = IllustratedMessage.MEDIA.DIALOG; } else { - newMedia = IllustratedMessage.MEDIA.SCENE; + media = IllustratedMessage.MEDIA.SCENE; } - const lastKnownOffsetWidth = this._lastKnownOffsetWidthForMedia[newMedia], - lastKnownOffsetHeight = this._lastKnownOffsetHeightForMedia[newMedia]; - // prevents infinite resizing, when same width is detected for the same media, - // excluding the case in which, the control is placed inside expand/collapse container - if (!(lastKnownOffsetWidth && currOffsetWidth === lastKnownOffsetWidth - && lastKnownOffsetHeight && currOffsetHeight === lastKnownOffsetHeight) - || this._lastKnownOffsetWidthForMedia[this._lastKnownMedia] === 0 - || this._lastKnownOffsetHeightForMedia[this._lastKnownMedia] === 0) { - this.media = newMedia; - this._lastKnownOffsetWidthForMedia[newMedia] = currOffsetWidth; - this._lastKnownOffsetHeightForMedia[newMedia] = currOffsetHeight; - this._lastKnownMedia = newMedia; + + mediaIndex = Object.values(IllustratedMessage.MEDIA).indexOf(media); + + while (mediaIndex > 0 && this._mediaRequiresVerticalScrollbar(media)) { + mediaIndex--; + media = Object.values(IllustratedMessage.MEDIA)[mediaIndex]; } + + return media; + } + + _mediaRequiresVerticalScrollbar(media: string): boolean { + const containerHeight = this.clientHeight || 0; + return !!this._contentHeightForMedia[media] && containerHeight < this._contentHeightForMedia[media]; } _setSVGAccAttrs() { @@ -446,21 +480,20 @@ class IllustratedMessage extends UI5Element { } } - _adjustHeightToFitContainer() { - const illustrationWrapper = this.shadowRoot!.querySelector(".ui5-illustrated-message-illustration"), - illustration = illustrationWrapper.querySelector("svg"); - - if (illustration) { - illustrationWrapper.classList.toggle("ui5-illustrated-message-illustration-fit-content", false); - if (this.getDomRef()!.scrollHeight > this.getDomRef()!.offsetHeight) { - illustrationWrapper.classList.toggle("ui5-illustrated-message-illustration-fit-content", true); - this._applyMedia(true /* height change */); - } + onAfterRendering() { + this._setSVGAccAttrs(); + if (this.design === IllustrationMessageDesign.Auto) { + this._cacheContentHeight(); + this._applyMedia(); } } - onAfterRendering() { - this._setSVGAccAttrs(); + _cacheContentHeight() { + if (this.media && this.scrollHeight > this.clientHeight) { + const innerEl = this.shadowRoot!.querySelector(".ui5-illustrated-message-inner"); + const innerElHeight = innerEl ? innerEl.scrollHeight : 0; + innerElHeight && (this._contentHeightForMedia[this.media] = innerElHeight); + } } /** diff --git a/packages/fiori/src/themes/IllustratedMessage.css b/packages/fiori/src/themes/IllustratedMessage.css index b7e7b8d6d4d29..769658c837501 100644 --- a/packages/fiori/src/themes/IllustratedMessage.css +++ b/packages/fiori/src/themes/IllustratedMessage.css @@ -6,7 +6,6 @@ box-sizing: border-box; width: 100%; height:100%; - padding: 1rem; } .ui5-illustrated-message-root, @@ -17,10 +16,19 @@ align-items: center; justify-content: center; height: inherit; - min-height: 0; flex-basis: content; } +.ui5-illustrated-message-root, +.ui5-illustrated-message-inner { + min-height: 0; +} + +.ui5-illustrated-message-inner { + padding: 1rem; + box-sizing: border-box; +} + .ui5-illustrated-message-illustration { margin: 2rem 0; } @@ -30,24 +38,6 @@ max-width: 100%; } -.ui5-illustrated-message-illustration.ui5-illustrated-message-illustration-fit-content { - position: relative; - width: 0; - padding: 0; - padding-left: 100%; - height: 100%; - display: flex; - align-items: center; -} - -.ui5-illustrated-message-illustration.ui5-illustrated-message-illustration-fit-content svg { - position: absolute; - height: 100%; - width: 100%; - left: 0; - top: 0; -} - .ui5-illustrated-message-title [ui5-title], .ui5-illustrated-message-title ::slotted([slot="title"]) { text-align: center; @@ -93,7 +83,7 @@ margin-bottom: 1rem; } -:host([media="spot"]) { +:host([media="spot"]) .ui5-illustrated-message-inner { padding: 0.5rem; } @@ -119,7 +109,7 @@ margin: 0.5rem 0; } -:host([media="dot"]) { +:host([media="dot"]) .ui5-illustrated-message-inner { padding: 0.25rem; min-height: 4rem; /* needed to avoid infinite loop between "dot" and "base" when only illustration is used (no title, etc.) */ } From 3a1b36ca60fda2dc466bf6f33f7a1b7fcf3d2f0e Mon Sep 17 00:00:00 2001 From: Diana Pazheva Date: Tue, 7 Jul 2026 19:34:25 +0300 Subject: [PATCH 2/6] fix(ui5-illustrated-message): remove obsolete code --- packages/fiori/src/IllustratedMessage.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/fiori/src/IllustratedMessage.ts b/packages/fiori/src/IllustratedMessage.ts index daaef74cc1b3c..c2166577f7fb8 100644 --- a/packages/fiori/src/IllustratedMessage.ts +++ b/packages/fiori/src/IllustratedMessage.ts @@ -305,15 +305,6 @@ class IllustratedMessage extends UI5Element { }; } - static get BREAKPOINTS_HEIGHT() { - return { - DIALOG: 415, - SPOT: 284, - DOT: 207, - BASE: 61, - }; - } - static get MEDIA() { return { BASE: "base", From 70874172b49e1b1a000755b830f7fc0744a536c8 Mon Sep 17 00:00:00 2001 From: Diana Pazheva Date: Wed, 8 Jul 2026 09:46:38 +0300 Subject: [PATCH 3/6] fix(ui5-illustrated-message): minot improvements --- .../cypress/specs/IllustratedMessage.cy.tsx | 32 ++++++++++++++ packages/fiori/src/IllustratedMessage.ts | 42 ++++++++++--------- 2 files changed, 55 insertions(+), 19 deletions(-) diff --git a/packages/fiori/cypress/specs/IllustratedMessage.cy.tsx b/packages/fiori/cypress/specs/IllustratedMessage.cy.tsx index df17fa6c05e14..a3ae8c2f21920 100644 --- a/packages/fiori/cypress/specs/IllustratedMessage.cy.tsx +++ b/packages/fiori/cypress/specs/IllustratedMessage.cy.tsx @@ -212,6 +212,38 @@ describe("Width-based responsiveness (auto height)", () => { .should("have.attr", "media", IllustratedMessage.MEDIA.SCENE); }); + it("media updates from dot to scene when container width expands", () => { + // This test guards against a specific bug: without the `scrollHeight > clientHeight` + // guard in _checkHeightConstraints, _contentHeightForMedia["scene"] gets recorded + // while scene is rendered at full width. When width then shrinks (dot media), the + // container height (auto) shrinks too. On re-expansion, _mediaExceedsContainerHeight("scene") + // compares the stored scene height against the current (small) clientHeight and wrongly + // blocks the upgrade back to scene. + cy.mount( +
+ +
+ ); + + // First render at wide width so _contentHeightForMedia["scene"] gets populated + cy.get("[ui5-illustrated-message]") + .should("have.attr", "media", IllustratedMessage.MEDIA.SCENE); + + // Shrink to dot — clientHeight (auto) now equals the small dot illustration height + cy.get("#resizable-container") + .invoke("css", "width", "200px"); + + cy.get("[ui5-illustrated-message]") + .should("have.attr", "media", IllustratedMessage.MEDIA.DOT); + + // Expand back — must resolve to scene, not stay stuck at dot/dialog + cy.get("#resizable-container") + .invoke("css", "width", "800px"); + + cy.get("[ui5-illustrated-message]") + .should("have.attr", "media", IllustratedMessage.MEDIA.SCENE); + }); + it("shows image with unconstrained height when container has auto height", () => { cy.mount(
diff --git a/packages/fiori/src/IllustratedMessage.ts b/packages/fiori/src/IllustratedMessage.ts index c2166577f7fb8..a03bfa57d8036 100644 --- a/packages/fiori/src/IllustratedMessage.ts +++ b/packages/fiori/src/IllustratedMessage.ts @@ -376,8 +376,8 @@ class IllustratedMessage extends UI5Element { onInvalidation(changeInfo: ChangeInfo) { if ( - (changeInfo.type === "property" && ["name", "titleText", "subtitleText"].includes(changeInfo.name)) || - (changeInfo.type === "slot" && ["title", "subtitle", "default"].includes(changeInfo.name)) + (changeInfo.type === "property" && ["name", "titleText", "subtitleText"].includes(changeInfo.name)) + || (changeInfo.type === "slot" && ["title", "subtitle", "default"].includes(changeInfo.name)) ) { this._contentHeightForMedia = {}; } @@ -385,24 +385,22 @@ class IllustratedMessage extends UI5Element { handleResize() { if (this.design === IllustrationMessageDesign.Auto) { - this._cacheContentHeight(); + this._checkHeightConstraints(); this._applyMedia(); } } _applyMedia() { - const currentWidth = this.offsetWidth; - const currentHeight = this.offsetHeight; + const currentWidth = this.offsetWidth, + currentHeight = this.offsetHeight, + isWidthChanged = this._lastWidth !== currentWidth, + isHeightChanged = this._lastHeight !== currentHeight, + newMedia = this._getMediaForSize(currentWidth), + lastKnownOffsetWidth = this._lastKnownOffsetWidthForMedia[newMedia], + lastKnownOffsetHeight = this._lastKnownOffsetHeightForMedia[newMedia]; - const isWidthChanged = this._lastWidth !== currentWidth; - const isHeightChanged = this._lastHeight !== currentHeight; this._lastWidth = currentWidth; this._lastHeight = currentHeight; - - let newMedia = this._getMediaForSize(currentWidth); - - const lastKnownOffsetWidth = this._lastKnownOffsetWidthForMedia[newMedia]; - const lastKnownOffsetHeight = this._lastKnownOffsetHeightForMedia[newMedia]; this._lastKnownOffsetWidthForMedia[newMedia] = currentWidth; this._lastKnownOffsetHeightForMedia[newMedia] = currentHeight; // prevents infinite resizing, when same width is detected for the same media, @@ -418,8 +416,8 @@ class IllustratedMessage extends UI5Element { } _getMediaForSize(width: number): string { - let media = ""; - let mediaIndex = -1; + let media = "", + mediaIndex = -1; if (width <= IllustratedMessage.BREAKPOINTS.BASE) { media = IllustratedMessage.MEDIA.BASE; } else if (width <= IllustratedMessage.BREAKPOINTS.DOT) { @@ -434,7 +432,7 @@ class IllustratedMessage extends UI5Element { mediaIndex = Object.values(IllustratedMessage.MEDIA).indexOf(media); - while (mediaIndex > 0 && this._mediaRequiresVerticalScrollbar(media)) { + while (mediaIndex > 0 && this._mediaExceedsContainerHeight(media)) { mediaIndex--; media = Object.values(IllustratedMessage.MEDIA)[mediaIndex]; } @@ -442,7 +440,7 @@ class IllustratedMessage extends UI5Element { return media; } - _mediaRequiresVerticalScrollbar(media: string): boolean { + _mediaExceedsContainerHeight(media: string): boolean { const containerHeight = this.clientHeight || 0; return !!this._contentHeightForMedia[media] && containerHeight < this._contentHeightForMedia[media]; } @@ -474,13 +472,19 @@ class IllustratedMessage extends UI5Element { onAfterRendering() { this._setSVGAccAttrs(); if (this.design === IllustrationMessageDesign.Auto) { - this._cacheContentHeight(); + this._checkHeightConstraints(); this._applyMedia(); } } - _cacheContentHeight() { - if (this.media && this.scrollHeight > this.clientHeight) { + /** + * Checks if the current height of the component is enough to display the illustration, title, subtitle and actions. + * If not, the minimum required height for the current media is stored in the `_contentHeightForMedia` object. + * @private + * @since 1.5.0 + */ + _checkHeightConstraints() { + if (this.media && this.scrollHeight > this.clientHeight) { // needs vertical responsiveness const innerEl = this.shadowRoot!.querySelector(".ui5-illustrated-message-inner"); const innerElHeight = innerEl ? innerEl.scrollHeight : 0; innerElHeight && (this._contentHeightForMedia[this.media] = innerElHeight); From 5ef5267534954922520a580bccfe9775e645a7f5 Mon Sep 17 00:00:00 2001 From: Diana Pazheva Date: Wed, 8 Jul 2026 15:19:14 +0300 Subject: [PATCH 4/6] fix(ui5-illustrated-message): remove obsolete code --- packages/fiori/src/IllustratedMessage.ts | 65 ++++++------------------ 1 file changed, 16 insertions(+), 49 deletions(-) diff --git a/packages/fiori/src/IllustratedMessage.ts b/packages/fiori/src/IllustratedMessage.ts index a03bfa57d8036..d2b8fd297f68e 100644 --- a/packages/fiori/src/IllustratedMessage.ts +++ b/packages/fiori/src/IllustratedMessage.ts @@ -275,25 +275,15 @@ class IllustratedMessage extends UI5Element { @i18n("@ui5/webcomponents-fiori") static i18nBundle: I18nBundle; - _lastKnownOffsetWidthForMedia: Record; - _lastKnownOffsetHeightForMedia: Record; _contentHeightForMedia: Record; - _lastKnownMedia: string; - _lastWidth: number | undefined; - _lastHeight: number | undefined; _handleResize: ResizeObserverCallback; constructor() { super(); this._handleResize = this.handleResize.bind(this); - // this will store the last known offsetWidth of the IllustratedMessage DOM node for a given media (e.g. "Spot") - this._lastKnownOffsetWidthForMedia = {}; - this._lastKnownOffsetHeightForMedia = {}; - // this will store the last known height of the inner content of the IllustratedMessage (illustration + title + subtitle + actions) for a given media (e.g. "Spot") + // this will store the height of the inner content of the IllustratedMessage (illustration + title + subtitle + actions) for a given media (e.g. "Spot") this._contentHeightForMedia = {}; - // this will store the last known media, in order to detect if IllustratedMessage has been hidden by expand/collapse container - this._lastKnownMedia = "base"; } static get BREAKPOINTS() { @@ -390,34 +380,25 @@ class IllustratedMessage extends UI5Element { } } - _applyMedia() { - const currentWidth = this.offsetWidth, - currentHeight = this.offsetHeight, - isWidthChanged = this._lastWidth !== currentWidth, - isHeightChanged = this._lastHeight !== currentHeight, - newMedia = this._getMediaForSize(currentWidth), - lastKnownOffsetWidth = this._lastKnownOffsetWidthForMedia[newMedia], - lastKnownOffsetHeight = this._lastKnownOffsetHeightForMedia[newMedia]; - - this._lastWidth = currentWidth; - this._lastHeight = currentHeight; - this._lastKnownOffsetWidthForMedia[newMedia] = currentWidth; - this._lastKnownOffsetHeightForMedia[newMedia] = currentHeight; - // prevents infinite resizing, when same width is detected for the same media, - // excluding the case in which, the control is placed inside expand/collapse container - if (isWidthChanged && !isHeightChanged && lastKnownOffsetWidth && currentWidth === lastKnownOffsetWidth - && this._lastKnownOffsetWidthForMedia[this._lastKnownMedia] !== 0 && currentHeight === lastKnownOffsetHeight - && this._lastKnownOffsetHeightForMedia[this._lastKnownMedia] !== 0) { - return; + /** + * Checks if the current height of the component is enough to display the illustration, title, subtitle and actions. + * If not, the minimum required height for the current media is stored in the `_contentHeightForMedia` object. + * @private + * @since 1.5.0 + */ + _checkHeightConstraints() { + if (this.media && this.scrollHeight > this.clientHeight) { // needs vertical responsiveness + const innerEl = this.shadowRoot!.querySelector(".ui5-illustrated-message-inner"); + const innerElHeight = innerEl ? innerEl.scrollHeight : 0; + innerElHeight && (this._contentHeightForMedia[this.media] = innerElHeight); } - - this.media = newMedia; - this._lastKnownMedia = newMedia; } - _getMediaForSize(width: number): string { + _applyMedia() { + const width = this.offsetWidth; let media = "", mediaIndex = -1; + if (width <= IllustratedMessage.BREAKPOINTS.BASE) { media = IllustratedMessage.MEDIA.BASE; } else if (width <= IllustratedMessage.BREAKPOINTS.DOT) { @@ -437,7 +418,7 @@ class IllustratedMessage extends UI5Element { media = Object.values(IllustratedMessage.MEDIA)[mediaIndex]; } - return media; + this.media = media; } _mediaExceedsContainerHeight(media: string): boolean { @@ -477,20 +458,6 @@ class IllustratedMessage extends UI5Element { } } - /** - * Checks if the current height of the component is enough to display the illustration, title, subtitle and actions. - * If not, the minimum required height for the current media is stored in the `_contentHeightForMedia` object. - * @private - * @since 1.5.0 - */ - _checkHeightConstraints() { - if (this.media && this.scrollHeight > this.clientHeight) { // needs vertical responsiveness - const innerEl = this.shadowRoot!.querySelector(".ui5-illustrated-message-inner"); - const innerElHeight = innerEl ? innerEl.scrollHeight : 0; - innerElHeight && (this._contentHeightForMedia[this.media] = innerElHeight); - } - } - /** * Modifies the IM styles in accordance to the `size` property's value. * Note: The resize handler has no effect when size is different than "Auto". From 36796d9691e7039182e6c4b01413083a66a36c64 Mon Sep 17 00:00:00 2001 From: Diana Pazheva Date: Mon, 20 Jul 2026 16:57:36 +0300 Subject: [PATCH 5/6] fix(ui5-illustrated-message): clear content-height cache on theme change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A themeAware component re-renders via renderDeferred from reRenderAllUI5Elements, which bypasses _invalidate. onInvalidation therefore never fires on a theme switch, and _contentHeightForMedia accumulates stale entries across themes: a theme whose typography once caused scene -> dialog leaves an oversized scene height in the cache; switching to a theme whose typography would now fit keeps the stale entry, so _applyMedia keeps rejecting scene. Subscribe to attachThemeLoaded in onEnterDOM (detach in onExitDOM) and clear _contentHeightForMedia in the listener. The framework's own themeAware re-render then re-measures fresh via onAfterRendering -> _checkHeightConstraints -> _applyMedia. Also document the load-bearing scrollHeight > clientHeight guard in _checkHeightConstraints so a future contributor is not tempted to remove it. The Cypress test poisons the internal cache with an out-of-range value, fires setTheme, and asserts the media does not downgrade — pinning the contract that the cache is cleared on theme change without depending on the pixel-boundary between two themes' typographies. --- .../cypress/specs/IllustratedMessage.cy.tsx | 53 +++++++++++++++++++ packages/fiori/src/IllustratedMessage.ts | 14 +++++ .../fiori/test/pages/IllustratedMessage.html | 17 +++++- 3 files changed, 83 insertions(+), 1 deletion(-) diff --git a/packages/fiori/cypress/specs/IllustratedMessage.cy.tsx b/packages/fiori/cypress/specs/IllustratedMessage.cy.tsx index a3ae8c2f21920..def7ae4c09783 100644 --- a/packages/fiori/cypress/specs/IllustratedMessage.cy.tsx +++ b/packages/fiori/cypress/specs/IllustratedMessage.cy.tsx @@ -2,6 +2,7 @@ import IllustratedMessage from "../../src/IllustratedMessage.js"; import Label from "@ui5/webcomponents/dist/Label.js"; import "@ui5/webcomponents-fiori/dist/illustrations/AllIllustrations.js" import Panel from "@ui5/webcomponents/dist/Panel.js"; +import { setTheme } from "@ui5/webcomponents-base/dist/config/Theme.js"; describe("Accessibility", () => { it("should add aria-hidden and role=presetation to the SVG when decorative is true", () => { @@ -262,6 +263,12 @@ describe("Width-based responsiveness (auto height)", () => { }); describe("Height-based responsiveness (restricted height, wide container)", () => { + after(() => { + // Restore theme so that a switch performed inside this describe does not bleed into + // subsequent specs. Mirrors the pattern in IllustratedMessageV5.cy.tsx. + cy.wrap({ setTheme }).invoke("setTheme", "sap_horizon"); + }); + it("media is scene when container height is 600px", () => { cy.mount(
@@ -365,6 +372,52 @@ describe("Height-based responsiveness (restricted height, wide container)", () = expect(scrollHeight).to.not.equal(0); }); }); + + it("clears the content-height cache on theme change so media re-evaluates freshly", () => { + // Guards against a stale-cache bug: a themeAware re-render bypasses `_invalidate` (it goes + // through `renderDeferred` from `reRenderAllUI5Elements`), so `onInvalidation` does not fire + // on a theme switch. Without an explicit `attachThemeLoaded` cache clear, a downgrade + // recorded under one theme's typography would persist and prevent the media from + // re-upgrading once a theme with tighter fonts is applied. + // + // Rather than depend on the exact pixel-boundary between two themes' typographies (which is + // fragile to CSS changes), this test asserts the contract directly: after a theme change, + // the internal `_contentHeightForMedia` cache is empty, so any previously recorded + // overflow height no longer blocks media selection. + cy.mount( +
+ +
+ ); + + // Baseline: scene fits comfortably in 800×600. + cy.get("[ui5-illustrated-message]") + .should("have.attr", "media", IllustratedMessage.MEDIA.SCENE); + + // Poison the cache with an impossibly large scene height. On the next render without a + // cache clear, `_applyMedia` would treat scene as exceeding the container and downgrade. + cy.get("[ui5-illustrated-message]").then(($el) => { + const im = $el[0] as unknown as IllustratedMessage; + im._contentHeightForMedia[IllustratedMessage.MEDIA.SCENE] = 99999; + }); + + // Fire a theme change. The fix installs an `attachThemeLoaded` listener that clears the + // cache before the framework's themeAware re-render measures against the new theme. + cy.wrap({ setTheme }).invoke("setTheme", "sap_fiori_3"); + + // If the cache was cleared, the poisoned entry is gone and scene stays selected. + // Without the fix, the stale entry blocks scene and media downgrades to dialog. + cy.get("[ui5-illustrated-message]") + .should("have.attr", "media", IllustratedMessage.MEDIA.SCENE); + + cy.get("[ui5-illustrated-message]").then(($el) => { + const im = $el[0] as unknown as IllustratedMessage; + // The cache was cleared and then re-populated only if the (fresh) content actually + // overflows the container. In 800×600 nothing overflows, so no scene entry should + // remain from the poisoned value. + expect(im._contentHeightForMedia[IllustratedMessage.MEDIA.SCENE]).to.be.undefined; + }); + }); }); describe("Height-based responsiveness (restricted height, dialog-width container)", () => { diff --git a/packages/fiori/src/IllustratedMessage.ts b/packages/fiori/src/IllustratedMessage.ts index d2b8fd297f68e..5c99af7613e33 100644 --- a/packages/fiori/src/IllustratedMessage.ts +++ b/packages/fiori/src/IllustratedMessage.ts @@ -6,6 +6,7 @@ import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js"; import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js"; import ResizeHandler from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js"; import type { ResizeObserverCallback } from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js"; +import { attachThemeLoaded, detachThemeLoaded } from "@ui5/webcomponents-base/dist/Theming.js"; import { getIllustrationDataSync, getIllustrationData } from "@ui5/webcomponents-base/dist/asset-registries/Illustrations.js"; import { getEffectiveAriaLabelText } from "@ui5/webcomponents-base/dist/util/AccessibilityTextsHelper.js"; import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js"; @@ -277,11 +278,17 @@ class IllustratedMessage extends UI5Element { static i18nBundle: I18nBundle; _contentHeightForMedia: Record; _handleResize: ResizeObserverCallback; + _handleThemeLoaded: () => void; constructor() { super(); this._handleResize = this.handleResize.bind(this); + this._handleThemeLoaded = () => { + // Cached content-height are theme-dependent, so clear them when the theme changes. + // This hook is needed because `onInvalidation` does not fire when the theme changes + this._contentHeightForMedia = {}; + }; // this will store the height of the inner content of the IllustratedMessage (illustration + title + subtitle + actions) for a given media (e.g. "Spot") this._contentHeightForMedia = {}; } @@ -358,10 +365,12 @@ class IllustratedMessage extends UI5Element { onEnterDOM() { ResizeHandler.register(this, this._handleResize); + attachThemeLoaded(this._handleThemeLoaded); } onExitDOM() { ResizeHandler.deregister(this, this._handleResize); + detachThemeLoaded(this._handleThemeLoaded); } onInvalidation(changeInfo: ChangeInfo) { @@ -387,6 +396,11 @@ class IllustratedMessage extends UI5Element { * @since 1.5.0 */ _checkHeightConstraints() { + // The `scrollHeight > clientHeight` guard is load-bearing: the cache must be populated ONLY + // when the content genuinely overflows the container. When the host container has + // `height: auto`, its clientHeight equals the content height and there is by definition no + // real constraint — recording that height would falsely poison `_contentHeightForMedia` and + // cause spurious downgrades on the next render (e.g. after a width shrink-and-grow). if (this.media && this.scrollHeight > this.clientHeight) { // needs vertical responsiveness const innerEl = this.shadowRoot!.querySelector(".ui5-illustrated-message-inner"); const innerElHeight = innerEl ? innerEl.scrollHeight : 0; diff --git a/packages/fiori/test/pages/IllustratedMessage.html b/packages/fiori/test/pages/IllustratedMessage.html index 59d356e948fed..8770f1effd7c4 100644 --- a/packages/fiori/test/pages/IllustratedMessage.html +++ b/packages/fiori/test/pages/IllustratedMessage.html @@ -198,13 +198,23 @@

Vertical responsiveness

500px 700px + Theme: + + sap_horizon + sap_horizon_hcb + sap_horizon_hcw + sap_fiori_3 + sap_fiori_3_hcb +
Action 1
- From eb1625d1a3224fd630ce64730cd7c008f7a224c6 Mon Sep 17 00:00:00 2001 From: Diana Pazheva Date: Mon, 20 Jul 2026 17:08:39 +0300 Subject: [PATCH 6/6] fix(ui5-illustrated-message): cleanup comments --- .../fiori/cypress/specs/IllustratedMessage.cy.tsx | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/fiori/cypress/specs/IllustratedMessage.cy.tsx b/packages/fiori/cypress/specs/IllustratedMessage.cy.tsx index def7ae4c09783..41724802b2a75 100644 --- a/packages/fiori/cypress/specs/IllustratedMessage.cy.tsx +++ b/packages/fiori/cypress/specs/IllustratedMessage.cy.tsx @@ -374,16 +374,6 @@ describe("Height-based responsiveness (restricted height, wide container)", () = }); it("clears the content-height cache on theme change so media re-evaluates freshly", () => { - // Guards against a stale-cache bug: a themeAware re-render bypasses `_invalidate` (it goes - // through `renderDeferred` from `reRenderAllUI5Elements`), so `onInvalidation` does not fire - // on a theme switch. Without an explicit `attachThemeLoaded` cache clear, a downgrade - // recorded under one theme's typography would persist and prevent the media from - // re-upgrading once a theme with tighter fonts is applied. - // - // Rather than depend on the exact pixel-boundary between two themes' typographies (which is - // fragile to CSS changes), this test asserts the contract directly: after a theme change, - // the internal `_contentHeightForMedia` cache is empty, so any previously recorded - // overflow height no longer blocks media selection. cy.mount(
@@ -394,7 +384,7 @@ describe("Height-based responsiveness (restricted height, wide container)", () = cy.get("[ui5-illustrated-message]") .should("have.attr", "media", IllustratedMessage.MEDIA.SCENE); - // Poison the cache with an impossibly large scene height. On the next render without a + // Poison the cache with an impossibly large scene content-height. On the next render without a // cache clear, `_applyMedia` would treat scene as exceeding the container and downgrade. cy.get("[ui5-illustrated-message]").then(($el) => { const im = $el[0] as unknown as IllustratedMessage;