Skip to content
145 changes: 145 additions & 0 deletions packages/fiori/cypress/specs/DynamicPage.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<DynamicPage style={{ height: "600px" }}>
<DynamicPageTitle slot="titleArea">
<div slot="heading">Page Title</div>
</DynamicPageTitle>
<DynamicPageHeader slot="headerArea">
<div>Header Content</div>
</DynamicPageHeader>
<div style={{ height: "1000px" }}>content</div>
</DynamicPage>
);

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(
<DynamicPage style={{ height: "600px" }}>
<DynamicPageTitle slot="titleArea">
<div slot="heading">Page Title</div>
</DynamicPageTitle>
<DynamicPageHeader slot="headerArea">
<div>Header Content</div>
</DynamicPageHeader>
<div style={{ height: "1000px" }}>content</div>
</DynamicPage>
);

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(
<DynamicPage style={{ height: "600px" }}>
<DynamicPageTitle slot="titleArea">
<div slot="heading">Page Title</div>
</DynamicPageTitle>
<DynamicPageHeader slot="headerArea">
<div>Header Content</div>
</DynamicPageHeader>
<div style={{ height: "1000px" }}>content</div>
</DynamicPage>
);

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(
<DynamicPage style={{ height: "600px" }}>
<DynamicPageTitle slot="titleArea">
<div slot="heading">Page Title</div>
</DynamicPageTitle>
<DynamicPageHeader slot="headerArea">
<div>Header Content</div>
</DynamicPageHeader>
<div style={{ height: "1000px" }}>content</div>
</DynamicPage>
);

// 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(
Expand Down
4 changes: 4 additions & 0 deletions packages/fiori/src/DynamicPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 2 additions & 3 deletions packages/fiori/src/DynamicPageTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default function DynamicPageTemplate(this: DynamicPage) {
name="headerArea"
></slot>
}
{this.actionsInTitle && headerActions.call(this)}
{!this.headerActionsAfterHeader && headerActions.call(this)}
</div>

{this.headerInContent &&
Expand All @@ -30,8 +30,7 @@ export default function DynamicPageTemplate(this: DynamicPage) {
name="headerArea"
></slot>
}

{!this.actionsInTitle && headerActions.call(this)}
{this.headerActionsAfterHeader && headerActions.call(this)}

<div
part="content"
Expand Down
Loading