diff --git a/packages/main/cypress/specs/Select.cy.tsx b/packages/main/cypress/specs/Select.cy.tsx index 98663ddf2215..59ec17dd59e6 100644 --- a/packages/main/cypress/specs/Select.cy.tsx +++ b/packages/main/cypress/specs/Select.cy.tsx @@ -262,6 +262,155 @@ describe("Select - Accessibility", () => { .should("have.attr", "aria-expanded", "false") .should("have.attr", "aria-roledescription", EXPECTED_ARIA_ROLEDESCRIPTION); }); + + it("tests Select with valueState Positive and aria-describedby", () => { + cy.mount( + + ); + + // Test that valueState creates the correct aria-describedby reference + cy.get("[ui5-select]") + .shadow() + .find(".ui5-select-label-root") + .should("have.attr", "aria-describedby") + .and("contain", "-valueStateDesc"); + + // Test that the value state description text contains "Success" + cy.get("[ui5-select]") + .shadow() + .find("[id$='-valueStateDesc']") + .should("contain.text", "Success"); + }); + + it("tests accessibleDescription and accessibleDescriptionRef", () => { + cy.mount( + <> + Description text + + + + ); + + const EXPECTED_DESCRIPTION = "Select description"; + const EXPECTED_DESCRIPTION_REF = "Description text"; + + // Test first select with accessibleDescription + cy.get("#selectWithAccessibleDescription") + .shadow() + .find("#accessibleDescription") + .should("have.text", EXPECTED_DESCRIPTION); + + cy.get("#selectWithAccessibleDescription") + .shadow() + .find(".ui5-select-label-root") + .should("have.attr", "aria-describedby") + .and("contain", "accessibleDescription"); + + // Test second select with accessibleDescriptionRef + cy.get("#selectWithAccessibleDescriptionRef") + .shadow() + .find("#accessibleDescription") + .should("have.text", EXPECTED_DESCRIPTION_REF); + + cy.get("#selectWithAccessibleDescriptionRef") + .shadow() + .find(".ui5-select-label-root") + .should("have.attr", "aria-describedby") + .and("contain", "accessibleDescription"); + + // Test that changing the referenced element updates the description + cy.get("#descText") + .invoke("text", "Updated description text"); + + cy.get("#selectWithAccessibleDescriptionRef") + .shadow() + .find("#accessibleDescription") + .should("have.text", "Updated description text"); + }); + + it("tests Select with both valueState Positive and accessibleDescription", () => { + cy.mount( + + ); + + const EXPECTED_VALUE_STATE_TEXT = "Success"; + const EXPECTED_DESCRIPTION = "Additional description"; + + // Test that both valueState and accessibleDescription are included in aria-describedby + cy.get("[ui5-select]") + .shadow() + .find(".ui5-select-label-root") + .should("have.attr", "aria-describedby") + .and("contain", "-valueStateDesc") + .and("contain", "accessibleDescription"); + + // Test that the value state description text is correct + cy.get("[ui5-select]") + .shadow() + .find("[id$='-valueStateDesc']") + .should("contain.text", EXPECTED_VALUE_STATE_TEXT); + + // Test that the accessible description text is correct + cy.get("[ui5-select]") + .shadow() + .find("#accessibleDescription") + .should("have.text", EXPECTED_DESCRIPTION); + }); + + it("tests Select with multiple accessibleDescriptionRef values", () => { + cy.mount( + <> + First description + Second description + Third description + + + ); + + const EXPECTED_COMBINED_DESCRIPTION = "First description Second description Third description"; + + // Test that accessibleDescriptionRef with multiple IDs creates the correct aria-describedby reference + cy.get("[ui5-select]") + .shadow() + .find(".ui5-select-label-root") + .should("have.attr", "aria-describedby") + .and("contain", "accessibleDescription"); + + // Test that the combined description text from multiple elements is correct + cy.get("[ui5-select]") + .shadow() + .find("#accessibleDescription") + .should("have.text", EXPECTED_COMBINED_DESCRIPTION); + + // Test that changing one of the referenced elements updates the combined description + cy.get("#desc2") + .invoke("text", "Updated second description"); + + cy.get("[ui5-select]") + .shadow() + .find("#accessibleDescription") + .should("have.text", "First description Updated second description Third description"); + }); }); describe("Select - Popover", () => { diff --git a/packages/main/src/Select.ts b/packages/main/src/Select.ts index 79f342fae08c..9a7510b39f08 100644 --- a/packages/main/src/Select.ts +++ b/packages/main/src/Select.ts @@ -17,7 +17,14 @@ import { isTabPrevious, } from "@ui5/webcomponents-base/dist/Keys.js"; import announce from "@ui5/webcomponents-base/dist/util/InvisibleMessage.js"; -import { getEffectiveAriaLabelText, getAssociatedLabelForTexts } from "@ui5/webcomponents-base/dist/util/AccessibilityTextsHelper.js"; +import { + getEffectiveAriaLabelText, + getAssociatedLabelForTexts, + registerUI5Element, + deregisterUI5Element, + getAllAccessibleDescriptionRefTexts, + getEffectiveAriaDescriptionText, +} from "@ui5/webcomponents-base/dist/util/AccessibilityTextsHelper.js"; import ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js"; import "@ui5/webcomponents-icons/dist/error.js"; import "@ui5/webcomponents-icons/dist/alert.js"; @@ -303,6 +310,24 @@ class Select extends UI5Element implements IFormInputElement { @property() accessibleNameRef?: string; + /** + * Defines the accessible description of the component. + * @default undefined + * @public + * @since 2.14.0 + */ + @property() + accessibleDescription?: string; + + /** + * Receives id(or many ids) of the elements that describe the select. + * @default undefined + * @public + * @since 2.14.0 + */ + @property() + accessibleDescriptionRef?: string; + /** * Defines the tooltip of the select. * @default undefined @@ -312,6 +337,13 @@ class Select extends UI5Element implements IFormInputElement { @property() tooltip?: string; + /** + * Constantly updated value of texts collected from the associated description texts + * @private + */ + @property({ type: String, noAttribute: true }) + _associatedDescriptionRefTexts?: string; + /** * @private */ @@ -415,6 +447,14 @@ class Select extends UI5Element implements IFormInputElement { return ""; } + onEnterDOM() { + registerUI5Element(this, this._updateAssociatedLabelsTexts.bind(this)); + } + + onExitDOM() { + deregisterUI5Element(this); + } + onBeforeRendering() { this._applySelection(); @@ -1026,6 +1066,22 @@ class Select extends UI5Element implements IFormInputElement { return this.selectedOption && this.selectedOption.icon; } + get ariaDescriptionText() { + return this._associatedDescriptionRefTexts || getEffectiveAriaDescriptionText(this); + } + + get ariaDescriptionTextId() { + return this.ariaDescriptionText ? "accessibleDescription" : ""; + } + + get ariaDescribedByIds() { + return [this.valueStateTextId, this.ariaDescriptionTextId].filter(Boolean).join(" "); + } + + _updateAssociatedLabelsTexts() { + this._associatedDescriptionRefTexts = getAllAccessibleDescriptionRefTexts(this); + } + _getPopover() { return this.shadowRoot!.querySelector("[ui5-popover]"); } diff --git a/packages/main/src/SelectTemplate.tsx b/packages/main/src/SelectTemplate.tsx index ad26db7d462e..054d86f6b7ac 100644 --- a/packages/main/src/SelectTemplate.tsx +++ b/packages/main/src/SelectTemplate.tsx @@ -29,7 +29,7 @@ export default function SelectTemplate(this: Select) { role="combobox" aria-haspopup="listbox" aria-label={this.ariaLabelText} - aria-describedby={this.valueStateTextId} + aria-describedby={this.ariaDescribedByIds} aria-disabled={this.isDisabled} aria-required={this.required} aria-readonly={this.readonly} @@ -82,6 +82,12 @@ export default function SelectTemplate(this: Select) { {this.valueStateText} } + + {this.ariaDescriptionText && + + {this.ariaDescriptionText} + + } {SelectPopoverTemplate.call(this)} diff --git a/packages/main/test/pages/Select.html b/packages/main/test/pages/Select.html index 94646ca6c059..18b930d0ea2d 100644 --- a/packages/main/test/pages/Select.html +++ b/packages/main/test/pages/Select.html @@ -142,6 +142,48 @@

Select aria-label and aria-labelledby

+
+

Select with accessible description

+ Select your preferred mode: + + Cozy + Compact + Condensed + +
+ +
+

Select with accessible description ref

+ Country: +

Select your home country from the list below. This information will be used for shipping calculations.

+ + United States + United Kingdom + Germany + France + Italy + +
+ +
+

Select with multiple description references

+ Product: + Available products in your region + Some products may have shipping restrictions + + Laptop + Desktop + Monitor + Keyboard + +
+

Select in Compact