diff --git a/packages/fiori/cypress/specs/DynamicPage.cy.tsx b/packages/fiori/cypress/specs/DynamicPage.cy.tsx
index 23847b6f4ba42..1b9bdbb798b9b 100644
--- a/packages/fiori/cypress/specs/DynamicPage.cy.tsx
+++ b/packages/fiori/cypress/specs/DynamicPage.cy.tsx
@@ -1329,37 +1329,35 @@ describe("ARIA attributes", () => {
cy.get("[ui5-dynamic-page]").as("dynamicPage");
- // Initially the header should not be pinned, so tooltip should be "Pin Header"
cy.get("@dynamicPage")
.shadow()
.find("[ui5-dynamic-page-header-actions]")
.shadow()
.find(".ui5-dynamic-page-header-action-pin")
- .should("have.attr", "tooltip", "Pin Header");
+ .should("have.prop", "accessibleName", "Pin Header")
+ .should("have.prop", "tooltip", "Pin Header");
- // Pin the header
cy.get("@dynamicPage")
.invoke("prop", "headerPinned", true);
- // After pinning, tooltip should change to "Unpin Header"
cy.get("@dynamicPage")
.shadow()
.find("[ui5-dynamic-page-header-actions]")
.shadow()
.find(".ui5-dynamic-page-header-action-pin")
- .should("have.attr", "tooltip", "Unpin Header");
+ .should("have.prop", "accessibleName", "Unpin Header")
+ .should("have.prop", "tooltip", "Unpin Header");
- // Unpin the header
cy.get("@dynamicPage")
.invoke("prop", "headerPinned", false);
- // After unpinning, tooltip should change back to "Pin Header"
cy.get("@dynamicPage")
.shadow()
.find("[ui5-dynamic-page-header-actions]")
.shadow()
.find(".ui5-dynamic-page-header-action-pin")
- .should("have.attr", "tooltip", "Pin Header");
+ .should("have.prop", "accessibleName", "Pin Header")
+ .should("have.prop", "tooltip", "Pin Header");
});
it("should use default aria-label based on header state", () => {
@@ -1478,4 +1476,105 @@ describe("ARIA attributes", () => {
.should("have.attr", "role", "contentinfo")
.should("have.attr", "aria-label", "Page Footer");
});
+});
+
+describe("Header actions DOM order and screen reader reading", () => {
+ it("renders header actions wrapper outside the banner landmark", () => {
+ cy.mount(
+
+
+ Page Title
+
+
+ Header Content
+
+ Content
+
+ );
+
+ cy.get("[ui5-dynamic-page]")
+ .shadow()
+ .find(".ui5-dynamic-page-title-header-wrapper .ui5-dynamic-page-header-actions-wrapper")
+ .should("not.exist");
+
+ cy.get("[ui5-dynamic-page]")
+ .shadow()
+ .find(".ui5-dynamic-page-scroll-container > .ui5-dynamic-page-header-actions-wrapper")
+ .should("exist");
+ });
+
+ it("renders header actions after the header slot in DOM order when header is expanded", () => {
+ cy.mount(
+
+
+ Page Title
+
+
+ Header Content
+
+ Content
+
+ );
+
+ cy.get("[ui5-dynamic-page]")
+ .shadow()
+ .find(".ui5-dynamic-page-scroll-container")
+ .then($container => {
+ const children = [...$container[0].children];
+ const headerSlotIdx = children.findIndex(el => el.nodeName === "SLOT" && el.getAttribute("name") === "headerArea");
+ const actionsIdx = children.findIndex(el => el.classList.contains("ui5-dynamic-page-header-actions-wrapper"));
+ expect(actionsIdx).to.be.greaterThan(headerSlotIdx);
+ });
+ });
+
+ it("applies sticky positioning to the actions wrapper when the header is snapped", () => {
+ cy.mount(
+
+
+ Page Title
+
+
+ Header Content
+
+ Content
+
+ );
+
+ cy.get("[ui5-dynamic-page]")
+ .shadow()
+ .find(".ui5-dynamic-page-header-actions-wrapper")
+ .should("not.have.attr", "style");
+
+ cy.get("[ui5-dynamic-page]")
+ .invoke("prop", "headerSnapped", true);
+
+ cy.get("[ui5-dynamic-page]")
+ .shadow()
+ .find(".ui5-dynamic-page-header-actions-wrapper")
+ .should("have.attr", "style")
+ .and("include", "--_ui5_dp_header_actions_top");
+ });
+
+ it("applies sticky positioning to the actions wrapper when the header is 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-header-actions-wrapper")
+ .should("have.attr", "style")
+ .and("include", "--_ui5_dp_header_actions_top");
+ });
});
\ No newline at end of file
diff --git a/packages/fiori/src/DynamicPage.ts b/packages/fiori/src/DynamicPage.ts
index 05c203029d215..69746b7d7e9da 100644
--- a/packages/fiori/src/DynamicPage.ts
+++ b/packages/fiori/src/DynamicPage.ts
@@ -358,6 +358,12 @@ class DynamicPage extends UI5Element {
return this.hidePinButton || isPhone();
}
+ get _actionsBarStickyTop(): number {
+ const titleHeight = this.dynamicPageTitle?.getBoundingClientRect().height || 0;
+ const headerHeight = this.headerInTitle ? (this.dynamicPageHeader?.getBoundingClientRect().height || 0) : 0;
+ return titleHeight + headerHeight;
+ }
+
/**
* Defines if the header is snapped.
*
diff --git a/packages/fiori/src/DynamicPageTemplate.tsx b/packages/fiori/src/DynamicPageTemplate.tsx
index 6ae5e33c31871..ab086dd62ef98 100644
--- a/packages/fiori/src/DynamicPageTemplate.tsx
+++ b/packages/fiori/src/DynamicPageTemplate.tsx
@@ -21,7 +21,6 @@ export default function DynamicPageTemplate(this: DynamicPage) {
name="headerArea"
>
}
- {this.actionsInTitle && headerActions.call(this)}
{this.headerInContent &&
@@ -31,7 +30,12 @@ export default function DynamicPageTemplate(this: DynamicPage) {
>
}
- {!this.actionsInTitle && headerActions.call(this)}
+