Skip to content

Commit f6e6981

Browse files
authored
feat(ui5-li-custom): improve accessibility announcements (#12696)
* wip(ui-li-custom): improve accessibility announcements * chore: fix lint errors * chore: fix lint errors * chore: fix lint errors * chore: add unit tests * chore: add test for delete button and move type before description * fix unit tests * fix jsDoc * do not update invisible text when dragging and remove default slot explicit definition * fix eslint errors * use custom announcement util for processing nodes for accessibility
1 parent 1ea6d5e commit f6e6981

3 files changed

Lines changed: 538 additions & 0 deletions

File tree

Lines changed: 396 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,396 @@
1+
import ListItemCustom from "../../src/ListItemCustom.js";
2+
import List from "../../src/List.js";
3+
import Button from "../../src/Button.js";
4+
import CheckBox from "../../src/CheckBox.js";
5+
6+
describe("ListItemCustom - _onfocusin and _onfocusout Tests", () => {
7+
describe("With pure HTML elements", () => {
8+
it("should update invisible text content on focusin and clear on focusout", () => {
9+
// Mount ListItemCustom with pure HTML elements
10+
cy.mount(
11+
<List>
12+
<ListItemCustom id="li-custom-html">
13+
<div>Test Content</div>
14+
<span>Additional Text</span>
15+
</ListItemCustom>
16+
</List>
17+
);
18+
19+
// Store the component ID for accessing the invisible text span
20+
cy.get("#li-custom-html").invoke("prop", "_id").as("itemId");
21+
22+
// Initially, the invisible text content should be empty
23+
cy.get("@itemId").then(itemId => {
24+
cy.get("#li-custom-html")
25+
.shadow()
26+
.find(`#${itemId}-invisibleTextContent`)
27+
.should("have.text", "");
28+
});
29+
30+
// Focus the list item
31+
cy.get("#li-custom-html").click();
32+
33+
// After focus, invisible text content should be populated
34+
cy.get("@itemId").then(itemId => {
35+
cy.get("#li-custom-html")
36+
.shadow()
37+
.find(`#${itemId}-invisibleTextContent`)
38+
.should("have.text", "List Item Test Content Additional Text");
39+
40+
// Check that aria-labelledby on the internal li element includes the invisibleTextContent span id
41+
cy.get("#li-custom-html")
42+
.shadow()
43+
.find("li[part='native-li']")
44+
.should("have.attr", "aria-labelledby")
45+
.and("include", `${itemId}-invisibleTextContent`);
46+
});
47+
48+
// Remove focus
49+
cy.focused().blur();
50+
51+
// After blur, invisible text content should be cleared
52+
cy.get("@itemId").then(itemId => {
53+
cy.get("#li-custom-html")
54+
.shadow()
55+
.find(`#${itemId}-invisibleTextContent`)
56+
.should("have.text", "");
57+
});
58+
});
59+
60+
it("should process text content from HTML elements for accessibility", () => {
61+
// Mount ListItemCustom with specific text content we can test for
62+
cy.mount(
63+
<List>
64+
<ListItemCustom id="li-custom-html-content">
65+
<div>Primary Content</div>
66+
<span>Secondary Information</span>
67+
<p>Paragraph text</p>
68+
</ListItemCustom>
69+
</List>
70+
);
71+
72+
// Store the component ID
73+
cy.get("#li-custom-html-content").invoke("prop", "_id").as("itemId");
74+
75+
// Focus the list item
76+
cy.get("#li-custom-html-content").click();
77+
78+
// Verify text content is processed and included in the invisible text
79+
cy.get("@itemId").then(itemId => {
80+
cy.get("#li-custom-html-content")
81+
.shadow()
82+
.find(`#${itemId}-invisibleTextContent`)
83+
.should("have.text", "List Item Primary Content Secondary Information Paragraph text");
84+
85+
// Check that aria-labelledby on the internal li element includes the invisibleTextContent span id
86+
cy.get("#li-custom-html-content")
87+
.shadow()
88+
.find("li[part='native-li']")
89+
.should("have.attr", "aria-labelledby")
90+
.and("include", `${itemId}-invisibleTextContent`);
91+
});
92+
});
93+
});
94+
95+
describe("With UI5 components", () => {
96+
it("should update invisible text content on focusin and clear on focusout with UI5 components", () => {
97+
// Mount ListItemCustom with UI5 components
98+
cy.mount(
99+
<List>
100+
<ListItemCustom id="li-custom-ui5">
101+
<Button id="test-button">Click me</Button>
102+
<CheckBox id="test-checkbox" text="Check option" required/>
103+
</ListItemCustom>
104+
</List>
105+
);
106+
107+
// Store the component ID
108+
cy.get("#li-custom-ui5").invoke("prop", "_id").as("itemId");
109+
110+
// Initially, the invisible text content should be empty
111+
cy.get("@itemId").then(itemId => {
112+
cy.get("#li-custom-ui5")
113+
.shadow()
114+
.find(`#${itemId}-invisibleTextContent`)
115+
.should("have.text", "");
116+
});
117+
118+
// Focus the list item
119+
cy.get("#li-custom-ui5").click();
120+
121+
// After focus, invisible text content should be populated
122+
cy.get("@itemId").then(itemId => {
123+
cy.get("#li-custom-ui5")
124+
.shadow()
125+
.find(`#${itemId}-invisibleTextContent`)
126+
.should("have.text", "List Item Button Click me Checkbox Check option Not checked Required");
127+
128+
// Check that aria-labelledby on the internal li element includes the invisibleTextContent span id
129+
cy.get("#li-custom-ui5")
130+
.shadow()
131+
.find("li[part='native-li']")
132+
.should("have.attr", "aria-labelledby")
133+
.and("include", `${itemId}-invisibleTextContent`);
134+
});
135+
136+
// Remove focus
137+
cy.focused().blur();
138+
139+
// After blur, invisible text content should be cleared
140+
cy.get("@itemId").then(itemId => {
141+
cy.get("#li-custom-ui5")
142+
.shadow()
143+
.find(`#${itemId}-invisibleTextContent`)
144+
.should("have.text", "");
145+
});
146+
});
147+
148+
it("should handle focus changes between list item and UI5 components", () => {
149+
// Mount ListItemCustom with UI5 components
150+
cy.mount(
151+
<List>
152+
<ListItemCustom id="li-custom-ui5-focus">
153+
<Button id="test-focus-button">Click Me</Button>
154+
<CheckBox id="test-focus-checkbox" text="Check Option" />
155+
</ListItemCustom>
156+
</List>
157+
);
158+
159+
// Store the component ID
160+
cy.get("#li-custom-ui5-focus").invoke("prop", "_id").as("itemId");
161+
162+
// Click the list item first to get focus
163+
cy.get("#li-custom-ui5-focus").click();
164+
165+
// Verify invisible text is populated
166+
cy.get("@itemId").then(itemId => {
167+
cy.get("#li-custom-ui5-focus")
168+
.shadow()
169+
.find(`#${itemId}-invisibleTextContent`)
170+
.should("have.text", "List Item Button Click Me Checkbox Check Option Not checked");
171+
172+
// Check that aria-labelledby on the internal li element includes the invisibleTextContent span id
173+
cy.get("#li-custom-ui5-focus")
174+
.shadow()
175+
.find("li[part='native-li']")
176+
.should("have.attr", "aria-labelledby")
177+
.and("include", `${itemId}-invisibleTextContent`);
178+
});
179+
180+
// Now click the button - this shouldn't trigger focusout on the list item
181+
// as it's a child element
182+
cy.get("#test-focus-button").click();
183+
184+
// Verify invisible text is still populated (list item should maintain focus state)
185+
cy.get("@itemId").then(itemId => {
186+
cy.get("#li-custom-ui5-focus")
187+
.shadow()
188+
.find(`#${itemId}-invisibleTextContent`)
189+
.should("have.text", "List Item Button Click Me Checkbox Check Option Not checked");
190+
});
191+
192+
// Click outside the list to truly remove focus
193+
cy.get("body").click({ force: true });
194+
195+
// Now invisible text should be cleared
196+
cy.get("@itemId").then(itemId => {
197+
cy.get("#li-custom-ui5-focus")
198+
.shadow()
199+
.find(`#${itemId}-invisibleTextContent`)
200+
.should("have.text", "");
201+
});
202+
});
203+
});
204+
205+
describe("With mixed elements and nesting", () => {
206+
it("should process nested elements for accessibility", () => {
207+
// Mount ListItemCustom with nested elements
208+
cy.mount(
209+
<List>
210+
<ListItemCustom id="li-custom-nested">
211+
<div className="container">
212+
<span>Container Text</span>
213+
<div className="nested-container">
214+
<Button id="nested-button">Nested Button</Button>
215+
</div>
216+
</div>
217+
<p>Paragraph outside container</p>
218+
</ListItemCustom>
219+
</List>
220+
);
221+
222+
// Store the component ID
223+
cy.get("#li-custom-nested").invoke("prop", "_id").as("itemId");
224+
225+
// Focus the list item
226+
cy.get("#li-custom-nested").click();
227+
228+
// Verify text content is processed and included in the invisible text
229+
cy.get("@itemId").then(itemId => {
230+
cy.get("#li-custom-nested")
231+
.shadow()
232+
.find(`#${itemId}-invisibleTextContent`)
233+
.should("have.text", "List Item Container Text Button Nested Button Paragraph outside container");
234+
235+
// Check that aria-labelledby on the internal li element includes the invisibleTextContent span id
236+
cy.get("#li-custom-nested")
237+
.shadow()
238+
.find("li[part='native-li']")
239+
.should("have.attr", "aria-labelledby")
240+
.and("include", `${itemId}-invisibleTextContent`);
241+
});
242+
});
243+
244+
it("should handle deep nesting of elements", () => {
245+
// Mount ListItemCustom with deeply nested elements
246+
cy.mount(
247+
<List>
248+
<ListItemCustom id="li-custom-deep-nested">
249+
<div className="level1">
250+
<div className="level2">
251+
<div className="level3">
252+
<Button id="deep-nested-button">Deeply Nested Button</Button>
253+
</div>
254+
<span className="level2-span">Level 2 Text</span>
255+
</div>
256+
<CheckBox id="nested-checkbox" text="Nested" />
257+
</div>
258+
</ListItemCustom>
259+
</List>
260+
);
261+
262+
// Store the component ID
263+
cy.get("#li-custom-deep-nested").invoke("prop", "_id").as("itemId");
264+
265+
// Focus the list item
266+
cy.get("#li-custom-deep-nested").click();
267+
268+
// Verify all nested content is processed
269+
cy.get("@itemId").then(itemId => {
270+
cy.get("#li-custom-deep-nested")
271+
.shadow()
272+
.find(`#${itemId}-invisibleTextContent`)
273+
.should("have.text", "List Item Button Deeply Nested Button Level 2 Text Checkbox Nested Not checked");
274+
275+
// Check that aria-labelledby on the internal li element includes the invisibleTextContent span id
276+
cy.get("#li-custom-deep-nested")
277+
.shadow()
278+
.find("li[part='native-li']")
279+
.should("have.attr", "aria-labelledby")
280+
.and("include", `${itemId}-invisibleTextContent`);
281+
});
282+
283+
// Remove focus
284+
cy.focused().blur();
285+
286+
// After blur, invisible text content should be cleared
287+
cy.get("@itemId").then(itemId => {
288+
cy.get("#li-custom-deep-nested")
289+
.shadow()
290+
.find(`#${itemId}-invisibleTextContent`)
291+
.should("have.text", "");
292+
});
293+
});
294+
});
295+
296+
describe("With delete mode and custom delete button", () => {
297+
it("should handle ListItemCustom with delete mode and custom delete button", () => {
298+
// Mount ListItemCustom with delete mode and custom delete button
299+
cy.mount(
300+
<List selectionMode="Delete">
301+
<ListItemCustom id="li-custom-delete">
302+
<div>Delete Mode Item</div>
303+
<Button slot="deleteButton" id="custom-delete-button">
304+
Remove
305+
</Button>
306+
</ListItemCustom>
307+
</List>
308+
);
309+
310+
// Store the component ID
311+
cy.get("#li-custom-delete").invoke("prop", "_id").as("itemId");
312+
313+
// Focus the list item
314+
cy.get("#li-custom-delete").click();
315+
316+
// Verify text content is processed and included in the invisible text
317+
cy.get("@itemId").then(itemId => {
318+
cy.get("#li-custom-delete")
319+
.shadow()
320+
.find(`#${itemId}-invisibleTextContent`)
321+
.should("have.text", "List Item Delete Mode Item Button Remove");
322+
323+
// Check that aria-labelledby on the internal li element includes the invisibleTextContent span id
324+
cy.get("#li-custom-delete")
325+
.shadow()
326+
.find("li[part='native-li']")
327+
.should("have.attr", "aria-labelledby")
328+
.and("include", `${itemId}-invisibleTextContent`);
329+
});
330+
331+
// Remove focus
332+
cy.focused().blur();
333+
334+
// After blur, invisible text content should be cleared
335+
cy.get("@itemId").then(itemId => {
336+
cy.get("#li-custom-delete")
337+
.shadow()
338+
.find(`#${itemId}-invisibleTextContent`)
339+
.should("have.text", "");
340+
});
341+
});
342+
});
343+
344+
describe("Edge cases", () => {
345+
it("should handle empty list item content", () => {
346+
cy.mount(
347+
<List>
348+
<ListItemCustom id="li-custom-empty"></ListItemCustom>
349+
</List>
350+
);
351+
352+
// Store the component ID
353+
cy.get("#li-custom-empty").invoke("prop", "_id").as("itemId");
354+
355+
// Focus the list item
356+
cy.get("#li-custom-empty").click();
357+
358+
// Should still have basic announcement text
359+
cy.get("@itemId").then(itemId => {
360+
cy.get("#li-custom-empty")
361+
.shadow()
362+
.find(`#${itemId}-invisibleTextContent`)
363+
.should("have.text", "List Item");
364+
365+
// Check that aria-labelledby on the internal li element includes the invisibleTextContent span id
366+
cy.get("#li-custom-empty")
367+
.shadow()
368+
.find("li[part='native-li']")
369+
.should("have.attr", "aria-labelledby")
370+
.and("include", `${itemId}-invisibleTextContent`);
371+
});
372+
});
373+
374+
it("should handle list item with accessibleName", () => {
375+
cy.mount(
376+
<List>
377+
<ListItemCustom
378+
id="li-custom-accessible-name"
379+
accessibleName="Accessible Name Test"
380+
>
381+
<div>This content should not be announced</div>
382+
</ListItemCustom>
383+
</List>
384+
);
385+
386+
// Check that aria-labelledBy on the internal li element doesn't include the ID of the invisibleTextContent span
387+
cy.get("#li-custom-accessible-name").invoke("prop", "_id").then(itemId => {
388+
cy.get("#li-custom-accessible-name")
389+
.shadow()
390+
.find("li[part='native-li']")
391+
.invoke("attr", "aria-labelledby")
392+
.should("not.include", `${itemId}-invisibleTextContent`);
393+
});
394+
});
395+
});
396+
});

0 commit comments

Comments
 (0)