Skip to content

Commit 74e4d5b

Browse files
committed
feat(ui5-select): add accessibleDescription and accessibleDescriptionRef support
- Add accessibleDescription property for providing descriptive text - Add accessibleDescriptionRef property for referencing external description elements - Update aria-describedby to include both value state and accessible descriptions - Add comprehensive Cypress tests covering: * Basic accessibleDescription functionality * accessibleDescriptionRef with single and multiple element references * Dynamic updates when referenced elements change * Interaction with existing valueState descriptions - Add test page examples demonstrating various usage scenarios - Ensure proper accessibility attribute handling and ARIA compliance Related: #12004
1 parent a8e8dd1 commit 74e4d5b

File tree

4 files changed

+255
-2
lines changed

4 files changed

+255
-2
lines changed

packages/main/cypress/specs/Select.cy.tsx

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,155 @@ describe("Select - Accessibility", () => {
283283
.should("have.attr", "aria-expanded", "false")
284284
.should("have.attr", "aria-roledescription", EXPECTED_ARIA_ROLEDESCRIPTION);
285285
});
286+
287+
it("tests Select with valueState Positive and aria-describedby", () => {
288+
cy.mount(
289+
<Select valueState="Positive">
290+
<Option value="First">First</Option>
291+
<Option value="Second">Second</Option>
292+
<Option value="Third" selected>Third</Option>
293+
</Select>
294+
);
295+
296+
// Test that valueState creates the correct aria-describedby reference
297+
cy.get("[ui5-select]")
298+
.shadow()
299+
.find(".ui5-select-label-root")
300+
.should("have.attr", "aria-describedby")
301+
.and("contain", "-valueStateDesc");
302+
303+
// Test that the value state description text contains "Success"
304+
cy.get("[ui5-select]")
305+
.shadow()
306+
.find("[id$='-valueStateDesc']")
307+
.should("contain.text", "Success");
308+
});
309+
310+
it("tests accessibleDescription and accessibleDescriptionRef", () => {
311+
cy.mount(
312+
<>
313+
<span id="descText">Description text</span>
314+
<Select id="selectWithAccessibleDescription" accessibleDescription="Select description">
315+
<Option value="First">First</Option>
316+
<Option value="Second">Second</Option>
317+
<Option value="Third" selected>Third</Option>
318+
</Select>
319+
<Select id="selectWithAccessibleDescriptionRef" accessibleDescriptionRef="descText">
320+
<Option value="One">One</Option>
321+
<Option value="Two">Two</Option>
322+
<Option value="Three" selected>Three</Option>
323+
</Select>
324+
</>
325+
);
326+
327+
const EXPECTED_DESCRIPTION = "Select description";
328+
const EXPECTED_DESCRIPTION_REF = "Description text";
329+
330+
// Test first select with accessibleDescription
331+
cy.get("#selectWithAccessibleDescription")
332+
.shadow()
333+
.find("#accessibleDescription")
334+
.should("have.text", EXPECTED_DESCRIPTION);
335+
336+
cy.get("#selectWithAccessibleDescription")
337+
.shadow()
338+
.find(".ui5-select-label-root")
339+
.should("have.attr", "aria-describedby")
340+
.and("contain", "accessibleDescription");
341+
342+
// Test second select with accessibleDescriptionRef
343+
cy.get("#selectWithAccessibleDescriptionRef")
344+
.shadow()
345+
.find("#accessibleDescription")
346+
.should("have.text", EXPECTED_DESCRIPTION_REF);
347+
348+
cy.get("#selectWithAccessibleDescriptionRef")
349+
.shadow()
350+
.find(".ui5-select-label-root")
351+
.should("have.attr", "aria-describedby")
352+
.and("contain", "accessibleDescription");
353+
354+
// Test that changing the referenced element updates the description
355+
cy.get("#descText")
356+
.invoke("text", "Updated description text");
357+
358+
cy.get("#selectWithAccessibleDescriptionRef")
359+
.shadow()
360+
.find("#accessibleDescription")
361+
.should("have.text", "Updated description text");
362+
});
363+
364+
it("tests Select with both valueState Positive and accessibleDescription", () => {
365+
cy.mount(
366+
<Select valueState="Positive" accessibleDescription="Additional description">
367+
<Option value="First">First</Option>
368+
<Option value="Second">Second</Option>
369+
<Option value="Third" selected>Third</Option>
370+
</Select>
371+
);
372+
373+
const EXPECTED_VALUE_STATE_TEXT = "Success";
374+
const EXPECTED_DESCRIPTION = "Additional description";
375+
376+
// Test that both valueState and accessibleDescription are included in aria-describedby
377+
cy.get("[ui5-select]")
378+
.shadow()
379+
.find(".ui5-select-label-root")
380+
.should("have.attr", "aria-describedby")
381+
.and("contain", "-valueStateDesc")
382+
.and("contain", "accessibleDescription");
383+
384+
// Test that the value state description text is correct
385+
cy.get("[ui5-select]")
386+
.shadow()
387+
.find("[id$='-valueStateDesc']")
388+
.should("contain.text", EXPECTED_VALUE_STATE_TEXT);
389+
390+
// Test that the accessible description text is correct
391+
cy.get("[ui5-select]")
392+
.shadow()
393+
.find("#accessibleDescription")
394+
.should("have.text", EXPECTED_DESCRIPTION);
395+
});
396+
397+
it("tests Select with multiple accessibleDescriptionRef values", () => {
398+
cy.mount(
399+
<>
400+
<span id="desc1">First description</span>
401+
<span id="desc2">Second description</span>
402+
<span id="desc3">Third description</span>
403+
<Select accessibleDescriptionRef="desc1 desc2 desc3">
404+
<Option value="First">First</Option>
405+
<Option value="Second">Second</Option>
406+
<Option value="Third" selected>Third</Option>
407+
</Select>
408+
</>
409+
);
410+
411+
const EXPECTED_COMBINED_DESCRIPTION = "First description Second description Third description";
412+
413+
// Test that accessibleDescriptionRef with multiple IDs creates the correct aria-describedby reference
414+
cy.get("[ui5-select]")
415+
.shadow()
416+
.find(".ui5-select-label-root")
417+
.should("have.attr", "aria-describedby")
418+
.and("contain", "accessibleDescription");
419+
420+
// Test that the combined description text from multiple elements is correct
421+
cy.get("[ui5-select]")
422+
.shadow()
423+
.find("#accessibleDescription")
424+
.should("have.text", EXPECTED_COMBINED_DESCRIPTION);
425+
426+
// Test that changing one of the referenced elements updates the combined description
427+
cy.get("#desc2")
428+
.invoke("text", "Updated second description");
429+
430+
cy.get("[ui5-select]")
431+
.shadow()
432+
.find("#accessibleDescription")
433+
.should("have.text", "First description Updated second description Third description");
434+
});
286435
});
287436

288437
describe("Select - Popover", () => {

packages/main/src/Select.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,14 @@ import {
1717
isTabPrevious,
1818
} from "@ui5/webcomponents-base/dist/Keys.js";
1919
import announce from "@ui5/webcomponents-base/dist/util/InvisibleMessage.js";
20-
import { getEffectiveAriaLabelText, getAssociatedLabelForTexts } from "@ui5/webcomponents-base/dist/util/AccessibilityTextsHelper.js";
20+
import {
21+
getEffectiveAriaLabelText,
22+
getAssociatedLabelForTexts,
23+
registerUI5Element,
24+
deregisterUI5Element,
25+
getAllAccessibleDescriptionRefTexts,
26+
getEffectiveAriaDescriptionText,
27+
} from "@ui5/webcomponents-base/dist/util/AccessibilityTextsHelper.js";
2128
import ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js";
2229
import "@ui5/webcomponents-icons/dist/error.js";
2330
import "@ui5/webcomponents-icons/dist/alert.js";
@@ -303,6 +310,24 @@ class Select extends UI5Element implements IFormInputElement {
303310
@property()
304311
accessibleNameRef?: string;
305312

313+
/**
314+
* Defines the accessible description of the component.
315+
* @default undefined
316+
* @public
317+
* @since 2.14.0
318+
*/
319+
@property()
320+
accessibleDescription?: string;
321+
322+
/**
323+
* Receives id(or many ids) of the elements that describe the select.
324+
* @default undefined
325+
* @public
326+
* @since 2.14.0
327+
*/
328+
@property()
329+
accessibleDescriptionRef?: string;
330+
306331
/**
307332
* Defines the tooltip of the select.
308333
* @default undefined
@@ -312,6 +337,13 @@ class Select extends UI5Element implements IFormInputElement {
312337
@property()
313338
tooltip?: string;
314339

340+
/**
341+
* Constantly updated value of texts collected from the associated description texts
342+
* @private
343+
*/
344+
@property({ type: String, noAttribute: true })
345+
_associatedDescriptionRefTexts?: string;
346+
315347
/**
316348
* @private
317349
*/
@@ -415,6 +447,14 @@ class Select extends UI5Element implements IFormInputElement {
415447
return "";
416448
}
417449

450+
onEnterDOM() {
451+
registerUI5Element(this, this._updateAssociatedLabelsTexts.bind(this));
452+
}
453+
454+
onExitDOM() {
455+
deregisterUI5Element(this);
456+
}
457+
418458
onBeforeRendering() {
419459
this._applySelection();
420460

@@ -1030,6 +1070,22 @@ class Select extends UI5Element implements IFormInputElement {
10301070
return this.selectedOption && this.selectedOption.icon;
10311071
}
10321072

1073+
get ariaDescriptionText() {
1074+
return this._associatedDescriptionRefTexts || getEffectiveAriaDescriptionText(this);
1075+
}
1076+
1077+
get ariaDescriptionTextId() {
1078+
return this.ariaDescriptionText ? "accessibleDescription" : "";
1079+
}
1080+
1081+
get ariaDescribedByIds() {
1082+
return [this.valueStateTextId, this.ariaDescriptionTextId].filter(Boolean).join(" ");
1083+
}
1084+
1085+
_updateAssociatedLabelsTexts() {
1086+
this._associatedDescriptionRefTexts = getAllAccessibleDescriptionRefTexts(this);
1087+
}
1088+
10331089
_getPopover() {
10341090
return this.shadowRoot!.querySelector<Popover>("[ui5-popover]");
10351091
}

packages/main/src/SelectTemplate.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export default function SelectTemplate(this: Select) {
2929
role="combobox"
3030
aria-haspopup="listbox"
3131
aria-label={this.ariaLabelText}
32-
aria-describedby={this.valueStateTextId}
32+
aria-describedby={this.ariaDescribedByIds}
3333
aria-disabled={this.isDisabled}
3434
aria-required={this.required}
3535
aria-readonly={this.readonly}
@@ -83,6 +83,12 @@ export default function SelectTemplate(this: Select) {
8383
{this.valueStateText}
8484
</span>
8585
}
86+
87+
{this.ariaDescriptionText &&
88+
<span id="accessibleDescription" class="ui5-hidden-text">
89+
{this.ariaDescriptionText}
90+
</span>
91+
}
8692
</div>
8793
{SelectPopoverTemplate.call(this)}
8894
</>

packages/main/test/pages/Select.html

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,48 @@ <h3>Select aria-label and aria-labelledby</h3>
142142
</div>
143143
</section>
144144

145+
<section>
146+
<h2>Select with accessible description</h2>
147+
<ui5-label for="select-with-description">Select your preferred mode:</ui5-label>
148+
<ui5-select
149+
id="select-with-description"
150+
accessible-description="Choose the display density that best suits your needs">
151+
<ui5-option>Cozy</ui5-option>
152+
<ui5-option selected>Compact</ui5-option>
153+
<ui5-option>Condensed</ui5-option>
154+
</ui5-select>
155+
</section>
156+
157+
<section>
158+
<h2>Select with accessible description ref</h2>
159+
<ui5-label for="select-with-description-ref">Country:</ui5-label>
160+
<p id="country-description">Select your home country from the list below. This information will be used for shipping calculations.</p>
161+
<ui5-select
162+
id="select-with-description-ref"
163+
accessible-description-ref="country-description">
164+
<ui5-option>United States</ui5-option>
165+
<ui5-option>United Kingdom</ui5-option>
166+
<ui5-option selected>Germany</ui5-option>
167+
<ui5-option>France</ui5-option>
168+
<ui5-option>Italy</ui5-option>
169+
</ui5-select>
170+
</section>
171+
172+
<section>
173+
<h2>Select with multiple description references</h2>
174+
<ui5-label for="select-multiple-refs">Product:</ui5-label>
175+
<span id="product-info">Available products in your region</span>
176+
<span id="product-note">Some products may have shipping restrictions</span>
177+
<ui5-select
178+
id="select-multiple-refs"
179+
accessible-description-ref="product-info product-note">
180+
<ui5-option>Laptop</ui5-option>
181+
<ui5-option selected>Desktop</ui5-option>
182+
<ui5-option>Monitor</ui5-option>
183+
<ui5-option>Keyboard</ui5-option>
184+
</ui5-select>
185+
</section>
186+
145187
<section class="ui5-content-density-compact">
146188
<h3>Select in Compact</h3>
147189
<div>

0 commit comments

Comments
 (0)