diff --git a/packages/main/cypress/specs/List.cy.tsx b/packages/main/cypress/specs/List.cy.tsx index 0e3acd85315d..4a45f6b35892 100644 --- a/packages/main/cypress/specs/List.cy.tsx +++ b/packages/main/cypress/specs/List.cy.tsx @@ -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( + + Item 1 + Item 2 + + ); + + 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( + + Item 1 + Item 2 + + ); + + 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( + + Item 1 + + ); + + 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( + + Item 1 + Item 2 + + ); + + 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( + + Item 1 + Item 2 + + ); + + cy.get("#item1") + .shadow() + .find("li") + .should("have.attr", "role", "menuitem"); + + cy.get("#item2") + .shadow() + .find("li") + .should("have.attr", "role", "listitem"); + }); }); \ No newline at end of file diff --git a/packages/main/src/List.ts b/packages/main/src/List.ts index 4ce0a29f3332..4a116f63ed22 100644 --- a/packages/main/src/List.ts +++ b/packages/main/src/List.ts @@ -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> = { + Menu: "menuitem", + Tree: "treeitem", + ListBox: "option", +}; + // ListItemBase-based events type ListItemFocusEventDetail = { item: ListItemBase, @@ -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; @@ -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; diff --git a/packages/main/src/ListItem.ts b/packages/main/src/ListItem.ts index f2d64f0227e1..eb763f8b39f4 100644 --- a/packages/main/src/ListItem.ts +++ b/packages/main/src/ListItem.ts @@ -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" + * @public * @since 1.3.0 - * */ @property() - accessibleRole: `${ListItemAccessibleRole}` = "ListItem"; + accessibleRole?: `${ListItemAccessibleRole}`; @property() _forcedAccessibleRole?: string; + @property({ noAttribute: true }) + _inheritedAccessibleRole?: string; + @property() _selectionMode: `${ListSelectionMode}` = "None"; @@ -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() { diff --git a/packages/main/test/pages/List_accessible_role.html b/packages/main/test/pages/List_accessible_role.html new file mode 100644 index 000000000000..9320e3169337 --- /dev/null +++ b/packages/main/test/pages/List_accessible_role.html @@ -0,0 +1,85 @@ + + + + + + ui5-list / ui5-li - accessible-role inheritance + + + + + + List accessible-role ? child role inheritance +
+ + +
+ Default: accessible-role="List" (default) ? children get role="listitem" + + Item 1 + Item 2 + Item 3 + +
+ + +
+ accessible-role="Menu" ? children inherit role="menuitem" + + Open + Save + Save As? + Close + +
+ + +
+ accessible-role="ListBox" ? children inherit role="option" + + Option A + Option B + Option C + +
+ + +
+ accessible-role="Tree" ? children inherit role="treeitem" + + Node 1 + Node 2 + Node 3 + +
+ + +
+ Explicit accessible-role on ui5-li overrides inherited role from ui5-list + + Inherits menuitem from list + Explicitly set to "option" + Inherits menuitem from list + +
+ + +
+ Explicit accessible-role="MenuItem" set directly on ui5-li (no list role) + + Explicit menuitem + Default listitem + Default listitem + +
+ + + diff --git a/packages/website/docs/_components_pages/main/List/List.mdx b/packages/website/docs/_components_pages/main/List/List.mdx index a203bc9f84c4..cbacf726b022 100644 --- a/packages/website/docs/_components_pages/main/List/List.mdx +++ b/packages/website/docs/_components_pages/main/List/List.mdx @@ -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%> @@ -86,4 +87,12 @@ The `` is intentionally designed as a generic container to provid ### Sticky Header Use the stickyHeader property to keep the header visible during scrolling. - \ No newline at end of file + + +### Accessible Role +Use the accessibleRole 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. + + \ No newline at end of file diff --git a/packages/website/docs/_samples/main/List/AccessibleRole/AccessibleRole.md b/packages/website/docs/_samples/main/List/AccessibleRole/AccessibleRole.md new file mode 100644 index 000000000000..0c062a836e84 --- /dev/null +++ b/packages/website/docs/_samples/main/List/AccessibleRole/AccessibleRole.md @@ -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'; + + diff --git a/packages/website/docs/_samples/main/List/AccessibleRole/main.js b/packages/website/docs/_samples/main/List/AccessibleRole/main.js new file mode 100644 index 000000000000..5a1af4371d9d --- /dev/null +++ b/packages/website/docs/_samples/main/List/AccessibleRole/main.js @@ -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"; diff --git a/packages/website/docs/_samples/main/List/AccessibleRole/sample.html b/packages/website/docs/_samples/main/List/AccessibleRole/sample.html new file mode 100644 index 000000000000..73e640261217 --- /dev/null +++ b/packages/website/docs/_samples/main/List/AccessibleRole/sample.html @@ -0,0 +1,44 @@ + + + + + + + + Sample + + + + + + + + New Document + Save + Delete + + +
+ + + + Argentina + Bulgaria + China + + +
+ + + + Inherits menuitem + Separator-like (role=none) + Inherits menuitem + + + + + + + + diff --git a/packages/website/docs/_samples/main/List/AccessibleRole/sample.tsx b/packages/website/docs/_samples/main/List/AccessibleRole/sample.tsx new file mode 100644 index 000000000000..605b8f8cbcc7 --- /dev/null +++ b/packages/website/docs/_samples/main/List/AccessibleRole/sample.tsx @@ -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" */} + + New Document + Save + Delete + + +
+ + {/* ListBox: list role="listbox", items inherit role="option" */} + + Argentina + Bulgaria + China + + +
+ + {/* Explicit accessible-role on ui5-li overrides the inherited role */} + + Inherits menuitem + + Separator-like (role=none) + + Inherits menuitem + + + ); +} + +export default App;