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(
);
+
+ 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();
+
+ 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();
+
+ 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();
+
+ 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"
>
-