diff --git a/packages/fiori/cypress/specs/DynamicPage.cy.tsx b/packages/fiori/cypress/specs/DynamicPage.cy.tsx index 6e02f410df0c..ad17d50ada99 100644 --- a/packages/fiori/cypress/specs/DynamicPage.cy.tsx +++ b/packages/fiori/cypress/specs/DynamicPage.cy.tsx @@ -165,7 +165,7 @@ describe("DynamicPage", () => { cy.get("[ui5-dynamic-page]") .shadow() - .find("header.ui5-dynamic-page-title-header-wrapper > slot[name=headerArea]") + .find("div.ui5-dynamic-page-title-header-wrapper > slot[name=headerArea]") .should("not.exist"); cy.get("[ui5-dynamic-page]") @@ -178,7 +178,7 @@ describe("DynamicPage", () => { cy.get("[ui5-dynamic-page]") .shadow() - .find("header.ui5-dynamic-page-title-header-wrapper > slot[name=headerArea]") + .find("div.ui5-dynamic-page-title-header-wrapper > slot[name=headerArea]") .should("exist"); cy.get("[ui5-dynamic-page]") @@ -1276,4 +1276,100 @@ 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 accessibleName on DynamicPageHeader", () => { + cy.mount( + + +
Page Title
+
+ +
Header Content
+
+
Content
+
+ ); + + cy.get("[ui5-dynamic-page-header]") + .shadow() + .find(".ui5-dynamic-page-header-root") + .should("have.attr", "aria-label", "Custom Region Label"); + }); + + it("renders default banner role when only header.name is set", () => { + cy.mount( + + +
Page Title
+
+ +
Header Content
+
+
Content
+
+ ); + + cy.get("[ui5-dynamic-page]").invoke("prop", "accessibilityAttributes", { + header: { name: "Custom Header Label" }, + }); + + cy.get("[ui5-dynamic-page]") + .shadow() + .find("div.ui5-dynamic-page-title-header-wrapper") + .should("exist") + .should("have.attr", "role", "banner") + .should("have.attr", "aria-label", "Custom Header 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 224436a813f9..39a57a36786f 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,29 @@ import { import type { Slot, DefaultSlot } from "@ui5/webcomponents-base/dist/UI5Element.js"; +type DynamicPageHeaderRoles = Extract; +type DynamicPageContentRoles = Extract; +type DynamicPageFooterRoles = Extract; +type DynamicPageRootRoles = Extract; +type DynamicPageAccessibilityAttributes = { + root?: { + role?: DynamicPageRootRoles, + name?: string, + }, + header?: { + role?: DynamicPageHeaderRoles, + name?: string, + }, + content?: { + role?: DynamicPageContentRoles, + name?: string, + }, + footer?: { + role?: DynamicPageFooterRoles, + name?: string, + }, +}; + const SCROLL_DEBOUNCE_RATE = 5; // ms const SCROLL_THRESHOLD = 10; // px /** @@ -184,6 +208,36 @@ class DynamicPage extends UI5Element { @slot({ type: HTMLElement }) footerArea!: Slot; + /** + * Defines additional accessibility attributes on different areas of the component. + * + * The accessibilityAttributes object has the following fields, + * where each field is an object supporting one or more accessibility attributes: + * + * - **root**: `root.role` and `root.name`. + * - **header**: `header.role` and `header.name`. + * - **content**: `content.role` and `content.name`. + * - **footer**: `footer.role` and `footer.name`. + * + * The accessibility attributes support the following values: + * + * - **role**: Defines the accessible ARIA landmark role of the area. + * Accepts the following values per section: + * `root` — `none`, `main`, `region`; + * `header` — `none`, `banner`, `region`; + * `content` — `none`, `main`, `region`, `form`; + * `footer` — `none`, `contentinfo`, `region`. + * + * - **name**: Defines the accessible ARIA name of the area. + * Accepts any string. + * + * @default {} + * @public + * @since 2.23.0 + */ + @property({ type: Object }) + accessibilityAttributes: DynamicPageAccessibilityAttributes = {}; + @i18n("@ui5/webcomponents-fiori") static i18nBundle: I18nBundle; @@ -289,9 +343,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(); } @@ -489,3 +551,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..b43e315d0dc6 100644 --- a/packages/fiori/src/DynamicPageHeader.ts +++ b/packages/fiori/src/DynamicPageHeader.ts @@ -75,6 +75,16 @@ class DynamicPageHeader extends UI5Element { @property({ type: Boolean }) _snapped = false; + /** + * Defines the accessible ARIA label for the header region. + * Overrides the default "Header Expanded" / "Header Snapped" text. + * @public + * @default undefined + * @since 2.23.0 + */ + @property() + accessibleName?: string; + @i18n("@ui5/webcomponents-fiori") static i18nBundle: I18nBundle; @@ -83,6 +93,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..6ae5e33c3187 100644 --- a/packages/fiori/src/DynamicPageTemplate.tsx +++ b/packages/fiori/src/DynamicPageTemplate.tsx @@ -3,13 +3,14 @@ import DynamicPageHeaderActions from "./DynamicPageHeaderActions.js"; export default function DynamicPageTemplate(this: DynamicPage) { return ( -
+
-
@@ -20,9 +21,8 @@ export default function DynamicPageTemplate(this: DynamicPage) { name="headerArea" > } - {this.actionsInTitle && headerActions.call(this)} -
+
{this.headerInContent && @@ -48,7 +50,7 @@ export default function DynamicPageTemplate(this: DynamicPage) {
- diff --git a/packages/fiori/test/pages/DynamicPage.html b/packages/fiori/test/pages/DynamicPage.html index 7012c33409dd..d819a3f62a1e 100644 --- a/packages/fiori/test/pages/DynamicPage.html +++ b/packages/fiori/test/pages/DynamicPage.html @@ -183,6 +183,7 @@ const cancelEdit = document.querySelector("#cancel-edit"); const saveEdit = document.querySelector("#save-edit"); + editButton.addEventListener("click", () => { dynamicPage.setAttribute("show-footer", true); });