From f70180eb911d467482226bc49e6a6f17799c7396 Mon Sep 17 00:00:00 2001 From: David Langley Date: Fri, 30 Jan 2026 09:43:47 +0000 Subject: [PATCH] Add RoomList component Add RoomList component that renders a virtualized list of room items. Includes story mocks for testing. --- .../RoomList.stories.tsx/default-auto.png | Bin 0 -> 46838 bytes .../room-list/RoomList/RoomList.module.css | 14 + .../room-list/RoomList/RoomList.stories.tsx | 87 ++ .../src/room-list/RoomList/RoomList.test.tsx | 67 + .../src/room-list/RoomList/RoomList.tsx | 197 +++ .../__snapshots__/RoomList.test.tsx.snap | 1277 +++++++++++++++++ .../src/room-list/RoomList/index.ts | 9 + .../src/room-list/story-mocks.tsx | 136 ++ 8 files changed, 1787 insertions(+) create mode 100644 packages/shared-components/__vis__/linux/__baselines__/room-list/RoomList/RoomList.stories.tsx/default-auto.png create mode 100644 packages/shared-components/src/room-list/RoomList/RoomList.module.css create mode 100644 packages/shared-components/src/room-list/RoomList/RoomList.stories.tsx create mode 100644 packages/shared-components/src/room-list/RoomList/RoomList.test.tsx create mode 100644 packages/shared-components/src/room-list/RoomList/RoomList.tsx create mode 100644 packages/shared-components/src/room-list/RoomList/__snapshots__/RoomList.test.tsx.snap create mode 100644 packages/shared-components/src/room-list/RoomList/index.ts create mode 100644 packages/shared-components/src/room-list/story-mocks.tsx diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomList/RoomList.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomList/RoomList.stories.tsx/default-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..9cf03c0899ed60f1810afda5f601940dd8cb8d6e GIT binary patch literal 46838 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 literal 0 HcmV?d00001 diff --git a/packages/shared-components/src/room-list/RoomList/RoomList.module.css b/packages/shared-components/src/room-list/RoomList/RoomList.module.css new file mode 100644 index 0000000000..c444c8c1cd --- /dev/null +++ b/packages/shared-components/src/room-list/RoomList/RoomList.module.css @@ -0,0 +1,14 @@ +/* + * Copyright 2025 Element Creations 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. + */ + +/** + * Room list container styles + */ +.roomList { + height: 100%; + width: 100%; +} diff --git a/packages/shared-components/src/room-list/RoomList/RoomList.stories.tsx b/packages/shared-components/src/room-list/RoomList/RoomList.stories.tsx new file mode 100644 index 0000000000..a76ffe7e34 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomList/RoomList.stories.tsx @@ -0,0 +1,87 @@ +/* + * Copyright 2026 Element Creations 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. + */ + +import React, { type JSX } from "react"; +import { fn } from "storybook/test"; + +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { RoomList, type RoomListViewState } from "./RoomList"; +import type { RoomListSnapshot, RoomListViewActions } from "../RoomListView"; +import { useMockedViewModel } from "../../viewmodel"; +import type { FilterId } from "../RoomListPrimaryFilters"; +import { renderAvatar, createGetRoomItemViewModel, mockRoomIds } from "../story-mocks"; + +type RoomListStoryProps = RoomListSnapshot & RoomListViewActions & { renderAvatar: (room: any) => React.ReactElement }; + +// Use first 10 room IDs for this story +const storyRoomIds = mockRoomIds.slice(0, 10); + +// Wrapper component that creates a mocked ViewModel +const RoomListWrapper = ({ + onToggleFilter, + createChatRoom, + createRoom, + getRoomItemViewModel, + updateVisibleRooms, + renderAvatar: renderAvatarProp, + ...rest +}: RoomListStoryProps): JSX.Element => { + const vm = useMockedViewModel(rest, { + onToggleFilter, + createChatRoom, + createRoom, + getRoomItemViewModel, + updateVisibleRooms, + }); + + return ( +
+ +
+ ); +}; + +const mockFilterIds: FilterId[] = ["unread", "people"]; + +const defaultRoomListState: RoomListViewState = { + activeRoomIndex: 0, + spaceId: "!space:server", + filterKeys: undefined, +}; + +const meta: Meta = { + title: "Room List/RoomList", + component: RoomListWrapper, + tags: ["autodocs"], + args: { + isLoadingRooms: false, + isRoomListEmpty: false, + filterIds: mockFilterIds, + activeFilterId: undefined, + roomIds: storyRoomIds, + roomListState: defaultRoomListState, + canCreateRoom: true, + onToggleFilter: fn(), + createChatRoom: fn(), + createRoom: fn(), + getRoomItemViewModel: createGetRoomItemViewModel(storyRoomIds), + updateVisibleRooms: fn(), + renderAvatar, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/packages/shared-components/src/room-list/RoomList/RoomList.test.tsx b/packages/shared-components/src/room-list/RoomList/RoomList.test.tsx new file mode 100644 index 0000000000..c5ee9b8b75 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomList/RoomList.test.tsx @@ -0,0 +1,67 @@ +/* + * Copyright 2026 Element Creations 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. + */ + +import React from "react"; +import { render, screen, fireEvent } from "@test-utils"; +import { VirtuosoMockContext } from "react-virtuoso"; +import { composeStories } from "@storybook/react-vite"; +import { describe, it, expect } from "vitest"; + +import * as stories from "./RoomList.stories"; + +const { Default } = composeStories(stories); + +const renderWithMockContext = (component: React.ReactElement): ReturnType => { + return render(component, { + wrapper: ({ children }) => ( + + {children} + + ), + }); +}; + +describe("", () => { + it("renders Default story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("should render the room list listbox", () => { + renderWithMockContext(); + expect(screen.getByRole("listbox", { name: "Room list" })).toBeInTheDocument(); + }); + + it("should render room items", () => { + renderWithMockContext(); + const items = screen.getAllByRole("option"); + expect(items.length).toBeGreaterThan(0); + }); + + it("should mark selected room with aria-selected true", () => { + renderWithMockContext(); + const items = screen.getAllByRole("option"); + // The first item (index 0) should be selected based on Default story (activeRoomIndex: 0) + expect(items[0]).toHaveAttribute("aria-selected", "true"); + }); + + it("should handle focus state correctly", () => { + renderWithMockContext(); + + const listbox = screen.getByRole("listbox", { name: "Room list" }); + fireEvent.focus(listbox); + + const items = screen.getAllByRole("option"); + // First item should have tabIndex 0 (focusable) when list is focused + expect(items[0]).toHaveAttribute("tabIndex", "0"); + }); + + it("should call updateVisibleRooms on render", () => { + renderWithMockContext(); + expect(Default.args.updateVisibleRooms).toHaveBeenCalled(); + }); +}); diff --git a/packages/shared-components/src/room-list/RoomList/RoomList.tsx b/packages/shared-components/src/room-list/RoomList/RoomList.tsx new file mode 100644 index 0000000000..ee5f7b3961 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomList/RoomList.tsx @@ -0,0 +1,197 @@ +/* + * Copyright 2026 Element Creations 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. + */ + +import React, { useCallback, useMemo, useRef, type JSX, type ReactNode } from "react"; +import { type ScrollIntoViewLocation } from "react-virtuoso"; +import { isEqual } from "lodash"; + +import { useViewModel } from "../../viewmodel"; +import { _t } from "../../utils/i18n"; +import { VirtualizedList, type VirtualizedListContext } from "../../utils/VirtualizedList"; +import { RoomListItemView } from "../RoomListItem"; +import type { RoomListViewModel } from "../RoomListView"; + +/** + * Filter key type - opaque string type for filter identifiers + */ +export type FilterKey = string; + +/** + * State for the room list data (nested within RoomListSnapshot) + */ +export interface RoomListViewState { + /** Optional active room index for keyboard navigation */ + activeRoomIndex?: number; + /** Space ID for context tracking */ + spaceId?: string; + /** Active filter keys for context tracking */ + filterKeys?: FilterKey[]; +} + +/** + * Props for the RoomList component + */ +export interface RoomListProps { + /** + * The view model containing all room list data and callbacks + */ + vm: RoomListViewModel; + + /** + * Render function for room avatar + * @param room - The opaque Room object from the client + */ + renderAvatar: (room: any) => ReactNode; + + /** + * Optional callback for keyboard key down events + */ + onKeyDown?: (e: React.KeyboardEvent) => void; +} + +/** Height of a single room list item in pixels */ +const ROOM_LIST_ITEM_HEIGHT = 48; + +/** + * Type for context used in ListView + */ +type Context = { spaceId: string; filterKeys: FilterKey[] | undefined }; + +/** + * Amount to extend the top and bottom of the viewport by. + * From manual testing and user feedback 25 items is reported to be enough to avoid blank space + * when using the mouse wheel, and the trackpad scrolling at a slow to moderate speed where you + * can still see/read the content. Using the trackpad to sling through a large percentage of the + * list quickly will still show blank space. We would likely need to simplify the item content to + * improve this case. + */ +const EXTENDED_VIEWPORT_HEIGHT = 25 * ROOM_LIST_ITEM_HEIGHT; + +/** + * A virtualized list of rooms. + * This component provides efficient rendering of large room lists using virtualization, + * and renders RoomListItemView components for each room. + * + * @example + * ```tsx + * } /> + * ``` + */ +export function RoomList({ vm, renderAvatar, onKeyDown }: RoomListProps): JSX.Element { + const snapshot = useViewModel(vm); + const { roomListState, roomIds } = snapshot; + const activeRoomIndex = roomListState.activeRoomIndex; + const lastSpaceId = useRef(undefined); + const lastFilterKeys = useRef(undefined); + const roomCount = roomIds.length; + + /** + * Callback when the visible range changes + * Notifies the view model which rooms are visible + */ + const rangeChanged = useCallback( + (range: { startIndex: number; endIndex: number }) => { + vm.updateVisibleRooms(range.startIndex, range.endIndex); + }, + [vm], + ); + + /** + * Get the item component for a specific index + * Gets the room's view model and passes it to RoomListItemView + */ + const getItemComponent = useCallback( + ( + index: number, + roomId: string, + context: VirtualizedListContext, + onFocus: (item: string, e: React.FocusEvent) => void, + ): JSX.Element => { + const isSelected = activeRoomIndex === index; + const roomItemVM = vm.getRoomItemViewModel(roomId); + + // Item is focused when the list has focus AND this item's key matches tabIndexKey + // This matches the old RoomList implementation's roving tabindex pattern + const isFocused = context.focused && context.tabIndexKey === roomId; + + return ( + + ); + }, + [activeRoomIndex, roomCount, renderAvatar, vm], + ); + + /** + * Get the key for a room item + * Since we're using virtualization, items are always room ID strings + */ + const getItemKey = useCallback((item: string): string => { + return item; + }, []); + + const context = useMemo( + () => ({ spaceId: roomListState.spaceId || "", filterKeys: roomListState.filterKeys }), + [roomListState.spaceId, roomListState.filterKeys], + ); + + /** + * Determine if we should scroll the active index into view + * This happens when the space or filters change + */ + const scrollIntoViewOnChange = useCallback( + (params: { + context: VirtualizedListContext<{ spaceId: string; filterKeys: FilterKey[] | undefined }>; + }): ScrollIntoViewLocation | null | undefined | false => { + const { spaceId, filterKeys } = params.context.context; + const shouldScrollIndexIntoView = + lastSpaceId.current !== spaceId || !isEqual(lastFilterKeys.current, filterKeys); + lastFilterKeys.current = filterKeys; + lastSpaceId.current = spaceId; + + if (shouldScrollIndexIntoView) { + return { + align: "start", + index: activeRoomIndex || 0, + behavior: "auto", + }; + } + return false; + }, + [activeRoomIndex], + ); + + return ( + true} + rangeChanged={rangeChanged} + onKeyDown={onKeyDown} + increaseViewportBy={{ + bottom: EXTENDED_VIEWPORT_HEIGHT, + top: EXTENDED_VIEWPORT_HEIGHT, + }} + /> + ); +} diff --git a/packages/shared-components/src/room-list/RoomList/__snapshots__/RoomList.test.tsx.snap b/packages/shared-components/src/room-list/RoomList/__snapshots__/RoomList.test.tsx.snap new file mode 100644 index 0000000000..f14e886bb1 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomList/__snapshots__/RoomList.test.tsx.snap @@ -0,0 +1,1277 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[` > renders Default story 1`] = ` +
+
+
+
+
+
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ +
+ + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + + + + + + + +`; diff --git a/packages/shared-components/src/room-list/RoomList/index.ts b/packages/shared-components/src/room-list/RoomList/index.ts new file mode 100644 index 0000000000..0b0498d139 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomList/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright 2026 Element Creations 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. + */ + +export { RoomList } from "./RoomList"; +export type { RoomListProps, RoomListViewState, FilterKey } from "./RoomList"; diff --git a/packages/shared-components/src/room-list/story-mocks.tsx b/packages/shared-components/src/room-list/story-mocks.tsx new file mode 100644 index 0000000000..03450469c6 --- /dev/null +++ b/packages/shared-components/src/room-list/story-mocks.tsx @@ -0,0 +1,136 @@ +/* + * Copyright 2026 Element Creations 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. + */ + +import React from "react"; +import { fn } from "storybook/test"; + +import type { RoomListItemSnapshot } from "./RoomListItem"; +import { RoomNotifState } from "./RoomListItem/RoomNotifs"; + +/** + * Mock avatar component for stories + */ +export const mockAvatar = (name: string): React.ReactElement => ( +
+ {name.substring(0, 2).toUpperCase()} +
+); + +/** + * Render avatar function for stories + */ +export const renderAvatar = (room: any): React.ReactElement => { + return mockAvatar(room?.name || "Room"); +}; + +/** + * Room names used for mock data + */ +const roomNames = [ + "General", + "Random", + "Engineering", + "Design", + "Product", + "Marketing", + "Sales", + "Support", + "Announcements", + "Off-topic", + "Team Alpha", + "Team Beta", + "Project X", + "Project Y", + "Water Cooler", + "Feedback", + "Ideas", + "Bugs", + "Features", + "Releases", +]; + +/** + * Create a mock room item snapshot for stories + */ +export const createMockRoomSnapshot = (id: string, name: string, index: number): RoomListItemSnapshot => ({ + id, + room: { name }, + name, + isBold: index % 3 === 0, + messagePreview: index % 2 === 0 ? `Last message in ${name}` : undefined, + notification: { + hasAnyNotificationOrActivity: index % 5 === 0, + isUnsentMessage: false, + invited: false, + isMention: index % 5 === 0, + isActivityNotification: false, + isNotification: index % 5 === 0, + hasUnreadCount: index % 5 === 0, + count: index % 5 === 0 ? index : 0, + muted: false, + }, + showMoreOptionsMenu: true, + showNotificationMenu: true, + isFavourite: false, + isLowPriority: false, + canInvite: true, + canCopyRoomLink: true, + canMarkAsRead: false, + canMarkAsUnread: true, + roomNotifState: RoomNotifState.AllMessages, +}); + +/** + * Create a mock getRoomItemViewModel function for stories + */ +export const createGetRoomItemViewModel = (roomIds: string[]): ((roomId: string) => any) => { + const viewModels = new Map(); + roomIds.forEach((roomId, index) => { + const name = roomNames[index % roomNames.length]; + const snapshot = createMockRoomSnapshot(roomId, name, index); + + const mockViewModel = { + getSnapshot: () => snapshot, + subscribe: fn(), + unsubscribe: fn(), + onOpenRoom: fn(), + onMarkAsRead: fn(), + onMarkAsUnread: fn(), + onToggleFavorite: fn(), + onToggleLowPriority: fn(), + onInvite: fn(), + onCopyRoomLink: fn(), + onLeaveRoom: fn(), + onSetRoomNotifState: fn(), + }; + viewModels.set(roomId, mockViewModel); + }); + + return (roomId: string) => viewModels.get(roomId); +}; + +/** + * Mock room IDs for different list sizes + */ +export const mockRoomIds = Array.from({ length: 20 }, (_, i) => `!room${i}:server`); +export const smallListRoomIds = mockRoomIds.slice(0, 5); +export const largeListRoomIds = Array.from({ length: 100 }, (_, i) => `!room${i}:server`);