Skip to content

Move Room list to virtuoso #30356

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 9 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@
"react-string-replace": "^1.1.1",
"react-transition-group": "^4.4.1",
"react-virtualized": "^9.22.5",
"react-virtuoso": "https://gitpkg.vercel.app/langleyd/react-virtuoso/packages/react-virtuoso?36f7b675163853fc08ddc2704de90e59baa6f3fa&scripts.postinstall=npm%20install%20--ignore-scripts%20%26%26%20npm%20run%20build",
"rfc4648": "^1.4.0",
"sanitize-filename": "^1.6.3",
"sanitize-html": "2.17.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ test.describe("Room list panel", () => {

test("should respond to small screen sizes", { tag: "@screenshot" }, async ({ page }) => {
await page.setViewportSize({ width: 575, height: 600 });
const roomListPanel = getRoomListView(page);
await expect(roomListPanel).toMatchScreenshot("room-list-panel-smallscreen.png");

const roomListView = getRoomListView(page);
// Wait for the last room to be visible
await expect(roomListView.getByRole("gridcell", { name: "Open room room19" })).toBeVisible();
await expect(roomListView).toMatchScreenshot("room-list-panel-smallscreen.png");
});
});
1 change: 0 additions & 1 deletion res/css/_components.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,6 @@
@import "./views/right_panel/_WidgetCard.pcss";
@import "./views/room_settings/_AliasSettings.pcss";
@import "./views/rooms/RoomListPanel/_EmptyRoomList.pcss";
@import "./views/rooms/RoomListPanel/_RoomList.pcss";
@import "./views/rooms/RoomListPanel/_RoomListHeaderView.pcss";
@import "./views/rooms/RoomListPanel/_RoomListItemMenuView.pcss";
@import "./views/rooms/RoomListPanel/_RoomListItemView.pcss";
Expand Down
10 changes: 0 additions & 10 deletions res/css/views/rooms/RoomListPanel/_RoomList.pcss

This file was deleted.

60 changes: 32 additions & 28 deletions res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -15,40 +15,44 @@
* |-------------------------------------------------------|
*/
.mx_RoomListItemView {
all: unset;
/* Remove button default style */
background: unset;
border: none;
padding: 0;
text-align: unset;

cursor: pointer;
height: 48px;
width: 100%;

padding-left: var(--cpd-space-3x);
font: var(--cpd-font-body-md-regular);

.mx_RoomListItemView_container {
padding-left: var(--cpd-space-3x);
font: var(--cpd-font-body-md-regular);
.mx_RoomListItemView_content {
height: 100%;
flex: 1;
/* The border is only under the room name and the future hover menu */
border-bottom: var(--cpd-border-width-0-5) solid var(--cpd-color-bg-subtle-secondary);
box-sizing: border-box;
min-width: 0;
padding-right: var(--cpd-space-5x);

.mx_RoomListItemView_content {
height: 100%;
flex: 1;
/* The border is only under the room name and the future hover menu */
border-bottom: var(--cpd-border-width-0-5) solid var(--cpd-color-bg-subtle-secondary);
box-sizing: border-box;
.mx_RoomListItemView_text {
min-width: 0;
padding-right: var(--cpd-space-5x);

.mx_RoomListItemView_text {
min-width: 0;
}
}

.mx_RoomListItemView_roomName {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mx_RoomListItemView_roomName {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.mx_RoomListItemView_messagePreview {
font: var(--cpd-font-body-sm-regular);
color: var(--cpd-color-text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mx_RoomListItemView_messagePreview {
font: var(--cpd-font-body-sm-regular);
color: var(--cpd-color-text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
Expand All @@ -57,7 +61,7 @@
background-color: var(--cpd-color-bg-action-secondary-hovered);
}

.mx_RoomListItemView_menu_open .mx_RoomListItemView_container .mx_RoomListItemView_content {
.mx_RoomListItemView_menu_open .mx_RoomListItemView_content {
/**
* The figma uses 16px padding (--cpd-space-4x) but due to https://github.com/element-hq/compound-web/issues/331
* the icon size of the menu is 18px instead of 20px with a different internal padding
Expand Down
6 changes: 6 additions & 0 deletions src/components/viewmodels/roomlist/RoomListViewModel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ export interface RoomListViewState {
*/
isLoadingRooms: boolean;

/**
* The space id associated with the rooms returned.
*/
spaceId: string;

/**
* A list of rooms to be displayed in the left panel.
*/
Expand Down Expand Up @@ -88,6 +93,7 @@ export function useRoomListViewModel(): RoomListViewState {

return {
isLoadingRooms,
spaceId: currentSpace?.roomId || "",
rooms,
canCreateRoom,
createRoom,
Expand Down
135 changes: 82 additions & 53 deletions src/components/views/rooms/RoomListPanel/RoomList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,13 @@
* Please see LICENSE files in the repository root for full details.
*/

import React, { useCallback, type JSX } from "react";
import { AutoSizer, List, type ListRowProps } from "react-virtualized";
import React, { type JSX, useCallback, useEffect, useRef, useState } from "react";
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso";
import { type Room } from "matrix-js-sdk/src/matrix";

import { type RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel";
import { _t } from "../../../../languageHandler";
import { RoomListItemView } from "./RoomListItemView";
import { RovingTabIndexProvider } from "../../../../accessibility/RovingTabIndex";
import { getKeyBindingsManager } from "../../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../../accessibility/KeyboardShortcuts";
import { Landmark, LandmarkNavigation } from "../../../../accessibility/LandmarkNavigation";

interface RoomListProps {
/**
Expand All @@ -23,58 +20,90 @@ interface RoomListProps {
vm: RoomListViewState;
}


type RoomListState = {
currentRoomIndex: number | undefined;
rooms: Room[];
spaceId: string;
};

/**
* A virtualized list of rooms.
*/
export function RoomList({ vm: { rooms, activeIndex } }: RoomListProps): JSX.Element {
const roomRendererMemoized = useCallback(
({ key, index, style }: ListRowProps) => (
<RoomListItemView room={rooms[index]} key={key} style={style} isSelected={activeIndex === index} />
),
[rooms, activeIndex],
);
export function RoomList({ vm: { rooms, activeIndex: currentRoomIndex, spaceId } }: RoomListProps): JSX.Element {
// To follow the grid pattern (https://www.w3.org/WAI/ARIA/apg/patterns/grid/), we need to set the first element of the list as a row.
// The virtuoso component is set to role="grid" and the items are set to role="gridcell".
// TODO REPLACE WITH A CUSToM LIST COMPONENT
const scrollerRef = useCallback((node: HTMLElement | Window | null) => {
if (node instanceof HTMLElement) {
node.firstElementChild?.setAttribute("role", "row");
}
}, []);

const virtuosoRef = useRef<VirtuosoHandle | null>(null);
const [roomListState, setRoomListState] = useState<RoomListState>({ currentRoomIndex, rooms, spaceId});

// The first div is needed to make the virtualized list take all the remaining space and scroll correctly
useEffect(() => {
// TODO: this logic is not correct, but was introduced so that we change the spaceId and the rooms at the same time.
// Ideally the props should be updated in a valid way(we don't have a mismatched spaceId/rooms being passed in).
if(roomListState.spaceId !== spaceId && roomListState.rooms.length !== rooms.length) {
virtuosoRef.current?.scrollIntoView({
align: `start`,
index: currentRoomIndex || 0,
behavior: "auto",
targetsNextRefresh: true,
});
setRoomListState({ ...roomListState, rooms, spaceId, currentRoomIndex});
}

}, [spaceId, rooms, currentRoomIndex, roomListState]);

return (
<RovingTabIndexProvider handleHomeEnd={true} handleUpDown={true}>
{({ onKeyDownHandler }) => (
<div
className="mx_RoomList"
data-testid="room-list"
onKeyDown={(ev) => {
const navAction = getKeyBindingsManager().getNavigationAction(ev);
if (
navAction === KeyBindingAction.NextLandmark ||
navAction === KeyBindingAction.PreviousLandmark
) {
LandmarkNavigation.findAndFocusNextLandmark(
Landmark.ROOM_LIST,
navAction === KeyBindingAction.PreviousLandmark,
);
ev.stopPropagation();
ev.preventDefault();
return;
}
onKeyDownHandler(ev);
}}
>
<AutoSizer>
{({ height, width }) => (
<List
aria-label={_t("room_list|list_title")}
className="mx_RoomList_List"
rowRenderer={roomRendererMemoized}
rowCount={rooms.length}
rowHeight={48}
height={height}
width={width}
scrollToIndex={activeIndex ?? 0}
tabIndex={-1}
/>
)}
</AutoSizer>
</div>
// <RovingTabIndexProvider handleHomeEnd={true} handleUpDown={true}>
// {({ onKeyDownHandler }) => (
<Virtuoso
// key={spaceId}
data-testid="room-list"
role="grid"
aria-label={_t("room_list|list_title")}
aria-rowcount={1}
aria-colcount={1}
totalCount={roomListState.rooms.length}
data={roomListState.rooms}
fixedItemHeight={48}
context={{ currentRoomIndex: roomListState.currentRoomIndex }}
itemContent={(index, room, { currentRoomIndex }) => (
<RoomListItemView
room={room}
isSelected={currentRoomIndex === index}
aria-colindex={index}
role="gridcell"
/>
)}
</RovingTabIndexProvider>
computeItemKey={(index, item ) => item.roomId}
/* 240px = 5 rows */
increaseViewportBy={240}
initialTopMostItemIndex={currentRoomIndex ?? 0}
ref={virtuosoRef}
scrollerRef={scrollerRef}
// onKeyDown={(ev) => {
// const navAction = getKeyBindingsManager().getNavigationAction(ev);
// if (
// navAction === KeyBindingAction.NextLandmark ||
// navAction === KeyBindingAction.PreviousLandmark
// ) {
// LandmarkNavigation.findAndFocusNextLandmark(
// Landmark.ROOM_LIST,
// navAction === KeyBindingAction.PreviousLandmark,
// );
// ev.stopPropagation();
// ev.preventDefault();
// return;
// }
// onKeyDownHandler(ev);
// }}
/>
// )}
// </RovingTabIndexProvider>
);
}
4 changes: 4 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -13046,6 +13046,10 @@ react-virtualized@^9.22.5:
prop-types "^15.7.2"
react-lifecycles-compat "^3.0.4"

"react-virtuoso@https://gitpkg.vercel.app/langleyd/react-virtuoso/packages/react-virtuoso?36f7b675163853fc08ddc2704de90e59baa6f3fa&scripts.postinstall=npm%20install%20--ignore-scripts%20%26%26%20npm%20run%20build":
version "4.13.0"
resolved "https://gitpkg.vercel.app/langleyd/react-virtuoso/packages/react-virtuoso?36f7b675163853fc08ddc2704de90e59baa6f3fa&scripts.postinstall=npm%20install%20--ignore-scripts%20%26%26%20npm%20run%20build#a979dd4bb7c14e30c8011d5c3db9301fc8466e84"

"react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", react@^19.0.0:
version "19.1.0"
resolved "https://registry.yarnpkg.com/react/-/react-19.1.0.tgz#926864b6c48da7627f004795d6cce50e90793b75"
Expand Down
Loading