Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions packages/main/cypress/specs/List.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2796,4 +2796,95 @@ describe("List sticky header", () => {
});
});
});
});

describe("List - ListItem accessible role inheritance", () => {
it("list items inherit 'menuitem' role when ui5-list has accessible-role='Menu'", () => {
cy.mount(
<List accessibleRole="Menu">
<ListItemStandard id="item1">Item 1</ListItemStandard>
<ListItemStandard id="item2">Item 2</ListItemStandard>
</List>
);

cy.get("#item1")
.shadow()
.find("li")
.should("have.attr", "role", "menuitem");

cy.get("#item2")
.shadow()
.find("li")
.should("have.attr", "role", "menuitem");
});

it("list items inherit 'option' role when ui5-list has accessible-role='ListBox'", () => {
cy.mount(
<List accessibleRole="ListBox">
<ListItemStandard id="item1">Item 1</ListItemStandard>
<ListItemStandard id="item2">Item 2</ListItemStandard>
</List>
);

cy.get("#item1")
.shadow()
.find("li")
.should("have.attr", "role", "option");

cy.get("#item2")
.shadow()
.find("li")
.should("have.attr", "role", "option");
});

it("list items keep 'listitem' role when ui5-list has default accessible-role='List'", () => {
cy.mount(
<List>
<ListItemStandard id="item1">Item 1</ListItemStandard>
</List>
);

cy.get("#item1")
.shadow()
.find("li")
.should("have.attr", "role", "listitem");
});

it("explicit accessible-role on ui5-li takes precedence over inherited role from ui5-list", () => {
cy.mount(
<List accessibleRole="Menu">
<ListItemStandard id="explicit" accessibleRole="TreeItem">Item 1</ListItemStandard>
<ListItemStandard id="inherited">Item 2</ListItemStandard>
</List>
);

cy.get("#explicit")
.shadow()
.find("li")
.should("have.attr", "role", "treeitem");

cy.get("#inherited")
.shadow()
.find("li")
.should("have.attr", "role", "menuitem");
});

it("list items can have an explicit accessible-role set without a parent ui5-list role", () => {
cy.mount(
<List>
<ListItemStandard id="item1" accessibleRole="MenuItem">Item 1</ListItemStandard>
<ListItemStandard id="item2">Item 2</ListItemStandard>
</List>
);

cy.get("#item1")
.shadow()
.find("li")
.should("have.attr", "role", "menuitem");

cy.get("#item2")
.shadow()
.find("li")
.should("have.attr", "role", "listitem");
});
});
9 changes: 9 additions & 0 deletions packages/main/src/List.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ const INFINITE_SCROLL_DEBOUNCE_RATE = 250; // ms

const PAGE_UP_DOWN_SIZE = 10;

// Maps the List's accessible-role to the expected child item ARIA role (lowercase)
const LIST_ACCESSIBLE_ROLE_TO_ITEM_ROLE: Partial<Record<`${ListAccessibleRole}`, string>> = {
Menu: "menuitem",
Tree: "treeitem",
ListBox: "option",
};

// ListItemBase-based events
type ListItemFocusEventDetail = {
item: ListItemBase,
Expand Down Expand Up @@ -843,6 +850,7 @@ class List extends UI5Element {

prepareListItems() {
const slottedItems = this.getItemsForProcessing();
const inheritedItemRole = LIST_ACCESSIBLE_ROLE_TO_ITEM_ROLE[this.accessibleRole];

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

if (item.hasConfigurableMode) {
(item as ListItem)._selectionMode = this.selectionMode;
(item as ListItem)._inheritedAccessibleRole = inheritedItemRole;
}
item.hasBorder = showBottomBorder;

Expand Down
20 changes: 16 additions & 4 deletions packages/main/src/ListItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,17 +180,23 @@ abstract class ListItem extends ListItemBase {

/**
* Used to define the role of the list item.
* @private
*
* **Note:** If not set, the role is automatically inherited from the parent `ui5-list` based on its `accessible-role` property
* (e.g. `Menu` -> `MenuItem`, `Tree` -> `TreeItem`, `ListBox` -> `Option`).
* An explicitly set `accessible-role` on the list item takes precedence over the inherited role.
* @default "ListItem"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the @default "ListItem" tag be updated to @default undefined? element.accessibleRole returns undefined when unset, even though the rendered role correctly falls back to "listitem" via the getter.

* @public
* @since 1.3.0
*
*/
@property()
accessibleRole: `${ListItemAccessibleRole}` = "ListItem";
accessibleRole?: `${ListItemAccessibleRole}`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the type be narrowed to exclude Group? Using ${ListItemAccessibleRole} exposes the @Private Group member, which allows setting accessible-role="Group" on a <ui5-li> and renders role="group" on an interactive item.


@property()
_forcedAccessibleRole?: string;

@property({ noAttribute: true })
_inheritedAccessibleRole?: string;

@property()
_selectionMode: `${ListSelectionMode}` = "None";

Expand Down Expand Up @@ -436,7 +442,13 @@ abstract class ListItem extends ListItemBase {
}

get listItemAccessibleRole() {
return (this._forcedAccessibleRole || this.accessibleRole.toLowerCase()) as AriaRole | undefined;
if (this._forcedAccessibleRole) {
return this._forcedAccessibleRole as AriaRole;
}
if (this.accessibleRole) {
return this.accessibleRole.toLowerCase() as AriaRole;
}
return (this._inheritedAccessibleRole || "listitem") as AriaRole;
}

get ariaSelectedText() {
Expand Down
85 changes: 85 additions & 0 deletions packages/main/test/pages/List_accessible_role.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta charset="utf-8">
<title>ui5-list / ui5-li - accessible-role inheritance</title>
<script src="%VITE_BUNDLE_PATH%" type="module"></script>
<style>
body {
padding: 1rem 2rem;
font-family: var(--sapFontFamily);
}
section {
margin-bottom: 2rem;
}
</style>
</head>
<body>

<ui5-title level="H2">List accessible-role ? child role inheritance</ui5-title>
<br/>

<!-- 1. Default (no role set) -->
<section>
<ui5-title level="H4">Default: accessible-role="List" (default) ? children get role="listitem"</ui5-title>
<ui5-list id="listDefault" header-text="Default List">
<ui5-li id="li-default-1">Item 1</ui5-li>
<ui5-li id="li-default-2">Item 2</ui5-li>
<ui5-li id="li-default-3">Item 3</ui5-li>
</ui5-list>
</section>

<!-- 2. Menu role -->
<section>
<ui5-title level="H4">accessible-role="Menu" ? children inherit role="menuitem"</ui5-title>
<ui5-list id="listMenu" accessible-role="Menu" header-text="Menu List">
<ui5-li id="li-menu-1">Open</ui5-li>
<ui5-li id="li-menu-2">Save</ui5-li>
<ui5-li id="li-menu-3">Save As?</ui5-li>
<ui5-li id="li-menu-4">Close</ui5-li>
</ui5-list>
</section>

<!-- 3. ListBox role -->
<section>
<ui5-title level="H4">accessible-role="ListBox" ? children inherit role="option"</ui5-title>
<ui5-list id="listBox" accessible-role="ListBox" header-text="ListBox">
<ui5-li id="li-listbox-1">Option A</ui5-li>
<ui5-li id="li-listbox-2">Option B</ui5-li>
<ui5-li id="li-listbox-3">Option C</ui5-li>
</ui5-list>
</section>

<!-- 4. Tree role -->
<section>
<ui5-title level="H4">accessible-role="Tree" ? children inherit role="treeitem"</ui5-title>
<ui5-list id="listTree" accessible-role="Tree" header-text="Tree List">
<ui5-li id="li-tree-1">Node 1</ui5-li>
<ui5-li id="li-tree-2">Node 2</ui5-li>
<ui5-li id="li-tree-3">Node 3</ui5-li>
</ui5-list>
</section>

<!-- 5. Explicit accessible-role on ui5-li overrides inherited -->
<section>
<ui5-title level="H4">Explicit accessible-role on ui5-li overrides inherited role from ui5-list</ui5-title>
<ui5-list id="listMenuOverride" accessible-role="Menu" header-text="Menu with override">
<ui5-li id="li-override-1">Inherits menuitem from list</ui5-li>
<ui5-li id="li-override-2" accessible-role="Option">Explicitly set to "option"</ui5-li>
<ui5-li id="li-override-3">Inherits menuitem from list</ui5-li>
</ui5-list>
</section>

<!-- 6. Standalone explicit accessible-role on ui5-li -->
<section>
<ui5-title level="H4">Explicit accessible-role="MenuItem" set directly on ui5-li (no list role)</ui5-title>
<ui5-list id="listStandalone" header-text="Standard list with explicit item role">
<ui5-li id="li-standalone-1" accessible-role="MenuItem">Explicit menuitem</ui5-li>
<ui5-li id="li-standalone-2">Default listitem</ui5-li>
<ui5-li id="li-standalone-3">Default listitem</ui5-li>
</ui5-list>
</section>

</body>
</html>
11 changes: 10 additions & 1 deletion packages/website/docs/_components_pages/main/List/List.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import DragAndDrop from "../../../_samples/main/List/DragAndDrop/DragAndDrop.md"
import MultipleDrag from "../../../_samples/main/List/MultipleDrag/MultipleDrag.md";
import WrappingBehavior from "../../../_samples/main/List/WrappingBehavior/WrappingBehavior.md";
import StickyHeader from "../../../_samples/main/List/StickyHeader/StickyHeader.md";
import AccessibleRole from "../../../_samples/main/List/AccessibleRole/AccessibleRole.md";

<%COMPONENT_OVERVIEW%>

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

<StickyHeader/>
<StickyHeader/>

### Accessible Role
Use the <b>accessibleRole</b> property on `ui5-list` to set the ARIA role of the list element.
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.

Setting an explicit `accessible-role` attribute on an individual `ui5-li` takes precedence over the role inherited from the parent list.

<AccessibleRole />
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import html from '!!raw-loader!./sample.html';
import js from '!!raw-loader!./main.js';
import react from '!!raw-loader!./sample.tsx';

<Editor html={html} js={js} react={react} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import "@ui5/webcomponents/dist/List.js";
import "@ui5/webcomponents/dist/ListItemStandard.js";

import "@ui5/webcomponents-icons/dist/create.js";
import "@ui5/webcomponents-icons/dist/save.js";
import "@ui5/webcomponents-icons/dist/delete.js";
import "@ui5/webcomponents-icons/dist/filter.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<!-- playground-fold -->
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sample</title>
</head>

<body style="background-color: var(--sapBackgroundColor)">
<!-- playground-fold-end -->

<!-- Menu: list role="menu", items inherit role="menuitem" -->
<ui5-list accessible-role="Menu" header-text="Actions">
<ui5-li icon="create">New Document</ui5-li>
<ui5-li icon="save">Save</ui5-li>
<ui5-li icon="delete">Delete</ui5-li>
</ui5-list>

<br>

<!-- ListBox: list role="listbox", items inherit role="option" -->
<ui5-list accessible-role="ListBox" header-text="Select a Country">
<ui5-li>Argentina</ui5-li>
<ui5-li>Bulgaria</ui5-li>
<ui5-li>China</ui5-li>
</ui5-list>

<br>

<!-- Explicit accessible-role on ui5-li overrides the inherited role -->
<ui5-list accessible-role="Menu" header-text="Mixed Roles (override)">
<ui5-li icon="create">Inherits menuitem</ui5-li>
<ui5-li icon="filter" accessible-role="None">Separator-like (role=none)</ui5-li>
<ui5-li icon="save">Inherits menuitem</ui5-li>
</ui5-list>

<!-- playground-fold -->
<script type="module" src="main.js"></script>
</body>

</html>
<!-- playground-fold-end -->
45 changes: 45 additions & 0 deletions packages/website/docs/_samples/main/List/AccessibleRole/sample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import createReactComponent from "@ui5/webcomponents-base/dist/createReactComponent.js";
import ListClass from "@ui5/webcomponents/dist/List.js";
import ListItemStandardClass from "@ui5/webcomponents/dist/ListItemStandard.js";
import "@ui5/webcomponents-icons/dist/create.js";
import "@ui5/webcomponents-icons/dist/save.js";
import "@ui5/webcomponents-icons/dist/delete.js";
import "@ui5/webcomponents-icons/dist/filter.js";

const List = createReactComponent(ListClass);
const ListItemStandard = createReactComponent(ListItemStandardClass);

function App() {
return (
<>
{/* Menu: list role="menu", items inherit role="menuitem" */}
<List accessibleRole="Menu" headerText="Actions">
<ListItemStandard icon="create">New Document</ListItemStandard>
<ListItemStandard icon="save">Save</ListItemStandard>
<ListItemStandard icon="delete">Delete</ListItemStandard>
</List>

<br />

{/* ListBox: list role="listbox", items inherit role="option" */}
<List accessibleRole="ListBox" headerText="Select a Country">
<ListItemStandard>Argentina</ListItemStandard>
<ListItemStandard>Bulgaria</ListItemStandard>
<ListItemStandard>China</ListItemStandard>
</List>

<br />

{/* Explicit accessible-role on ui5-li overrides the inherited role */}
<List accessibleRole="Menu" headerText="Mixed Roles (override)">
<ListItemStandard icon="create">Inherits menuitem</ListItemStandard>
<ListItemStandard icon="filter" accessibleRole="None">
Separator-like (role=none)
</ListItemStandard>
<ListItemStandard icon="save">Inherits menuitem</ListItemStandard>
</List>
</>
);
}

export default App;
Loading