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)} +
+ {headerActions.call(this)} +