Skip to content

Commit 7152726

Browse files
authored
feat(ui5-select): add accessibleDescription and accessibleDescriptionRef (#12081)
* 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 * fix(ui5-select): prevent empty aria-describedby attribute rendering - Fixed ariaDescribedByIds getter to return undefined when no valid IDs exist - Updated SelectTemplate to conditionally render aria-describedby using spread operator - Added comprehensive tests for accessibleDescription and accessibleDescriptionRef - Ensures proper accessibility standards by not rendering empty ARIA attributes This prevents invalid HTML where aria-describedby="" would be rendered when no accessible description or value state is present.
1 parent 440fa11 commit 7152726

File tree

4 files changed

+269
-2
lines changed

4 files changed

+269
-2
lines changed

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

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,166 @@ 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+
<Select id="selectWithoutDescription">
325+
<Option value="A">A</Option>
326+
<Option value="B">B</Option>
327+
<Option value="C" selected>C</Option>
328+
</Select>
329+
</>
330+
);
331+
332+
const EXPECTED_DESCRIPTION = "Select description";
333+
const EXPECTED_DESCRIPTION_REF = "Description text";
334+
335+
// Test first select with accessibleDescription
336+
cy.get("#selectWithAccessibleDescription")
337+
.shadow()
338+
.find("#accessibleDescription")
339+
.should("have.text", EXPECTED_DESCRIPTION);
340+
341+
cy.get("#selectWithAccessibleDescription")
342+
.shadow()
343+
.find(".ui5-select-label-root")
344+
.should("have.attr", "aria-describedby")
345+
.and("contain", "accessibleDescription");
346+
347+
// Test second select with accessibleDescriptionRef
348+
cy.get("#selectWithAccessibleDescriptionRef")
349+
.shadow()
350+
.find("#accessibleDescription")
351+
.should("have.text", EXPECTED_DESCRIPTION_REF);
352+
353+
cy.get("#selectWithAccessibleDescriptionRef")
354+
.shadow()
355+
.find(".ui5-select-label-root")
356+
.should("have.attr", "aria-describedby")
357+
.and("contain", "accessibleDescription");
358+
359+
// Test select without description should not have aria-describedby
360+
cy.get("#selectWithoutDescription")
361+
.shadow()
362+
.find(".ui5-select-label-root")
363+
.should("not.have.attr", "aria-describedby");
364+
365+
// Test that changing the referenced element updates the description
366+
cy.get("#descText")
367+
.invoke("text", "Updated description text");
368+
369+
cy.get("#selectWithAccessibleDescriptionRef")
370+
.shadow()
371+
.find("#accessibleDescription")
372+
.should("have.text", "Updated description text");
373+
});
374+
375+
it("tests Select with both valueState Positive and accessibleDescription", () => {
376+
cy.mount(
377+
<Select valueState="Positive" accessibleDescription="Additional description">
378+
<Option value="First">First</Option>
379+
<Option value="Second">Second</Option>
380+
<Option value="Third" selected>Third</Option>
381+
</Select>
382+
);
383+
384+
const EXPECTED_VALUE_STATE_TEXT = "Success";
385+
const EXPECTED_DESCRIPTION = "Additional description";
386+
387+
// Test that both valueState and accessibleDescription are included in aria-describedby
388+
cy.get("[ui5-select]")
389+
.shadow()
390+
.find(".ui5-select-label-root")
391+
.should("have.attr", "aria-describedby")
392+
.and("contain", "-valueStateDesc")
393+
.and("contain", "accessibleDescription");
394+
395+
// Test that the value state description text is correct
396+
cy.get("[ui5-select]")
397+
.shadow()
398+
.find("[id$='-valueStateDesc']")
399+
.should("contain.text", EXPECTED_VALUE_STATE_TEXT);
400+
401+
// Test that the accessible description text is correct
402+
cy.get("[ui5-select]")
403+
.shadow()
404+
.find("#accessibleDescription")
405+
.should("have.text", EXPECTED_DESCRIPTION);
406+
});
407+
408+
it("tests Select with multiple accessibleDescriptionRef values", () => {
409+
cy.mount(
410+
<>
411+
<span id="desc1">First description</span>
412+
<span id="desc2">Second description</span>
413+
<span id="desc3">Third description</span>
414+
<Select accessibleDescriptionRef="desc1 desc2 desc3">
415+
<Option value="First">First</Option>
416+
<Option value="Second">Second</Option>
417+
<Option value="Third" selected>Third</Option>
418+
</Select>
419+
</>
420+
);
421+
422+
const EXPECTED_COMBINED_DESCRIPTION = "First description Second description Third description";
423+
424+
// Test that accessibleDescriptionRef with multiple IDs creates the correct aria-describedby reference
425+
cy.get("[ui5-select]")
426+
.shadow()
427+
.find(".ui5-select-label-root")
428+
.should("have.attr", "aria-describedby")
429+
.and("contain", "accessibleDescription");
430+
431+
// Test that the combined description text from multiple elements is correct
432+
cy.get("[ui5-select]")
433+
.shadow()
434+
.find("#accessibleDescription")
435+
.should("have.text", EXPECTED_COMBINED_DESCRIPTION);
436+
437+
// Test that changing one of the referenced elements updates the combined description
438+
cy.get("#desc2")
439+
.invoke("text", "Updated second description");
440+
441+
cy.get("[ui5-select]")
442+
.shadow()
443+
.find("#accessibleDescription")
444+
.should("have.text", "First description Updated second description Third description");
445+
});
286446
});
287447

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

packages/main/src/Select.ts

Lines changed: 58 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

@@ -1042,6 +1082,23 @@ class Select extends UI5Element implements IFormInputElement {
10421082
return this.selectedOption && this.selectedOption.icon;
10431083
}
10441084

1085+
get ariaDescriptionText() {
1086+
return this._associatedDescriptionRefTexts || getEffectiveAriaDescriptionText(this);
1087+
}
1088+
1089+
get ariaDescriptionTextId() {
1090+
return this.ariaDescriptionText ? "accessibleDescription" : "";
1091+
}
1092+
1093+
get ariaDescribedByIds() {
1094+
const ids = [this.valueStateTextId, this.ariaDescriptionTextId].filter(Boolean);
1095+
return ids.length ? ids.join(" ") : undefined;
1096+
}
1097+
1098+
_updateAssociatedLabelsTexts() {
1099+
this._associatedDescriptionRefTexts = getAllAccessibleDescriptionRefTexts(this);
1100+
}
1101+
10451102
_getPopover() {
10461103
return this.shadowRoot!.querySelector<Popover>("[ui5-popover]");
10471104
}

packages/main/src/SelectTemplate.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ export default function SelectTemplate(this: Select) {
2929
role="combobox"
3030
aria-haspopup="listbox"
3131
aria-label={this.ariaLabelText}
32-
aria-describedby={this.valueStateTextId}
32+
{...this.ariaDescribedByIds && {
33+
"aria-describedby": this.ariaDescribedByIds
34+
}}
3335
aria-disabled={this.isDisabled}
3436
aria-required={this.required}
3537
aria-readonly={this.readonly}
@@ -83,6 +85,12 @@ export default function SelectTemplate(this: Select) {
8385
{this.valueStateText}
8486
</span>
8587
}
88+
89+
{this.ariaDescriptionText &&
90+
<span id="accessibleDescription" class="ui5-hidden-text">
91+
{this.ariaDescriptionText}
92+
</span>
93+
}
8694
</div>
8795
{SelectPopoverTemplate.call(this)}
8896
</>

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)