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 */}