Skip to content

Commit 37e9fef

Browse files
feat(ui5-list): inherit list item aria roles from accessibleRole (#13463)
Map list accessibleRole values to child item roles (Menu -> menuitem, Tree -> treeitem, ListBox -> option) during list item preparation. Expose ui5-li accessibleRole as public and allow explicit item role to override inherited role. Add Cypress coverage for inheritance and precedence behavior. Add local test page and website sample documenting accessible-role inheritance and overrides. Fixes #13264
1 parent c6a946a commit 37e9fef

9 files changed

Lines changed: 313 additions & 6 deletions

File tree

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

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2796,4 +2796,95 @@ describe("List sticky header", () => {
27962796
});
27972797
});
27982798
});
2799+
});
2800+
2801+
describe("List - ListItem accessible role inheritance", () => {
2802+
it("list items inherit 'menuitem' role when ui5-list has accessible-role='Menu'", () => {
2803+
cy.mount(
2804+
<List accessibleRole="Menu">
2805+
<ListItemStandard id="item1">Item 1</ListItemStandard>
2806+
<ListItemStandard id="item2">Item 2</ListItemStandard>
2807+
</List>
2808+
);
2809+
2810+
cy.get("#item1")
2811+
.shadow()
2812+
.find("li")
2813+
.should("have.attr", "role", "menuitem");
2814+
2815+
cy.get("#item2")
2816+
.shadow()
2817+
.find("li")
2818+
.should("have.attr", "role", "menuitem");
2819+
});
2820+
2821+
it("list items inherit 'option' role when ui5-list has accessible-role='ListBox'", () => {
2822+
cy.mount(
2823+
<List accessibleRole="ListBox">
2824+
<ListItemStandard id="item1">Item 1</ListItemStandard>
2825+
<ListItemStandard id="item2">Item 2</ListItemStandard>
2826+
</List>
2827+
);
2828+
2829+
cy.get("#item1")
2830+
.shadow()
2831+
.find("li")
2832+
.should("have.attr", "role", "option");
2833+
2834+
cy.get("#item2")
2835+
.shadow()
2836+
.find("li")
2837+
.should("have.attr", "role", "option");
2838+
});
2839+
2840+
it("list items keep 'listitem' role when ui5-list has default accessible-role='List'", () => {
2841+
cy.mount(
2842+
<List>
2843+
<ListItemStandard id="item1">Item 1</ListItemStandard>
2844+
</List>
2845+
);
2846+
2847+
cy.get("#item1")
2848+
.shadow()
2849+
.find("li")
2850+
.should("have.attr", "role", "listitem");
2851+
});
2852+
2853+
it("explicit accessible-role on ui5-li takes precedence over inherited role from ui5-list", () => {
2854+
cy.mount(
2855+
<List accessibleRole="Menu">
2856+
<ListItemStandard id="explicit" accessibleRole="TreeItem">Item 1</ListItemStandard>
2857+
<ListItemStandard id="inherited">Item 2</ListItemStandard>
2858+
</List>
2859+
);
2860+
2861+
cy.get("#explicit")
2862+
.shadow()
2863+
.find("li")
2864+
.should("have.attr", "role", "treeitem");
2865+
2866+
cy.get("#inherited")
2867+
.shadow()
2868+
.find("li")
2869+
.should("have.attr", "role", "menuitem");
2870+
});
2871+
2872+
it("list items can have an explicit accessible-role set without a parent ui5-list role", () => {
2873+
cy.mount(
2874+
<List>
2875+
<ListItemStandard id="item1" accessibleRole="MenuItem">Item 1</ListItemStandard>
2876+
<ListItemStandard id="item2">Item 2</ListItemStandard>
2877+
</List>
2878+
);
2879+
2880+
cy.get("#item1")
2881+
.shadow()
2882+
.find("li")
2883+
.should("have.attr", "role", "menuitem");
2884+
2885+
cy.get("#item2")
2886+
.shadow()
2887+
.find("li")
2888+
.should("have.attr", "role", "listitem");
2889+
});
27992890
});

packages/main/src/List.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,13 @@ const INFINITE_SCROLL_DEBOUNCE_RATE = 250; // ms
8080

8181
const PAGE_UP_DOWN_SIZE = 10;
8282

83+
// Maps the List's accessible-role to the expected child item ARIA role (lowercase)
84+
const LIST_ACCESSIBLE_ROLE_TO_ITEM_ROLE: Partial<Record<`${ListAccessibleRole}`, string>> = {
85+
Menu: "menuitem",
86+
Tree: "treeitem",
87+
ListBox: "option",
88+
};
89+
8390
// ListItemBase-based events
8491
type ListItemFocusEventDetail = {
8592
item: ListItemBase,
@@ -843,6 +850,7 @@ class List extends UI5Element {
843850

844851
prepareListItems() {
845852
const slottedItems = this.getItemsForProcessing();
853+
const inheritedItemRole = LIST_ACCESSIBLE_ROLE_TO_ITEM_ROLE[this.accessibleRole];
846854

847855
slottedItems.forEach((item, key) => {
848856
const isLastChild = key === slottedItems.length - 1;
@@ -851,6 +859,7 @@ class List extends UI5Element {
851859

852860
if (item.hasConfigurableMode) {
853861
(item as ListItem)._selectionMode = this.selectionMode;
862+
(item as ListItem)._inheritedAccessibleRole = inheritedItemRole;
854863
}
855864
item.hasBorder = showBottomBorder;
856865

packages/main/src/ListItem.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -180,17 +180,23 @@ abstract class ListItem extends ListItemBase {
180180

181181
/**
182182
* Used to define the role of the list item.
183-
* @private
184-
* @default "ListItem"
185-
* @since 1.3.0
186183
*
184+
* **Note:** If not set, the role is automatically inherited from the parent `ui5-list` based on its `accessible-role` property
185+
* (e.g. `Menu` -> `MenuItem`, `Tree` -> `TreeItem`, `ListBox` -> `Option`).
186+
* An explicitly set `accessible-role` on the list item takes precedence over the inherited role.
187+
* @default undefined
188+
* @public
189+
* @since 1.3.0
187190
*/
188191
@property()
189-
accessibleRole: `${ListItemAccessibleRole}` = "ListItem";
192+
accessibleRole?: `${Exclude<ListItemAccessibleRole, ListItemAccessibleRole.Group>}`;
190193

191194
@property()
192195
_forcedAccessibleRole?: string;
193196

197+
@property({ noAttribute: true })
198+
_inheritedAccessibleRole?: string;
199+
194200
@property()
195201
_selectionMode: `${ListSelectionMode}` = "None";
196202

@@ -436,7 +442,13 @@ abstract class ListItem extends ListItemBase {
436442
}
437443

438444
get listItemAccessibleRole() {
439-
return (this._forcedAccessibleRole || this.accessibleRole.toLowerCase()) as AriaRole | undefined;
445+
if (this._forcedAccessibleRole) {
446+
return this._forcedAccessibleRole as AriaRole;
447+
}
448+
if (this.accessibleRole) {
449+
return this.accessibleRole.toLowerCase() as AriaRole;
450+
}
451+
return (this._inheritedAccessibleRole || "listitem") as AriaRole;
440452
}
441453

442454
get ariaSelectedText() {
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
5+
<meta charset="utf-8">
6+
<title>ui5-list / ui5-li - accessible-role inheritance</title>
7+
<script src="%VITE_BUNDLE_PATH%" type="module"></script>
8+
<style>
9+
body {
10+
padding: 1rem 2rem;
11+
font-family: var(--sapFontFamily);
12+
}
13+
section {
14+
margin-bottom: 2rem;
15+
}
16+
</style>
17+
</head>
18+
<body>
19+
20+
<ui5-title level="H2">List accessible-role ? child role inheritance</ui5-title>
21+
<br/>
22+
23+
<!-- 1. Default (no role set) -->
24+
<section>
25+
<ui5-title level="H4">Default: accessible-role="List" (default) ? children get role="listitem"</ui5-title>
26+
<ui5-list id="listDefault" header-text="Default List">
27+
<ui5-li id="li-default-1">Item 1</ui5-li>
28+
<ui5-li id="li-default-2">Item 2</ui5-li>
29+
<ui5-li id="li-default-3">Item 3</ui5-li>
30+
</ui5-list>
31+
</section>
32+
33+
<!-- 2. Menu role -->
34+
<section>
35+
<ui5-title level="H4">accessible-role="Menu" ? children inherit role="menuitem"</ui5-title>
36+
<ui5-list id="listMenu" accessible-role="Menu" header-text="Menu List">
37+
<ui5-li id="li-menu-1">Open</ui5-li>
38+
<ui5-li id="li-menu-2">Save</ui5-li>
39+
<ui5-li id="li-menu-3">Save As?</ui5-li>
40+
<ui5-li id="li-menu-4">Close</ui5-li>
41+
</ui5-list>
42+
</section>
43+
44+
<!-- 3. ListBox role -->
45+
<section>
46+
<ui5-title level="H4">accessible-role="ListBox" ? children inherit role="option"</ui5-title>
47+
<ui5-list id="listBox" accessible-role="ListBox" header-text="ListBox">
48+
<ui5-li id="li-listbox-1">Option A</ui5-li>
49+
<ui5-li id="li-listbox-2">Option B</ui5-li>
50+
<ui5-li id="li-listbox-3">Option C</ui5-li>
51+
</ui5-list>
52+
</section>
53+
54+
<!-- 4. Tree role -->
55+
<section>
56+
<ui5-title level="H4">accessible-role="Tree" ? children inherit role="treeitem"</ui5-title>
57+
<ui5-list id="listTree" accessible-role="Tree" header-text="Tree List">
58+
<ui5-li id="li-tree-1">Node 1</ui5-li>
59+
<ui5-li id="li-tree-2">Node 2</ui5-li>
60+
<ui5-li id="li-tree-3">Node 3</ui5-li>
61+
</ui5-list>
62+
</section>
63+
64+
<!-- 5. Explicit accessible-role on ui5-li overrides inherited -->
65+
<section>
66+
<ui5-title level="H4">Explicit accessible-role on ui5-li overrides inherited role from ui5-list</ui5-title>
67+
<ui5-list id="listMenuOverride" accessible-role="Menu" header-text="Menu with override">
68+
<ui5-li id="li-override-1">Inherits menuitem from list</ui5-li>
69+
<ui5-li id="li-override-2" accessible-role="Option">Explicitly set to "option"</ui5-li>
70+
<ui5-li id="li-override-3">Inherits menuitem from list</ui5-li>
71+
</ui5-list>
72+
</section>
73+
74+
<!-- 6. Standalone explicit accessible-role on ui5-li -->
75+
<section>
76+
<ui5-title level="H4">Explicit accessible-role="MenuItem" set directly on ui5-li (no list role)</ui5-title>
77+
<ui5-list id="listStandalone" header-text="Standard list with explicit item role">
78+
<ui5-li id="li-standalone-1" accessible-role="MenuItem">Explicit menuitem</ui5-li>
79+
<ui5-li id="li-standalone-2">Default listitem</ui5-li>
80+
<ui5-li id="li-standalone-3">Default listitem</ui5-li>
81+
</ui5-list>
82+
</section>
83+
84+
</body>
85+
</html>

packages/website/docs/_components_pages/main/List/List.mdx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import DragAndDrop from "../../../_samples/main/List/DragAndDrop/DragAndDrop.md"
1414
import MultipleDrag from "../../../_samples/main/List/MultipleDrag/MultipleDrag.md";
1515
import WrappingBehavior from "../../../_samples/main/List/WrappingBehavior/WrappingBehavior.md";
1616
import StickyHeader from "../../../_samples/main/List/StickyHeader/StickyHeader.md";
17+
import AccessibleRole from "../../../_samples/main/List/AccessibleRole/AccessibleRole.md";
1718

1819
<%COMPONENT_OVERVIEW%>
1920

@@ -86,4 +87,12 @@ The `<ui5-li-custom>` is intentionally designed as a generic container to provid
8687
### Sticky Header
8788
Use the <b>stickyHeader</b> property to keep the header visible during scrolling.
8889

89-
<StickyHeader/>
90+
<StickyHeader/>
91+
92+
### Accessible Role
93+
Use the <b>accessibleRole</b> property on `ui5-list` to set the ARIA role of the list element.
94+
Child `ui5-li` items automatically inherit the matching ARIA role - `menuitem` for `Menu`, `option` for `ListBox`, and `treeitem` for `Tree` - so you do not need to set it manually on every item.
95+
96+
Setting an explicit `accessible-role` attribute on an individual `ui5-li` takes precedence over the role inherited from the parent list.
97+
98+
<AccessibleRole />
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import html from '!!raw-loader!./sample.html';
2+
import js from '!!raw-loader!./main.js';
3+
import react from '!!raw-loader!./sample.tsx';
4+
5+
<Editor html={html} js={js} react={react} />
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import "@ui5/webcomponents/dist/List.js";
2+
import "@ui5/webcomponents/dist/ListItemStandard.js";
3+
4+
import "@ui5/webcomponents-icons/dist/create.js";
5+
import "@ui5/webcomponents-icons/dist/save.js";
6+
import "@ui5/webcomponents-icons/dist/delete.js";
7+
import "@ui5/webcomponents-icons/dist/filter.js";
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<!-- playground-fold -->
2+
<!DOCTYPE html>
3+
<html lang="en">
4+
5+
<head>
6+
<meta charset="UTF-8">
7+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
8+
<title>Sample</title>
9+
</head>
10+
11+
<body style="background-color: var(--sapBackgroundColor)">
12+
<!-- playground-fold-end -->
13+
14+
<!-- Menu: list role="menu", items inherit role="menuitem" -->
15+
<ui5-list accessible-role="Menu" header-text="Actions">
16+
<ui5-li icon="create">New Document</ui5-li>
17+
<ui5-li icon="save">Save</ui5-li>
18+
<ui5-li icon="delete">Delete</ui5-li>
19+
</ui5-list>
20+
21+
<br>
22+
23+
<!-- ListBox: list role="listbox", items inherit role="option" -->
24+
<ui5-list accessible-role="ListBox" header-text="Select a Country">
25+
<ui5-li>Argentina</ui5-li>
26+
<ui5-li>Bulgaria</ui5-li>
27+
<ui5-li>China</ui5-li>
28+
</ui5-list>
29+
30+
<br>
31+
32+
<!-- Explicit accessible-role on ui5-li overrides the inherited role -->
33+
<ui5-list accessible-role="Menu" header-text="Mixed Roles (override)">
34+
<ui5-li icon="create">Inherits menuitem</ui5-li>
35+
<ui5-li icon="filter" accessible-role="None">Separator-like (role=none)</ui5-li>
36+
<ui5-li icon="save">Inherits menuitem</ui5-li>
37+
</ui5-list>
38+
39+
<!-- playground-fold -->
40+
<script type="module" src="main.js"></script>
41+
</body>
42+
43+
</html>
44+
<!-- playground-fold-end -->
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import createReactComponent from "@ui5/webcomponents-base/dist/createReactComponent.js";
2+
import ListClass from "@ui5/webcomponents/dist/List.js";
3+
import ListItemStandardClass from "@ui5/webcomponents/dist/ListItemStandard.js";
4+
import "@ui5/webcomponents-icons/dist/create.js";
5+
import "@ui5/webcomponents-icons/dist/save.js";
6+
import "@ui5/webcomponents-icons/dist/delete.js";
7+
import "@ui5/webcomponents-icons/dist/filter.js";
8+
9+
const List = createReactComponent(ListClass);
10+
const ListItemStandard = createReactComponent(ListItemStandardClass);
11+
12+
function App() {
13+
return (
14+
<>
15+
{/* Menu: list role="menu", items inherit role="menuitem" */}
16+
<List accessibleRole="Menu" headerText="Actions">
17+
<ListItemStandard icon="create">New Document</ListItemStandard>
18+
<ListItemStandard icon="save">Save</ListItemStandard>
19+
<ListItemStandard icon="delete">Delete</ListItemStandard>
20+
</List>
21+
22+
<br />
23+
24+
{/* ListBox: list role="listbox", items inherit role="option" */}
25+
<List accessibleRole="ListBox" headerText="Select a Country">
26+
<ListItemStandard>Argentina</ListItemStandard>
27+
<ListItemStandard>Bulgaria</ListItemStandard>
28+
<ListItemStandard>China</ListItemStandard>
29+
</List>
30+
31+
<br />
32+
33+
{/* Explicit accessible-role on ui5-li overrides the inherited role */}
34+
<List accessibleRole="Menu" headerText="Mixed Roles (override)">
35+
<ListItemStandard icon="create">Inherits menuitem</ListItemStandard>
36+
<ListItemStandard icon="filter" accessibleRole="None">
37+
Separator-like (role=none)
38+
</ListItemStandard>
39+
<ListItemStandard icon="save">Inherits menuitem</ListItemStandard>
40+
</List>
41+
</>
42+
);
43+
}
44+
45+
export default App;

0 commit comments

Comments
 (0)