feat(new room list): improve list accessibility
This commit is contained in:
@@ -5,8 +5,9 @@
|
|||||||
* Please see LICENSE files in the repository root for full details.
|
* 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 { AutoSizer, List, type ListRowProps } from "react-virtualized";
|
||||||
|
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { type RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel";
|
import { type RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel";
|
||||||
import { _t } from "../../../../languageHandler";
|
import { _t } from "../../../../languageHandler";
|
||||||
@@ -30,9 +31,11 @@ export function RoomList({ vm: { rooms, openRoom } }: RoomListProps): JSX.Elemen
|
|||||||
[rooms, openRoom],
|
[rooms, openRoom],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const ref = useAccessibleList(rooms);
|
||||||
|
|
||||||
// The first div is needed to make the virtualized list take all the remaining space and scroll correctly
|
// The first div is needed to make the virtualized list take all the remaining space and scroll correctly
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomList" data-testid="room-list">
|
<div className="mx_RoomList" data-testid="room-list" ref={ref}>
|
||||||
<AutoSizer>
|
<AutoSizer>
|
||||||
{({ height, width }) => (
|
{({ height, width }) => (
|
||||||
<List
|
<List
|
||||||
@@ -43,9 +46,61 @@ export function RoomList({ vm: { rooms, openRoom } }: RoomListProps): JSX.Elemen
|
|||||||
rowHeight={48}
|
rowHeight={48}
|
||||||
height={height}
|
height={height}
|
||||||
width={width}
|
width={width}
|
||||||
|
role="listbox"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</AutoSizer>
|
</AutoSizer>
|
||||||
</div>
|
</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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ export function RoomListCell({ room, ...props }: RoomListCellProps): JSX.Element
|
|||||||
type="button"
|
type="button"
|
||||||
aria-label={_t("room_list|room|open_room", { roomName: room.name })}
|
aria-label={_t("room_list|room|open_room", { roomName: room.name })}
|
||||||
{...props}
|
{...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 */}
|
{/* 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">
|
<Flex className="mx_RoomListCell_container" gap="var(--cpd-space-3x)" align="center">
|
||||||
|
|||||||
Reference in New Issue
Block a user