diff --git a/src/components/views/rooms/RoomListPanel/RoomList.tsx b/src/components/views/rooms/RoomListPanel/RoomList.tsx index 3645a72bb9..4b51dcbaa4 100644 --- a/src/components/views/rooms/RoomListPanel/RoomList.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomList.tsx @@ -5,8 +5,9 @@ * Please see LICENSE files in the repository root for full details. */ -import React, { useCallback, type JSX } from "react"; +import React, { useCallback, type JSX, useEffect, useRef, type RefObject } from "react"; import { AutoSizer, List, type ListRowProps } from "react-virtualized"; +import { type Room } from "matrix-js-sdk/src/matrix"; import { type RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel"; import { _t } from "../../../../languageHandler"; @@ -30,9 +31,11 @@ export function RoomList({ vm: { rooms, openRoom } }: RoomListProps): JSX.Elemen [rooms, openRoom], ); + const ref = useAccessibleList(rooms); + // The first div is needed to make the virtualized list take all the remaining space and scroll correctly return ( -
+
{({ height, width }) => ( )}
); } + +/** + * Make the list of rooms accessible. The ref should be put on the list node. + * + * The list rendered by react-virtualized has not the best role and attributes for accessibility. + * The react-virtualized list has the following a11y attributes: "grid" -> "row" -> ["gridcell", "gridcell", "gridcell"]. + * Using a grid role is not the best choice for a list of items, we want instead a listbox role with children having an option role. + * + * The listbox and option roles can be set directly in the jsx of the `List` and the `RoomListCell` component but the "gridcell" role is remaining. + * This hook removes the "gridcell" role from the list items and set "aria-setsize" on the list too. + * + * @returns The ref to put on the list node. + */ +function useAccessibleList(rooms: Room[]): RefObject { + // To be put on the list node + const ref = useRef(null); + useEffect(() => { + const list = ref.current?.querySelector('[role="listbox"]'); + if (!list) return; + + // The list is virtualized so the number of items in the dom is not the same as the number of rooms + list.setAttribute("aria-setsize", `${rooms.length}`); + + // Determine if a node is a row node + const isRowNode = (node: HTMLElement): boolean => node.getAttribute("role") === "row"; + + // Watch the node with the "row" role to be added to the dom and remove the role + // If the role is re-added/modified later, we remove it too + const observer = new MutationObserver((mutationList) => { + for (const mutation of mutationList) { + if (mutation.type === "childList") { + mutation.addedNodes.forEach((node) => { + if (node instanceof HTMLElement && isRowNode(node)) { + node.removeAttribute("role"); + } + }); + } else if ( + mutation.type === "attributes" && + mutation.target instanceof HTMLElement && + isRowNode(mutation.target) + ) { + mutation.target.removeAttribute("role"); + } + } + }); + observer.observe(list, { childList: true, subtree: true, attributeFilter: ["role"] }); + return () => observer.disconnect(); + }, [rooms]); + + return ref; +} diff --git a/src/components/views/rooms/RoomListPanel/RoomListCell.tsx b/src/components/views/rooms/RoomListPanel/RoomListCell.tsx index a5e9cc5df2..4d86c36a0a 100644 --- a/src/components/views/rooms/RoomListPanel/RoomListCell.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomListCell.tsx @@ -29,6 +29,8 @@ export function RoomListCell({ room, ...props }: RoomListCellProps): JSX.Element type="button" aria-label={_t("room_list|room|open_room", { roomName: room.name })} {...props} + role="option" + aria-selected={false} > {/* We need this extra div between the button and the content in order to add a padding which is not messing with the virtualized list */}