diff --git a/packages/fiori/cypress/specs/IllustratedMessage.cy.tsx b/packages/fiori/cypress/specs/IllustratedMessage.cy.tsx index b89a2107509f..a3ae8c2f2192 100644 --- a/packages/fiori/cypress/specs/IllustratedMessage.cy.tsx +++ b/packages/fiori/cypress/specs/IllustratedMessage.cy.tsx @@ -156,73 +156,92 @@ 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]") - .shadow() - .find(".ui5-illustrated-message-root") - .should(($contents) => { - const scrollHeight = $contents[0].scrollHeight; - const offsetHeight = $contents[0].offsetHeight; + .should("have.attr", "media", IllustratedMessage.MEDIA.BASE); + }); - expect(scrollHeight).to.be.lessThan(300); - expect(scrollHeight).to.equal(offsetHeight); - }); + it("media is dot when container width is 200px", () => { + cy.mount( +
+ +
+ ); cy.get("[ui5-illustrated-message]") - .should("have.prop", "media", expectedMedia); + .should("have.attr", "media", IllustratedMessage.MEDIA.DOT); }); - it("content with auto design expands to fit the parent container", () => { - const expectedMedia = "scene"; + it("media is spot when container width is 300px", () => { + cy.mount( +
+ +
+ ); + + cy.get("[ui5-illustrated-message]") + .should("have.attr", "media", IllustratedMessage.MEDIA.SPOT); + }); + it("media is dialog when container width is 500px", () => { 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; - expect(scrollHeight).to.be.lessThan(500); - expect(scrollHeight).to.equal(offsetHeight); - }); - + .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.prop", "media", expectedMedia); + .should("have.attr", "media", IllustratedMessage.MEDIA.SCENE); }); - it("content with fixed design fits the parent container", () => { + 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( -
- +
+
); - cy.get("div") - .invoke("css", "height", "250px"); + // 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]") - .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); - }); + .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", () => { @@ -240,8 +259,82 @@ describe("Vertical responsiveness", () => { .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]") + .shadow() + .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("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); + }); + }); + + it("illustration is visible when container height fits content", () => { cy.mount(
@@ -257,7 +350,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 +367,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 73652773cd9d..d2b8fd297f68 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"; @@ -275,20 +275,15 @@ class IllustratedMessage extends UI5Element { @i18n("@ui5/webcomponents-fiori") static i18nBundle: I18nBundle; - _lastKnownOffsetWidthForMedia: Record; - _lastKnownOffsetHeightForMedia: Record; - _lastKnownMedia: string; + _contentHeightForMedia: Record; _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 media, in order to detect if IllustratedMessage has been hidden by expand/collapse container - this._lastKnownMedia = "base"; + // 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 = {}; } static get BREAKPOINTS() { @@ -300,15 +295,6 @@ class IllustratedMessage extends UI5Element { }; } - static get BREAKPOINTS_HEIGHT() { - return { - DIALOG: 415, - SPOT: 284, - DOT: 207, - BASE: 61, - }; - } - static get MEDIA() { return { BASE: "base", @@ -378,48 +364,66 @@ 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(); - return; + if (this.design === IllustrationMessageDesign.Auto) { + this._checkHeightConstraints(); + this._applyMedia(); } + } - this._applyMedia(); - window.requestAnimationFrame(this._adjustHeightToFitContainer.bind(this)); + /** + * 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); + } } - _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; + _applyMedia() { + const width = this.offsetWidth; + let media = "", + 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._mediaExceedsContainerHeight(media)) { + mediaIndex--; + media = Object.values(IllustratedMessage.MEDIA)[mediaIndex]; } + + this.media = media; + } + + _mediaExceedsContainerHeight(media: string): boolean { + const containerHeight = this.clientHeight || 0; + return !!this._contentHeightForMedia[media] && containerHeight < this._contentHeightForMedia[media]; } _setSVGAccAttrs() { @@ -446,21 +450,12 @@ 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._checkHeightConstraints(); + this._applyMedia(); + } } /** diff --git a/packages/fiori/src/themes/IllustratedMessage.css b/packages/fiori/src/themes/IllustratedMessage.css index b7e7b8d6d4d2..769658c83750 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.) */ }