Skip to content

Commit f59151f

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 b19d29f commit f59151f

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
@@ -262,6 +262,155 @@ describe("Select - Accessibility", () => {
262262
.should("have.attr", "aria-expanded", "false")
263263
.should("have.attr", "aria-roledescription", EXPECTED_ARIA_ROLEDESCRIPTION);
264264
});
265+
266+
it("tests Select with valueState Positive and aria-describedby", () => {
267+
cy.mount(
268+
<Select valueState="Positive">
269+
<Option value="First">First</Option>
270+
<Option value="Second">Second</Option>
271+
<Option value="Third" selected>Third</Option>
272+
</Select>
273+
);
274+
275+
// Test that valueState creates the correct aria-describedby reference
276+
cy.get("[ui5-select]")
277+
.shadow()
278+
.find(".ui5-select-label-root")
279+
.should("have.attr", "aria-describedby")
280+
.and("contain", "-valueStateDesc");
281+
282+
// Test that the value state description text contains "Success"
283+
cy.get("[ui5-select]")
284+
.shadow()
285+
.find("[id$='-valueStateDesc']")
286+
.should("contain.text", "Success");
287+
});
288+
289+
it("tests accessibleDescription and accessibleDescriptionRef", () => {
290+
cy.mount(
291+
<>
292+
<span id="descText">Description text</span>
293+
<Select id="selectWithAccessibleDescription" accessibleDescription="Select description">
294+
<Option value="First">First</Option>
295+
<Option value="Second">Second</Option>
296+
<Option value="Third" selected>Third</Option>
297+
</Select>
298+
<Select id="selectWithAccessibleDescriptionRef" accessibleDescriptionRef="descText">
299+
<Option value="One">One</Option>
300+
<Option value="Two">Two</Option>
301+
<Option value="Three" selected>Three</Option>
302+
</Select>
303+
</>
304+
);
305+
306+
const EXPECTED_DESCRIPTION = "Select description";
307+
const EXPECTED_DESCRIPTION_REF = "Description text";
308+
309+
// Test first select with accessibleDescription
310+
cy.get("#selectWithAccessibleDescription")
311+
.shadow()
312+
.find("#accessibleDescription")
313+
.should("have.text", EXPECTED_DESCRIPTION);
314+
315+
cy.get("#selectWithAccessibleDescription")
316+
.shadow()
317+
.find(".ui5-select-label-root")
318+
.should("have.attr", "aria-describedby")
319+
.and("contain", "accessibleDescription");
320+
321+
// Test second select with accessibleDescriptionRef
322+
cy.get("#selectWithAccessibleDescriptionRef")
323+
.shadow()
324+
.find("#accessibleDescription")
325+
.should("have.text", EXPECTED_DESCRIPTION_REF);
326+
327+
cy.get("#selectWithAccessibleDescriptionRef")
328+
.shadow()
329+
.find(".ui5-select-label-root")
330+
.should("have.attr", "aria-describedby")
331+
.and("contain", "accessibleDescription");
332+
333+
// Test that changing the referenced element updates the description
334+
cy.get("#descText")
335+
.invoke("text", "Updated description text");
336+
337+
cy.get("#selectWithAccessibleDescriptionRef")
338+
.shadow()
339+
.find("#accessibleDescription")
340+
.should("have.text", "Updated description text");
341+
});
342+
343+
it("tests Select with both valueState Positive and accessibleDescription", () => {
344+
cy.mount(
345+
<Select valueState="Positive" accessibleDescription="Additional description">
346+
<Option value="First">First</Option>
347+
<Option value="Second">Second</Option>
348+
<Option value="Third" selected>Third</Option>
349+
</Select>
350+
);
351+
352+
const EXPECTED_VALUE_STATE_TEXT = "Success";
353+
const EXPECTED_DESCRIPTION = "Additional description";
354+
355+
// Test that both valueState and accessibleDescription are included in aria-describedby
356+
cy.get("[ui5-select]")
357+
.shadow()
358+
.find(".ui5-select-label-root")
359+
.should("have.attr", "aria-describedby")
360+
.and("contain", "-valueStateDesc")
361+
.and("contain", "accessibleDescription");
362+
363+
// Test that the value state description text is correct
364+
cy.get("[ui5-select]")
365+
.shadow()
366+
.find("[id$='-valueStateDesc']")
367+
.should("contain.text", EXPECTED_VALUE_STATE_TEXT);
368+
369+
// Test that the accessible description text is correct
370+
cy.get("[ui5-select]")
371+
.shadow()
372+
.find("#accessibleDescription")
373+
.should("have.text", EXPECTED_DESCRIPTION);
374+
});
375+
376+
it("tests Select with multiple accessibleDescriptionRef values", () => {
377+
cy.mount(
378+
<>
379+
<span id="desc1">First description</span>
380+
<span id="desc2">Second description</span>
381+
<span id="desc3">Third description</span>
382+
<Select accessibleDescriptionRef="desc1 desc2 desc3">
383+
<Option value="First">First</Option>
384+
<Option value="Second">Second</Option>
385+
<Option value="Third" selected>Third</Option>
386+
</Select>
387+
</>
388+
);
389+
390+
const EXPECTED_COMBINED_DESCRIPTION = "First description Second description Third description";
391+
392+
// Test that accessibleDescriptionRef with multiple IDs creates the correct aria-describedby reference
393+
cy.get("[ui5-select]")
394+
.shadow()
395+
.find(".ui5-select-label-root")
396+
.should("have.attr", "aria-describedby")
397+
.and("contain", "accessibleDescription");
398+
399+
// Test that the combined description text from multiple elements is correct
400+
cy.get("[ui5-select]")
401+
.shadow()
402+
.find("#accessibleDescription")
403+
.should("have.text", EXPECTED_COMBINED_DESCRIPTION);
404+
405+
// Test that changing one of the referenced elements updates the combined description
406+
cy.get("#desc2")
407+
.invoke("text", "Updated second description");
408+
409+
cy.get("[ui5-select]")
410+
.shadow()
411+
.find("#accessibleDescription")
412+
.should("have.text", "First description Updated second description Third description");
413+
});
265414
});
266415

267416
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

@@ -1026,6 +1066,22 @@ class Select extends UI5Element implements IFormInputElement {
10261066
return this.selectedOption && this.selectedOption.icon;
10271067
}
10281068

1069+
get ariaDescriptionText() {
1070+
return this._associatedDescriptionRefTexts || getEffectiveAriaDescriptionText(this);
1071+
}
1072+
1073+
get ariaDescriptionTextId() {
1074+
return this.ariaDescriptionText ? "accessibleDescription" : "";
1075+
}
1076+
1077+
get ariaDescribedByIds() {
1078+
return [this.valueStateTextId, this.ariaDescriptionTextId].filter(Boolean).join(" ");
1079+
}
1080+
1081+
_updateAssociatedLabelsTexts() {
1082+
this._associatedDescriptionRefTexts = getAllAccessibleDescriptionRefTexts(this);
1083+
}
1084+
10291085
_getPopover() {
10301086
return this.shadowRoot!.querySelector<Popover>("[ui5-popover]");
10311087
}

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}
@@ -82,6 +82,12 @@ export default function SelectTemplate(this: Select) {
8282
{this.valueStateText}
8383
</span>
8484
}
85+
86+
{this.ariaDescriptionText &&
87+
<span id="accessibleDescription" class="ui5-hidden-text">
88+
{this.ariaDescriptionText}
89+
</span>
90+
}
8591
</div>
8692
{SelectPopoverTemplate.call(this)}
8793
</>

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)