Move the room list to the new ListView(backed by react-virtuoso) (#30515)

* Move Room List to ListView

- Also remove Space/Enter handing from keyboard navigation we can just leave the default behaviour of those keys and handle via onClick

* Update rooms when the primary filter changes

Otherwise when changing spaces, the filter does not reset until the next update to the RVS is made.

* Fix stickyRow/scrollIntoView when switiching space or changing filters

- Also remove the rest of space/enter keyboard handling use

* Remove the rest of space/enter keyboard handling use

* Remove useCombinedRef and add @radix-ui/react-compose-refs as we already depend on it

- Also remove eact-virtualized dep

* Update RoomList unit test

* Update snapshots and unit tests

* Fix e2e tests

* Remove react-virtualized from tests

* Fix e2e flake

* Update more screenshots

* Fix e2e test case where were should scroll to the top when the active room is no longer in the list

* Move from gitpkg to package-patch

* Update to latest react virtuoso release/api.

Also pass spaceId to the room list and scroll the activeIndex into view when spaceId or primaryFilter change.

* Use listbox/option roles to improve ScreenReader experience

* Change onKeyDown e.stopPropogation to cover context menu

* lint

* Remove unneeded exposure of the listView ref

Also move scrollIntoViewOnChange to useCallback

* Update unit test and snapshot

* Fix e2e tests and update screenshots

* Fix unit test and snapshot

* Update more unit tests

* Fix keyboard shortcuts and e2e test

* Fix another e2e and unit test

* lint

* Improve the naming for RoomResult and the documentation on it's fields meaning.

Also update the login in RoomList to check for any change in filters, this is a bit more future proof for when we introduce multi select than using activePrimaryFilter.

* Put back and fix landmark tests

* Fix test import

* Add comment regarding context object getting rendered.

* onKeyDown should be optional

* Use SpaceKey type on RoomResult

* lint
This commit is contained in:
David Langley
2025-08-21 15:43:40 +01:00
committed by GitHub
parent ef3a6a9429
commit c842b615db
50 changed files with 1139 additions and 1021 deletions

View File

@@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React, { useRef, type JSX, useCallback, useEffect, useState } from "react";
import { type VirtuosoHandle, type ListRange, Virtuoso, type VirtuosoProps } from "react-virtuoso";
import { isModifiedKeyEvent, Key } from "../../Keyboard";
/**
* Context object passed to each list item containing the currently focused key
* and any additional context data from the parent component.
@@ -34,6 +35,7 @@ export interface IListViewProps<Item, Context>
* @param index - The index of the item in the list
* @param item - The data item to render
* @param context - The context object containing the focused key and any additional data
* @param onFocus - A callback that is required to be called when the item component receives focus
* @returns JSX element representing the rendered item
*/
getItemComponent: (
@@ -62,6 +64,14 @@ export interface IListViewProps<Item, Context>
* @return The key to use for focusing the item
*/
getItemKey: (item: Item) => string;
/**
* Callback function to handle key down events on the list container.
* ListView handles keyboard navigation for focus(up, down, home, end, pageUp, pageDown)
* and stops propagation otherwise the event bubbles and this callback is called for the use of the parent.
* @param e - The keyboard event
* @returns
*/
onKeyDown?: (e: React.KeyboardEvent<HTMLDivElement>) => void;
}
/**
@@ -73,7 +83,7 @@ export interface IListViewProps<Item, Context>
*/
export function ListView<Item, Context = any>(props: IListViewProps<Item, Context>): React.ReactElement {
// Extract our custom props to avoid conflicts with Virtuoso props
const { items, getItemComponent, isItemFocusable, getItemKey, context, ...virtuosoProps } = props;
const { items, getItemComponent, isItemFocusable, getItemKey, context, onKeyDown, ...virtuosoProps } = props;
/** Reference to the Virtuoso component for programmatic scrolling */
const virtuosoHandleRef = useRef<VirtuosoHandle>(null);
/** Reference to the DOM element containing the virtualized list */
@@ -125,7 +135,7 @@ export function ListView<Item, Context = any>(props: IListViewProps<Item, Contex
const key = getItemKey(items[clampedIndex]);
setTabIndexKey(key);
isScrollingToItem.current = true;
virtuosoHandleRef?.current?.scrollIntoView({
virtuosoHandleRef.current?.scrollIntoView({
index: clampedIndex,
align: align,
behavior: "auto",
@@ -168,40 +178,44 @@ export function ListView<Item, Context = any>(props: IListViewProps<Item, Contex
* Supports Arrow keys, Home, End, Page Up/Down, Enter, and Space.
*/
const keyDownCallback = useCallback(
(e: React.KeyboardEvent) => {
if (!e) return; // Guard against null/undefined events
(e: React.KeyboardEvent<HTMLDivElement>) => {
const currentIndex = tabIndexKey ? keyToIndexMap.get(tabIndexKey) : undefined;
let handled = false;
if (e.code === "ArrowUp" && currentIndex !== undefined) {
scrollToItem(currentIndex - 1, false);
handled = true;
} else if (e.code === "ArrowDown" && currentIndex !== undefined) {
scrollToItem(currentIndex + 1, true);
handled = true;
} else if (e.code === "Home") {
scrollToIndex(0);
handled = true;
} else if (e.code === "End") {
scrollToIndex(items.length - 1);
handled = true;
} else if (e.code === "PageDown" && visibleRange && currentIndex !== undefined) {
const numberDisplayed = visibleRange.endIndex - visibleRange.startIndex;
scrollToItem(Math.min(currentIndex + numberDisplayed, items.length - 1), true, `start`);
handled = true;
} else if (e.code === "PageUp" && visibleRange && currentIndex !== undefined) {
const numberDisplayed = visibleRange.endIndex - visibleRange.startIndex;
scrollToItem(Math.max(currentIndex - numberDisplayed, 0), false, `start`);
handled = true;
// Guard against null/undefined events and modified keys which we don't want to handle here but do
// at the settings level shortcuts(E.g. Select next room, etc )
if (e || !isModifiedKeyEvent(e)) {
if (e.code === Key.ARROW_UP && currentIndex !== undefined) {
scrollToItem(currentIndex - 1, false);
handled = true;
} else if (e.code === Key.ARROW_DOWN && currentIndex !== undefined) {
scrollToItem(currentIndex + 1, true);
handled = true;
} else if (e.code === Key.HOME) {
scrollToIndex(0);
handled = true;
} else if (e.code === Key.END) {
scrollToIndex(items.length - 1);
handled = true;
} else if (e.code === Key.PAGE_DOWN && visibleRange && currentIndex !== undefined) {
const numberDisplayed = visibleRange.endIndex - visibleRange.startIndex;
scrollToItem(Math.min(currentIndex + numberDisplayed, items.length - 1), true, `start`);
handled = true;
} else if (e.code === Key.PAGE_UP && visibleRange && currentIndex !== undefined) {
const numberDisplayed = visibleRange.endIndex - visibleRange.startIndex;
scrollToItem(Math.max(currentIndex - numberDisplayed, 0), false, `start`);
handled = true;
}
}
if (handled) {
e.stopPropagation();
e.preventDefault();
} else {
onKeyDown?.(e);
}
},
[scrollToIndex, scrollToItem, tabIndexKey, keyToIndexMap, visibleRange, items],
[scrollToIndex, scrollToItem, tabIndexKey, keyToIndexMap, visibleRange, items, onKeyDown],
);
/**
@@ -251,8 +265,12 @@ export function ListView<Item, Context = any>(props: IListViewProps<Item, Contex
[keyToIndexMap, visibleRange, scrollToIndex, tabIndexKey],
);
const onBlur = useCallback((): void => {
setIsFocused(false);
const onBlur = useCallback((event: React.FocusEvent<HTMLDivElement>): void => {
// Only set isFocused to false if the focus is moving outside the list
// This prevents the list from losing focus when interacting with menus inside it
if (!event.currentTarget.contains(event.relatedTarget)) {
setIsFocused(false);
}
}, []);
const listContext: ListContext<Context> = {
@@ -264,8 +282,8 @@ export function ListView<Item, Context = any>(props: IListViewProps<Item, Contex
return (
<Virtuoso
tabIndex={props.tabIndex || undefined} // We don't need to focus the container, so leave it undefined by default
scrollerRef={scrollerRef}
ref={virtuosoHandleRef}
scrollerRef={scrollerRef}
onKeyDown={keyDownCallback}
context={listContext}
rangeChanged={setVisibleRange}