From 0df2538ee4203b276c4c487a84a920a949b1961d Mon Sep 17 00:00:00 2001 From: ilhan007 Date: Fri, 31 Oct 2025 10:15:47 +0200 Subject: [PATCH 1/2] feat(ui5-form): add accessibleNameRef --- packages/main/cypress/specs/Form.cy.tsx | 29 +++++++++++++++++++++++++ packages/main/src/Form.ts | 16 +++++++++++--- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/packages/main/cypress/specs/Form.cy.tsx b/packages/main/cypress/specs/Form.cy.tsx index 8e167d940ae9..27d12823cf94 100644 --- a/packages/main/cypress/specs/Form.cy.tsx +++ b/packages/main/cypress/specs/Form.cy.tsx @@ -845,6 +845,35 @@ describe("Accessibility", () => { .should("have.attr", "aria-label", "basic form"); }); + it("tests 'aria-label' via 'accessibleNameRef'", () => { + cy.mount( + <> + basic form +
+ + + Red Point Stores + + + + @sap + + + + Red Point Stores + +
+ ); + + cy.get("[ui5-form]") + .as("form"); + + cy.get("@form") + .shadow() + .find(".ui5-form-root") + .should("have.attr", "aria-label", "basic form"); + }); + describe("FormGroup accessibility", () => { it("tests 'aria-label' default", () => { cy.mount(
diff --git a/packages/main/src/Form.ts b/packages/main/src/Form.ts index f3cabc7b2586..452ca6c4acb9 100644 --- a/packages/main/src/Form.ts +++ b/packages/main/src/Form.ts @@ -7,6 +7,7 @@ import slot from "@ui5/webcomponents-base/dist/decorators/slot.js"; import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; import type { AriaRole } from "@ui5/webcomponents-base"; import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js"; +import { getEffectiveAriaLabelText } from "@ui5/webcomponents-base/dist/util/AccessibilityTextsHelper.js"; // Template import FormTemplate from "./FormTemplate.js"; @@ -229,6 +230,15 @@ class Form extends UI5Element { @property() accessibleName?: string; + /** + * Defines id (or many ids) of the element (or elements) that label the component. + * @default undefined + * @public + * @since 2.16.0 + */ + @property() + accessibleNameRef?: string; + /** * Defines the accessibility mode of the component in "edit" and "display" use-cases. * @@ -569,15 +579,15 @@ class Form extends UI5Element { } get effectiveAccessibleName() { - if (this.accessibleName) { - return this.accessibleName; + if (this.accessibleName || this.accessibleNameRef) { + return getEffectiveAriaLabelText(this); } return this.hasHeader ? undefined : Form.i18nBundle.getText(FORM_ACCESSIBLE_NAME); } get effectiveАccessibleNameRef(): string | undefined { - if (this.accessibleName) { + if (this.accessibleName || this.accessibleNameRef) { return; } From c59cd947232bfbe346fe3dc66d7f16df1675841d Mon Sep 17 00:00:00 2001 From: ilhan007 Date: Fri, 31 Oct 2025 11:29:12 +0200 Subject: [PATCH 2/2] feat(ui5-form-group): add accessibleNameRef --- packages/main/cypress/specs/Form.cy.tsx | 104 ++++++++++++++++++------ packages/main/src/FormGroup.ts | 17 +++- 2 files changed, 92 insertions(+), 29 deletions(-) diff --git a/packages/main/cypress/specs/Form.cy.tsx b/packages/main/cypress/specs/Form.cy.tsx index 27d12823cf94..6cacf0a98b39 100644 --- a/packages/main/cypress/specs/Form.cy.tsx +++ b/packages/main/cypress/specs/Form.cy.tsx @@ -895,7 +895,7 @@ describe("Accessibility", () => { .should("have.attr", "aria-label", Form.i18nBundle.getText(FORM_GROUP_ACCESSIBLE_NAME, "1")); }); - it("tests 'aria-label' via accessible-name", () => { + it("tests 'aria-label', 'aria-labelledby' via 'accessibleName'", () => { const EXPECTED_LABEL = "Custom group label"; cy.mount( @@ -913,10 +913,38 @@ describe("Accessibility", () => { .shadow() .find(".ui5-form-group-layout") .eq(0) - .should("have.attr", "aria-label", EXPECTED_LABEL); + // 'aria-label' is rendered in Shadow DOM when accessibleName or accessibleNameRef is set + .should("have.attr", "aria-label", EXPECTED_LABEL) + .should("not.have.attr", "aria-labelledby"); }); - it("tests 'aria-labelledby' via header-text", () => { + it("tests 'aria-label', 'aria-labelledby' via 'accessibleNameRef'", () => { + const EXPECTED_LABEL = "Custom group label"; + cy.mount(<> + {EXPECTED_LABEL} + + + + + Red Point Stores + + + + ); + + cy.get("[ui5-form]") + .as("form"); + + cy.get("@form") + .shadow() + .find(".ui5-form-group-layout") + .eq(0) + // 'aria-label' is rendered in Shadow DOM when accessibleName or accessibleNameRef is set + .should("have.attr", "aria-label", EXPECTED_LABEL) + .should("not.have.attr", "aria-labelledby"); + }); + + it("tests 'aria-label', 'aria-labelledby' when 'headerText' present", () => { cy.mount(
@@ -943,48 +971,70 @@ describe("Accessibility", () => { .find(".ui5-form-group-heading [ui5-title]") .invoke("attr", "id") .should(id => { + // aria-labelledby is used pointing to the header title ID expect(ariaLabelledBy).to.equal(id); }); }); + // no 'aria-label' when headerText is present, aria-labelledby is used instead cy.get("@group") .should("not.have.attr", "aria-label"); }); - it("tests 'aria-label' via accessible-name and header-text", () => { + it("tests 'aria-label', 'aria-labelledby' via 'accessibleName' when 'headerText' present", () => { const EXPECTED_LABEL = "Custom group header"; - cy.mount( - - - - Red Point Stores - - - ); + cy.mount( +
+ + + + Red Point Stores + + +
+ ); cy.get("[ui5-form]") .as("form"); + // accessibleName has higher priority than headerText cy.get("@form") .shadow() .find(".ui5-form-group-layout") .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); - }); - }); + // 'aria-label' is rendered in Shadow DOM when accessibleName or accessibleNameRef is set + .should("have.attr", "aria-label", EXPECTED_LABEL) + .should("not.have.attr", "aria-labelledby"); + }); - cy.get("@group") - .should("have.attr", "aria-label", EXPECTED_LABEL); + it("tests 'aria-label', 'aria-labelledby' via 'accessibleNameRef' when 'headerText' present", () => { + const EXPECTED_LABEL = "Custom group header"; + cy.mount( + <> + {EXPECTED_LABEL} +
+ + + + + Red Point Stores + + +
+ + ); + + cy.get("[ui5-form]") + .as("form"); + + // accessibleNameReg has higher priority than headerText + cy.get("@form") + .shadow() + .find(".ui5-form-group-layout") + .eq(0) + // 'aria-label' is rendered in Shadow DOM when accessibleName or accessibleNameRef is set + .should("have.attr", "aria-label", EXPECTED_LABEL) + .should("not.have.attr", "aria-labelledby"); }); }); diff --git a/packages/main/src/FormGroup.ts b/packages/main/src/FormGroup.ts index 3e9625f8859a..54e7362a54d4 100644 --- a/packages/main/src/FormGroup.ts +++ b/packages/main/src/FormGroup.ts @@ -4,6 +4,7 @@ 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"; +import { getEffectiveAriaLabelText } from "@ui5/webcomponents-base/dist/util/AccessibilityTextsHelper.js"; import type FormItem from "./FormItem.js"; import type { IFormItem } from "./Form.js"; @@ -81,6 +82,15 @@ class FormGroup extends UI5Element implements IFormItem { @property() accessibleName?: string; + /** + * Defines id (or many ids) of the element (or elements) that label the component. + * @default undefined + * @public + * @since 2.16.0 + */ + @property() + accessibleNameRef?: string; + /** * Defines the items of the component. * @public @@ -123,8 +133,8 @@ class FormGroup extends UI5Element implements IFormItem { } getEffectiveAccessibleName(index: number) { - if (this.accessibleName) { - return this.accessibleName; + if (this.accessibleName || this.accessibleNameRef) { + return getEffectiveAriaLabelText(this); } if (this.headerText) { @@ -135,6 +145,9 @@ class FormGroup extends UI5Element implements IFormItem { } get effectiveAccessibleNameRef() { + if (this.accessibleName || this.accessibleNameRef) { + return undefined; + } return this.headerText ? `${this._id}-group-header-text` : undefined; }