diff --git a/packages/main/cypress/specs/Form.cy.tsx b/packages/main/cypress/specs/Form.cy.tsx index 589e1066f423..cfc850db16bb 100644 --- a/packages/main/cypress/specs/Form.cy.tsx +++ b/packages/main/cypress/specs/Form.cy.tsx @@ -4,8 +4,8 @@ import FormItem from "../../src/FormItem.js"; import FormGroup from "../../src/FormGroup.js"; import Label from "../../src/Label.js"; import Text from "../../src/Text.js"; -import Title from "../../src/Title.js"; import Input from "../../src/Input.js"; +import { FORM_GROUP_ACCESSIBLE_NAME } from "../../src/generated/i18n/i18n-defaults.js"; describe("General API", () => { it("tests calculated state of Form with default layout, label-span and empty-span", () => { @@ -845,6 +845,120 @@ describe("Accessibility", () => { .should("have.attr", "aria-label", "basic form"); }); + describe("FormGroup accessibility", () => { + it("tests 'aria-label' default", () => { + cy.mount(
+ + + + Red Point Stores + + +
); + + cy.get("[ui5-form]") + .as("form"); + + cy.get("@form") + .shadow() + .find(".ui5-form-group") + .eq(0) + .should("have.attr", "aria-label", Form.i18nBundle.getText(FORM_GROUP_ACCESSIBLE_NAME, "1")); + }); + + it("tests 'aria-label' via accessible-name", () => { + const EXPECTED_LABEL = "Custom group label"; + cy.mount(
+ + + + Red Point Stores + + +
); + + cy.get("[ui5-form]") + .as("form"); + + cy.get("@form") + .shadow() + .find(".ui5-form-group") + .eq(0) + .should("have.attr", "aria-label", EXPECTED_LABEL); + }); + + it("tests 'aria-labelledby' via header-text", () => { + cy.mount(
+ + + + Red Point Stores + + +
); + + cy.get("[ui5-form]") + .as("form"); + + cy.get("@form") + .shadow() + .find(".ui5-form-group") + .eq(0) + .as("group") + .invoke("attr", "aria-labelledby") + .then(ariaLabelledBy => { + cy.get("@form") + .shadow() + .find(".ui5-form-group") + .eq(0) + .find(".ui5-form-group-heading [ui5-title]") + .invoke("attr", "id") + .should(id => { + expect(ariaLabelledBy).to.equal(id); + }); + }); + + cy.get("@group") + .should("not.have.attr", "aria-label"); + }); + + it("tests 'aria-label' via accessible-name and header-text", () => { + const EXPECTED_LABEL = "Custom group header"; + cy.mount(
+ + + + Red Point Stores + + +
); + + cy.get("[ui5-form]") + .as("form"); + + cy.get("@form") + .shadow() + .find(".ui5-form-group") + .eq(0) + .as("group") + .invoke("attr", "aria-labelledby") + .then(ariaLabelledBy => { + cy.get("@form") + .shadow() + .find(".ui5-form-group") + .eq(0) + .find(".ui5-form-group-heading [ui5-title]") + .invoke("attr", "id") + .should(id => { + expect(ariaLabelledBy).to.equal(id); + }); + }); + + cy.get("@group") + .should("have.attr", "aria-label", EXPECTED_LABEL); + }); + }); + it("tests F6 navigation", () => { cy.mount( <> diff --git a/packages/main/src/Form.ts b/packages/main/src/Form.ts index c40d94db8e2a..4eeb5f5404fb 100644 --- a/packages/main/src/Form.ts +++ b/packages/main/src/Form.ts @@ -52,7 +52,8 @@ interface IFormItem extends UI5Element { type GroupItemsInfo = { groupItem: IFormItem, items: Array, - accessibleNameRef: string | undefined + accessibleNameRef: string | undefined, + accessibleName: string | undefined, } type ItemsInfo = { @@ -558,7 +559,7 @@ class Form extends UI5Element { } get groupItemsInfo(): Array { - return this.items.map((groupItem: IFormItem) => { + return this.items.map((groupItem: IFormItem, index: number) => { const items = this.getItemsInfo((Array.from(groupItem.children) as Array)); breakpoints.forEach(breakpoint => { const cols = ((groupItem[`cols${breakpoint}` as keyof IFormItem]) as number || 1); @@ -583,7 +584,8 @@ class Form extends UI5Element { return { groupItem, - accessibleNameRef: (groupItem as FormGroup).headerText ? `${groupItem._id}-group-header-text` : undefined, + accessibleName: (groupItem as FormGroup).getEffectiveAccessibleName(index), + accessibleNameRef: (groupItem as FormGroup).effectiveАccessibleNameRef, items: this.getItemsInfo((Array.from(groupItem.children) as Array)), }; }); diff --git a/packages/main/src/FormGroup.ts b/packages/main/src/FormGroup.ts index 1c2e8a352632..a1de80036f66 100644 --- a/packages/main/src/FormGroup.ts +++ b/packages/main/src/FormGroup.ts @@ -1,4 +1,6 @@ import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; +import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js"; +import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js"; import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; import property from "@ui5/webcomponents-base/dist/decorators/property.js"; import slot from "@ui5/webcomponents-base/dist/decorators/slot.js"; @@ -8,6 +10,8 @@ import type { IFormItem } from "./Form.js"; import type FormItemSpacing from "./types/FormItemSpacing.js"; import type TitleLevel from "./types/TitleLevel.js"; +import { FORM_GROUP_ACCESSIBLE_NAME } from "./generated/i18n/i18n-defaults.js"; + /** * @class * @@ -68,6 +72,15 @@ class FormGroup extends UI5Element implements IFormItem { @property({ type: Number }) columnSpan?: number; + /** + * Defines the accessible ARIA name of the component. + * @default undefined + * @public + * @since 2.16.0 + */ + @property() + accessibleName?: string; + /** * Defines the items of the component. * @public @@ -96,6 +109,9 @@ class FormGroup extends UI5Element implements IFormItem { @property() itemSpacing: `${FormItemSpacing}` = "Normal"; + @i18n("@ui5/webcomponents") + static i18nBundle: I18nBundle; + onBeforeRendering() { this.processFormItems(); } @@ -106,6 +122,22 @@ class FormGroup extends UI5Element implements IFormItem { }); } + getEffectiveAccessibleName(index: number) { + if (this.accessibleName) { + return this.accessibleName; + } + + if (this.headerText) { + return undefined; + } + + return FormGroup.i18nBundle.getText(FORM_GROUP_ACCESSIBLE_NAME, index + 1); + } + + get effectiveАccessibleNameRef() { + return this.headerText ? `${this._id}-group-header-text` : undefined; + } + get isGroup() { return true; } diff --git a/packages/main/src/FormTemplate.tsx b/packages/main/src/FormTemplate.tsx index a666bab91a29..69e1753e439f 100644 --- a/packages/main/src/FormTemplate.tsx +++ b/packages/main/src/FormTemplate.tsx @@ -36,7 +36,7 @@ export default function FormTemplate(this: Form) { }} part="column" > -
+
{groupItem.headerText &&
{groupItem.headerText} diff --git a/packages/main/src/i18n/messagebundle.properties b/packages/main/src/i18n/messagebundle.properties index 9900e7b81b22..8557c6c3f258 100644 --- a/packages/main/src/i18n/messagebundle.properties +++ b/packages/main/src/i18n/messagebundle.properties @@ -708,6 +708,9 @@ TOOLBAR_POPOVER_AVAILABLE_VALUES=Available Values #XACT: ARIA announcement for the Form aria-label attribute FORM_ACCESSIBLE_NAME=Form +#XACT: ARIA announcement for the Form group aria-label attribute +FORM_GROUP_ACCESSIBLE_NAME=Group {0} + #XMSG: Text used for reporting that a radio button group requires one of the radio buttons to be checked FORM_CHECKABLE_REQUIRED=Please tick this box if you want to proceed.