diff --git a/packages/fiori/cypress/specs/DynamicPage.cy.tsx b/packages/fiori/cypress/specs/DynamicPage.cy.tsx index 9fc6b28a869f..40a711d0c86d 100644 --- a/packages/fiori/cypress/specs/DynamicPage.cy.tsx +++ b/packages/fiori/cypress/specs/DynamicPage.cy.tsx @@ -1121,4 +1121,79 @@ describe("ARIA attributes", () => { .find(".ui5-dynamic-page-header-root") .should("have.attr", "aria-label", "Header Expanded"); }); + + it("supports customizing header role and label via accessibilityAttributes", () => { + cy.mount( + + +
Page Title
+
+ +
Header Content
+
+
Content
+
+ ); + + cy.get("[ui5-dynamic-page]").invoke("prop", "accessibilityAttributes", { + header: { role: "none", name: "Custom Header" }, + }); + + cy.get("[ui5-dynamic-page]") + .shadow() + .find(".ui5-dynamic-page-title-header-wrapper") + .should("have.attr", "role", "none") + .should("have.attr", "aria-label", "Custom Header"); + }); + + it("supports customizing headerContent label via accessibilityAttributes", () => { + cy.mount( + + +
Page Title
+
+ +
Header Content
+
+
Content
+
+ ); + + cy.get("[ui5-dynamic-page]").invoke("prop", "accessibilityAttributes", { + headerContent: { name: "Custom Region Label" }, + }); + + cy.get("[ui5-dynamic-page-header]") + .shadow() + .find(".ui5-dynamic-page-header-root") + .should("have.attr", "aria-label", "Custom Region Label"); + }); + + it("supports customizing content and footer roles via accessibilityAttributes", () => { + cy.mount( + + +
Page Title
+
+
Content
+
+ ); + + cy.get("[ui5-dynamic-page]").invoke("prop", "accessibilityAttributes", { + content: { role: "main", name: "Page Content" }, + footer: { role: "contentinfo", name: "Page Footer" }, + }); + + cy.get("[ui5-dynamic-page]") + .shadow() + .find(".ui5-dynamic-page-content") + .should("have.attr", "role", "main") + .should("have.attr", "aria-label", "Page Content"); + + cy.get("[ui5-dynamic-page]") + .shadow() + .find(".ui5-dynamic-page-footer") + .should("have.attr", "role", "contentinfo") + .should("have.attr", "aria-label", "Page Footer"); + }); }); \ No newline at end of file diff --git a/packages/fiori/src/DynamicPage.ts b/packages/fiori/src/DynamicPage.ts index c3aab7f13465..8bb1a0eb0fe9 100644 --- a/packages/fiori/src/DynamicPage.ts +++ b/packages/fiori/src/DynamicPage.ts @@ -10,6 +10,7 @@ import { renderFinished } from "@ui5/webcomponents-base/dist/Render.js"; import announce from "@ui5/webcomponents-base/dist/util/InvisibleMessage.js"; import InvisibleMessageMode from "@ui5/webcomponents-base/dist/types/InvisibleMessageMode.js"; import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js"; +import type { AriaLandmarkRole } from "@ui5/webcomponents-base"; import { isPhone } from "@ui5/webcomponents-base/dist/Device.js"; import debounce from "@ui5/webcomponents-base/dist/util/debounce.js"; @@ -32,6 +33,14 @@ import { import type { Slot, DefaultSlot } from "@ui5/webcomponents-base/dist/UI5Element.js"; +type DynamicPageAccessibilityAttributes = { + root?: { role?: AriaLandmarkRole; name?: string }; + header?: { role?: AriaLandmarkRole; name?: string }; + headerContent?: { name?: string }; + content?: { role?: AriaLandmarkRole; name?: string }; + footer?: { role?: AriaLandmarkRole; name?: string }; +}; + const SCROLL_DEBOUNCE_RATE = 5; // ms const SCROLL_THRESHOLD = 10; // px /** @@ -184,6 +193,23 @@ class DynamicPage extends UI5Element { @slot({ type: HTMLElement }) footerArea!: Slot; + /** + * Defines the accessibility attributes for DynamicPage sections. + * + * Accepted fields per section — `root`, `header`, `content`, `footer`: + * - `role`: Overrides the ARIA landmark role. Accepts the following string values: `"none"`, `"banner"`, `"main"`, `"region"`, `"navigation"`, `"search"`, `"complementary"`, `"form"`, `"contentinfo"`. + * - `name`: Sets `aria-label` on the section. Accepts any string. + * + * Accepted fields for `headerContent`: + * - `name`: Sets `aria-label` on the DynamicPageHeader region (overrides the default "Header Expanded"/"Header Snapped" text). Accepts any string. + * + * @public + * @since 2.23.0 + * @default {} + */ + @property({ type: Object }) + accessibilityAttributes: DynamicPageAccessibilityAttributes = {}; + @i18n("@ui5/webcomponents-fiori") static i18nBundle: I18nBundle; @@ -213,6 +239,7 @@ class DynamicPage extends UI5Element { } if (this.dynamicPageHeader) { this.dynamicPageHeader._snapped = this._headerSnapped; + this.dynamicPageHeader._accessibleName = this.accessibilityAttributes.headerContent?.name; } } @@ -281,9 +308,17 @@ class DynamicPage extends UI5Element { } get headerAriaLabel() { - return this.hasHeading ? this._headerLabel : undefined; + return this.accessibilityAttributes.header?.name || (this.hasHeading ? this._headerLabel : undefined); } + get _headerRole() { return this.accessibilityAttributes.header?.role; } + get _rootRole() { return this.accessibilityAttributes.root?.role; } + get _rootAriaLabel() { return this.accessibilityAttributes.root?.name; } + get _contentRole() { return this.accessibilityAttributes.content?.role; } + get _contentAriaLabel() { return this.accessibilityAttributes.content?.name; } + get _footerRole() { return this.accessibilityAttributes.footer?.role; } + get _footerAriaLabel() { return this.accessibilityAttributes.footer?.name; } + get _hidePinButton() { return this.hidePinButton || isPhone(); } @@ -480,3 +515,5 @@ class DynamicPage extends UI5Element { DynamicPage.define(); export default DynamicPage; + +export type { DynamicPageAccessibilityAttributes }; diff --git a/packages/fiori/src/DynamicPageHeader.ts b/packages/fiori/src/DynamicPageHeader.ts index 0bb6b4fc1e47..0daab2bc3eda 100644 --- a/packages/fiori/src/DynamicPageHeader.ts +++ b/packages/fiori/src/DynamicPageHeader.ts @@ -75,6 +75,12 @@ class DynamicPageHeader extends UI5Element { @property({ type: Boolean }) _snapped = false; + /** + * @private + */ + @property() + _accessibleName?: string; + @i18n("@ui5/webcomponents-fiori") static i18nBundle: I18nBundle; @@ -83,6 +89,9 @@ class DynamicPageHeader extends UI5Element { * @internal */ get _headerRegionAriaLabel(): string { + if (this._accessibleName) { + return this._accessibleName; + } const defaultText = this._snapped ? DYNAMIC_PAGE_ARIA_LABEL_SNAPPED_HEADER : DYNAMIC_PAGE_ARIA_LABEL_EXPANDED_HEADER; diff --git a/packages/fiori/src/DynamicPageTemplate.tsx b/packages/fiori/src/DynamicPageTemplate.tsx index 91c1e81f320c..cf87b7d4f00c 100644 --- a/packages/fiori/src/DynamicPageTemplate.tsx +++ b/packages/fiori/src/DynamicPageTemplate.tsx @@ -3,26 +3,11 @@ import DynamicPageHeaderActions from "./DynamicPageHeaderActions.js"; export default function DynamicPageTemplate(this: DynamicPage) { return ( -
+
-
- - {this.headerInTitle && - - } - - {this.actionsInTitle && headerActions.call(this)} -
+ {titleHeaderWrapper.call(this)} {this.headerInContent && @@ -48,13 +35,47 @@ export default function DynamicPageTemplate(this: DynamicPage) {
- ); } +function titleHeaderWrapper(this: DynamicPage) { + const commonProps = { + "class": "ui5-dynamic-page-title-header-wrapper", + id: `${this._id}-header`, + "aria-label": this.headerAriaLabel, + "onui5-toggle-title": this.onToggleTitle, + }; + + return this._headerRole ? ( +
+ {titleHeaderContent.call(this)} +
+ ) : ( +
+ {titleHeaderContent.call(this)} +
+ ); +} + +function titleHeaderContent(this: DynamicPage) { + return ( + <> + + {this.headerInTitle && + + } + {this.actionsInTitle && headerActions.call(this)} + + ); +} + function headerActions(this: DynamicPage) { if (!this.hasSnappedTitleOnMobile && this.hasHeading) { return ( diff --git a/packages/fiori/test/pages/DynamicPage.html b/packages/fiori/test/pages/DynamicPage.html index 7012c33409dd..ffd3316ac67b 100644 --- a/packages/fiori/test/pages/DynamicPage.html +++ b/packages/fiori/test/pages/DynamicPage.html @@ -183,6 +183,11 @@ const cancelEdit = document.querySelector("#cancel-edit"); const saveEdit = document.querySelector("#save-edit"); + dynamicPage.accessibilityAttributes = { + header: { role: "none" }, + content: { role: "main" }, + }; + editButton.addEventListener("click", () => { dynamicPage.setAttribute("show-footer", true); });