diff --git a/package.json b/package.json index fef29cd1f5b..669f898848c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/playwright/e2e/left-panel/room-list-panel/room-list-panel.spec.ts b/playwright/e2e/left-panel/room-list-panel/room-list-panel.spec.ts index d0503e2caf9..813d9d8fd7e 100644 --- a/playwright/e2e/left-panel/room-list-panel/room-list-panel.spec.ts +++ b/playwright/e2e/left-panel/room-list-panel/room-list-panel.spec.ts @@ -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"); }); }); diff --git a/res/css/_components.pcss b/res/css/_components.pcss index b68da871672..7a07fc8d125 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -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"; diff --git a/res/css/views/rooms/RoomListPanel/_RoomList.pcss b/res/css/views/rooms/RoomListPanel/_RoomList.pcss deleted file mode 100644 index 54798f1ea96..00000000000 --- a/res/css/views/rooms/RoomListPanel/_RoomList.pcss +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -.mx_RoomList { - height: 100%; -} diff --git a/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss b/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss index ac58a69bef7..06ffe532d7c 100644 --- a/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss +++ b/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss @@ -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; } } } @@ -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 diff --git a/src/components/viewmodels/roomlist/RoomListViewModel.tsx b/src/components/viewmodels/roomlist/RoomListViewModel.tsx index e4e50937828..d2e936cbf5e 100644 --- a/src/components/viewmodels/roomlist/RoomListViewModel.tsx +++ b/src/components/viewmodels/roomlist/RoomListViewModel.tsx @@ -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. */ @@ -88,6 +93,7 @@ export function useRoomListViewModel(): RoomListViewState { return { isLoadingRooms, + spaceId: currentSpace?.roomId || "", rooms, canCreateRoom, createRoom, diff --git a/src/components/views/rooms/RoomListPanel/RoomList.tsx b/src/components/views/rooms/RoomListPanel/RoomList.tsx index f5f610f5ae4..a9466c12f7b 100644 --- a/src/components/views/rooms/RoomListPanel/RoomList.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomList.tsx @@ -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 { /** @@ -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) => ( - - ), - [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(null); + const [roomListState, setRoomListState] = useState({ 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 ( - - {({ onKeyDownHandler }) => ( -
{ - 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); - }} - > - - {({ height, width }) => ( - - )} - -
+ // + // {({ onKeyDownHandler }) => ( + ( + )} - + 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); + // }} + /> + // )} + //
); } diff --git a/yarn.lock b/yarn.lock index cd4bb55b3ff..e1739700009 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"