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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user