diff --git a/packages/fiori/cypress/specs/DynamicPage.cy.tsx b/packages/fiori/cypress/specs/DynamicPage.cy.tsx index 23847b6f4ba4..92aa2e93aa4b 100644 --- a/packages/fiori/cypress/specs/DynamicPage.cy.tsx +++ b/packages/fiori/cypress/specs/DynamicPage.cy.tsx @@ -1197,6 +1197,151 @@ describe("Page layout when content overflows", () => { }); }); +describe("Header actions DOM order", () => { + it("renders header actions after the header slot when expanded", () => { + cy.mount( + + +
Page Title
+
+ +
Header Content
+
+
content
+
+ ); + + cy.get("[ui5-dynamic-page]").should("have.prop", "headerSnapped", false); + + // Actions must NOT be inside the sticky title-header-wrapper + cy.get("[ui5-dynamic-page]") + .shadow() + .find(".ui5-dynamic-page-title-header-wrapper [ui5-dynamic-page-header-actions]") + .should("not.exist"); + + // Actions must be a direct child of the scroll container (after the header slot) + cy.get("[ui5-dynamic-page]") + .shadow() + .find(".ui5-dynamic-page-scroll-container > [ui5-dynamic-page-header-actions]") + .should("exist"); + + // DOM order: header slot must come before actions in the scroll container + cy.get("[ui5-dynamic-page]") + .shadow() + .find(".ui5-dynamic-page-scroll-container") + .then(($container) => { + const children = Array.from($container[0].children); + const headerSlotIdx = children.findIndex( + (el) => el.tagName === "SLOT" && el.getAttribute("name") === "headerArea" + ); + const actionsIdx = children.findIndex((el) => + el.hasAttribute("ui5-dynamic-page-header-actions") + ); + expect(headerSlotIdx).to.be.greaterThan(-1); + expect(actionsIdx).to.be.greaterThan(headerSlotIdx); + }); + }); + + it("renders header actions inside the sticky wrapper when snapped", () => { + cy.mount( + + +
Page Title
+
+ +
Header Content
+
+
content
+
+ ); + + cy.get("[ui5-dynamic-page]").invoke("prop", "headerSnapped", true); + + cy.get("[ui5-dynamic-page]") + .shadow() + .find(".ui5-dynamic-page-title-header-wrapper [ui5-dynamic-page-header-actions]") + .should("exist"); + + cy.get("[ui5-dynamic-page]") + .shadow() + .find(".ui5-dynamic-page-scroll-container > [ui5-dynamic-page-header-actions]") + .should("not.exist"); + }); + + it("renders header actions inside the sticky wrapper when pinned", () => { + cy.mount( + + +
Page Title
+
+ +
Header Content
+
+
content
+
+ ); + + cy.get("[ui5-dynamic-page]").invoke("prop", "headerPinned", true); + + cy.get("[ui5-dynamic-page]") + .shadow() + .find(".ui5-dynamic-page-title-header-wrapper [ui5-dynamic-page-header-actions]") + .should("exist"); + + cy.get("[ui5-dynamic-page]") + .shadow() + .find(".ui5-dynamic-page-scroll-container > [ui5-dynamic-page-header-actions]") + .should("not.exist"); + }); + + it("moves header actions back inside the sticky wrapper when snapping after being expanded", () => { + cy.mount( + + +
Page Title
+
+ +
Header Content
+
+
content
+
+ ); + + // Expanded: actions outside the sticky wrapper + cy.get("[ui5-dynamic-page]") + .shadow() + .find(".ui5-dynamic-page-scroll-container > [ui5-dynamic-page-header-actions]") + .should("exist"); + + // Snap the header + cy.get("[ui5-dynamic-page]").invoke("prop", "headerSnapped", true); + + // Snapped: actions move inside the sticky wrapper + cy.get("[ui5-dynamic-page]") + .shadow() + .find(".ui5-dynamic-page-title-header-wrapper [ui5-dynamic-page-header-actions]") + .should("exist"); + + cy.get("[ui5-dynamic-page]") + .shadow() + .find(".ui5-dynamic-page-scroll-container > [ui5-dynamic-page-header-actions]") + .should("not.exist"); + + // Expand again: actions return outside the sticky wrapper + cy.get("[ui5-dynamic-page]").invoke("prop", "headerSnapped", false); + + cy.get("[ui5-dynamic-page]") + .shadow() + .find(".ui5-dynamic-page-scroll-container > [ui5-dynamic-page-header-actions]") + .should("exist"); + + cy.get("[ui5-dynamic-page]") + .shadow() + .find(".ui5-dynamic-page-title-header-wrapper [ui5-dynamic-page-header-actions]") + .should("not.exist"); + }); +}); + describe("ARIA attributes", () => { it("sets expanded state attributes", () => { cy.mount( diff --git a/packages/fiori/src/DynamicPage.ts b/packages/fiori/src/DynamicPage.ts index 05c203029d21..8494b3e369f8 100644 --- a/packages/fiori/src/DynamicPage.ts +++ b/packages/fiori/src/DynamicPage.ts @@ -312,6 +312,10 @@ class DynamicPage extends UI5Element { return !this.showHeaderInStickArea && !this.headerInTitle && !this.hasSnappedTitleOnMobile; } + get headerActionsAfterHeader(): boolean { + return this.headerInContent && !this._headerSnapped; + } + get _headerLabel() { return this._headerSnapped ? DynamicPage.i18nBundle.getText(DYNAMIC_PAGE_ARIA_LABEL_SNAPPED_HEADER) diff --git a/packages/fiori/src/DynamicPageTemplate.tsx b/packages/fiori/src/DynamicPageTemplate.tsx index 6ae5e33c3187..b500ee2845f3 100644 --- a/packages/fiori/src/DynamicPageTemplate.tsx +++ b/packages/fiori/src/DynamicPageTemplate.tsx @@ -21,7 +21,7 @@ export default function DynamicPageTemplate(this: DynamicPage) { name="headerArea" > } - {this.actionsInTitle && headerActions.call(this)} + {!this.headerActionsAfterHeader && headerActions.call(this)} {this.headerInContent && @@ -30,8 +30,7 @@ export default function DynamicPageTemplate(this: DynamicPage) { name="headerArea" > } - - {!this.actionsInTitle && headerActions.call(this)} + {this.headerActionsAfterHeader && headerActions.call(this)}