Skip to content

feat(ui5-select): add accessibleDescription and accessibleDescriptionRef #12081

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions packages/main/cypress/specs/Select.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<Select valueState="Positive">
<Option value="First">First</Option>
<Option value="Second">Second</Option>
<Option value="Third" selected>Third</Option>
</Select>
);

// 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(
<>
<span id="descText">Description text</span>
<Select id="selectWithAccessibleDescription" accessibleDescription="Select description">
<Option value="First">First</Option>
<Option value="Second">Second</Option>
<Option value="Third" selected>Third</Option>
</Select>
<Select id="selectWithAccessibleDescriptionRef" accessibleDescriptionRef="descText">
<Option value="One">One</Option>
<Option value="Two">Two</Option>
<Option value="Three" selected>Three</Option>
</Select>
</>
);

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(
<Select valueState="Positive" accessibleDescription="Additional description">
<Option value="First">First</Option>
<Option value="Second">Second</Option>
<Option value="Third" selected>Third</Option>
</Select>
);

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(
<>
<span id="desc1">First description</span>
<span id="desc2">Second description</span>
<span id="desc3">Third description</span>
<Select accessibleDescriptionRef="desc1 desc2 desc3">
<Option value="First">First</Option>
<Option value="Second">Second</Option>
<Option value="Third" selected>Third</Option>
</Select>
</>
);

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", () => {
Expand Down
58 changes: 57 additions & 1 deletion packages/main/src/Select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand All @@ -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
*/
Expand Down Expand Up @@ -415,6 +447,14 @@ class Select extends UI5Element implements IFormInputElement {
return "";
}

onEnterDOM() {
registerUI5Element(this, this._updateAssociatedLabelsTexts.bind(this));
}

onExitDOM() {
deregisterUI5Element(this);
}

onBeforeRendering() {
this._applySelection();

Expand Down Expand Up @@ -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<Popover>("[ui5-popover]");
}
Expand Down
8 changes: 7 additions & 1 deletion packages/main/src/SelectTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -82,6 +82,12 @@ export default function SelectTemplate(this: Select) {
{this.valueStateText}
</span>
}

{this.ariaDescriptionText &&
<span id="accessibleDescription" class="ui5-hidden-text">
{this.ariaDescriptionText}
</span>
}
</div>
{SelectPopoverTemplate.call(this)}
</>
Expand Down
42 changes: 42 additions & 0 deletions packages/main/test/pages/Select.html
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,48 @@ <h3>Select aria-label and aria-labelledby</h3>
</div>
</section>

<section>
<h2>Select with accessible description</h2>
<ui5-label for="select-with-description">Select your preferred mode:</ui5-label>
<ui5-select
id="select-with-description"
accessible-description="Choose the display density that best suits your needs">
<ui5-option>Cozy</ui5-option>
<ui5-option selected>Compact</ui5-option>
<ui5-option>Condensed</ui5-option>
</ui5-select>
</section>

<section>
<h2>Select with accessible description ref</h2>
<ui5-label for="select-with-description-ref">Country:</ui5-label>
<p id="country-description">Select your home country from the list below. This information will be used for shipping calculations.</p>
<ui5-select
id="select-with-description-ref"
accessible-description-ref="country-description">
<ui5-option>United States</ui5-option>
<ui5-option>United Kingdom</ui5-option>
<ui5-option selected>Germany</ui5-option>
<ui5-option>France</ui5-option>
<ui5-option>Italy</ui5-option>
</ui5-select>
</section>

<section>
<h2>Select with multiple description references</h2>
<ui5-label for="select-multiple-refs">Product:</ui5-label>
<span id="product-info">Available products in your region</span>
<span id="product-note">Some products may have shipping restrictions</span>
<ui5-select
id="select-multiple-refs"
accessible-description-ref="product-info product-note">
<ui5-option>Laptop</ui5-option>
<ui5-option selected>Desktop</ui5-option>
<ui5-option>Monitor</ui5-option>
<ui5-option>Keyboard</ui5-option>
</ui5-select>
</section>

<section class="ui5-content-density-compact">
<h3>Select in Compact</h3>
<div>
Expand Down
Loading