feat(new room list): improve list accessibility

This commit is contained in:
Florian Duros
2025-03-05 18:54:56 +01:00
parent 90cc44b340
commit 3044fefbd7
2 changed files with 59 additions and 2 deletions

View File

@@ -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 (
<div className="mx_RoomList" data-testid="room-list">
<div className="mx_RoomList" data-testid="room-list" ref={ref}>
<AutoSizer>
{({ height, width }) => (
<List
@@ -43,9 +46,61 @@ export function RoomList({ vm: { rooms, openRoom } }: RoomListProps): JSX.Elemen
rowHeight={48}
height={height}
width={width}
role="listbox"
/>
)}
</AutoSizer>
</div>
);
}
/**
* 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<HTMLDivElement> {
// To be put on the list node
const ref = useRef<HTMLDivElement>(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;
}

View File

@@ -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 */}
<Flex className="mx_RoomListCell_container" gap="var(--cpd-space-3x)" align="center">