Skip to content
Open
100 changes: 98 additions & 2 deletions packages/fiori/cypress/specs/DynamicPage.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]")
Expand All @@ -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]")
Expand Down Expand Up @@ -1121,4 +1121,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(
<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", "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");
Comment thread
yanaminkova marked this conversation as resolved.
});

it("supports customizing headerContent label via accessibleName on DynamicPageHeader", () => {
cy.mount(
<DynamicPage style={{ height: "600px" }}>
<DynamicPageTitle slot="titleArea">
<div slot="heading">Page Title</div>
</DynamicPageTitle>
<DynamicPageHeader slot="headerArea" accessibleName="Custom Region Label">
<div>Header Content</div>
</DynamicPageHeader>
<div style={{ height: "1000px" }}>Content</div>
</DynamicPage>
);

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

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");
});
});
66 changes: 65 additions & 1 deletion packages/fiori/src/DynamicPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -32,6 +33,29 @@ import {

import type { Slot, DefaultSlot } from "@ui5/webcomponents-base/dist/UI5Element.js";

type DynamicPageHeaderRoles = Extract<AriaLandmarkRole, "none" | "banner" | "region">;
type DynamicPageContentRoles = Extract<AriaLandmarkRole, "none" | "main" | "region" | "form">;
type DynamicPageFooterRoles = Extract<AriaLandmarkRole, "none" | "contentinfo" | "region">;
type DynamicPageRootRoles = Extract<AriaLandmarkRole, "none" | "main" | "region">;
type DynamicPageAccessibilityAttributes = {
Comment thread
yanaminkova marked this conversation as resolved.
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
/**
Expand Down Expand Up @@ -184,6 +208,36 @@ class DynamicPage extends UI5Element {
@slot({ type: HTMLElement })
footerArea!: Slot<HTMLElement>;

/**
* 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;

Expand Down Expand Up @@ -281,9 +335,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();
}
Expand Down Expand Up @@ -480,3 +542,5 @@ class DynamicPage extends UI5Element {
DynamicPage.define();

export default DynamicPage;

export type { DynamicPageAccessibilityAttributes };
13 changes: 13 additions & 0 deletions packages/fiori/src/DynamicPageHeader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand Down
12 changes: 7 additions & 5 deletions packages/fiori/src/DynamicPageTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import DynamicPageHeaderActions from "./DynamicPageHeaderActions.js";

export default function DynamicPageTemplate(this: DynamicPage) {
return (
<div class="ui5-dynamic-page-root">
<div class="ui5-dynamic-page-root" role={this._rootRole} aria-label={this._rootAriaLabel}>
<div class="ui5-dynamic-page-scroll-container"
onScroll={this.snapOnScroll}
>
<header
<div
class="ui5-dynamic-page-title-header-wrapper"
id={`${this._id}-header`}
role={this._headerRole || "banner"}
aria-label={this.headerAriaLabel}
onui5-toggle-title={this.onToggleTitle}
>
Expand All @@ -20,9 +21,8 @@ export default function DynamicPageTemplate(this: DynamicPage) {
name="headerArea"
></slot>
}

{this.actionsInTitle && headerActions.call(this)}
</header>
</div>

{this.headerInContent &&
<slot tabIndex={this.headerTabIndex}
Expand All @@ -36,6 +36,8 @@ export default function DynamicPageTemplate(this: DynamicPage) {
<div
part="content"
class="ui5-dynamic-page-content"
role={this._contentRole}
aria-label={this._contentAriaLabel}
onFocusIn={this.onContentFocusIn}
onFocusOut={this.onContentFocusOut}
>
Expand All @@ -48,7 +50,7 @@ export default function DynamicPageTemplate(this: DynamicPage) {
</div>
</div>

<div class="ui5-dynamic-page-footer" part="footer">
<div class="ui5-dynamic-page-footer" part="footer" role={this._footerRole} aria-label={this._footerAriaLabel}>
<slot name="footerArea"></slot>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions packages/fiori/test/pages/DynamicPage.html
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@
const cancelEdit = document.querySelector("#cancel-edit");
const saveEdit = document.querySelector("#save-edit");


editButton.addEventListener("click", () => {
dynamicPage.setAttribute("show-footer", true);
});
Expand Down
Loading