Migrate the room list view to shared components (#31921)

* Add NotificationDecoration component

Add the NotificationDecoration component to shared-components.
This is a leaf component that renders notification badges and indicators
for rooms/items including mentions, unread counts, call indicators, etc.

* Add RoomListItem component

Add the RoomListItem component to shared-components.
Includes context menu, hover menu, notification menu, and more options menu.

* Add RoomListPrimaryFilters component

Add filter chips component for filtering the room list by
unread, people, rooms, favourites, mentions, invites, and low priority.

* Update VirtualizedList component

Update VirtualizedList to support the room list virtualization requirements.

* Add RoomList component

Add RoomList component that renders a virtualized list of room items.
Includes story mocks for testing.

* Add RoomListView component

Add RoomListView component that composes RoomList with filters,
empty states, and loading skeleton.

* Export room-list components from shared-components

Add exports for RoomListView, RoomListItem, RoomListPrimaryFilters, and RoomList.
Include i18n strings for room list components.

* Add RoomListItemViewModel

Add view model for individual room list items.
Manages per-room subscriptions and updates only when specific room data changes.

* Add RoomListViewViewModel

Add view model for the room list view.
Manages room list state, filtering, keyboard navigation, and child view models.

* Integrate shared components into RoomListView

Update RoomListView to use the new ViewModels and shared components.
Includes i18n string updates for element-web.

* Remove old room list implementation

Remove old ViewModels, hooks, and view components that are now
replaced by the shared-components implementation.

* Update sliding-sync playwright test

Update test expectations for new room list implementation.

* Add figma links

* Move viewModels to the right folder

* Rename to RoomListEmptyStateView

* Update VirtualizedRoomListView naming

* Update screenshots and snapshots

* Move viewmodel tests to the right location and fix some imports

* lint

* Use unknown as an Opaque type rather than any. It discourages property access within shared components and can still be cast back in EW.

* Update screenshots for new shared component rendering params

* Make room order tests deterministic
This commit is contained in:
David Langley
2026-02-05 21:05:14 +00:00
committed by GitHub
parent 6dba71a453
commit 6da1412de8
140 changed files with 20046 additions and 5950 deletions
@@ -1,9 +1,9 @@
/*
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.
*/
* 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, { useRef, type JSX, useCallback, useEffect, useState, useMemo } from "react";
import { type VirtuosoHandle, type ListRange, Virtuoso, type VirtuosoProps } from "react-virtuoso";
@@ -95,6 +95,19 @@ export interface IVirtualizedListProps<Item, Context> extends Omit<
* @returns
*/
onKeyDown?: (e: React.KeyboardEvent<HTMLDivElement>) => void;
/**
* Optional total count of items (for virtualization with partial data loading).
* If provided, this will be used instead of items.length for the total count.
*/
totalCount?: number;
/**
* Optional callback when the visible range of items changes.
* Useful for loading data on-demand as the user scrolls.
* @param range - The new visible range with startIndex and endIndex
*/
rangeChanged?: (range: ListRange) => void;
}
/**
@@ -113,7 +126,17 @@ export type ScrollIntoViewOnChange<Item, Context = any> = NonNullable<
*/
export function VirtualizedList<Item, Context = any>(props: IVirtualizedListProps<Item, Context>): React.ReactElement {
// Extract our custom props to avoid conflicts with Virtuoso props
const { items, getItemComponent, isItemFocusable, getItemKey, context, onKeyDown, ...virtuosoProps } = props;
const {
items,
getItemComponent,
isItemFocusable,
getItemKey,
context,
onKeyDown,
totalCount,
rangeChanged,
...virtuosoProps
} = props;
/** Reference to the Virtuoso component for programmatic scrolling */
const virtuosoHandleRef = useRef<VirtuosoHandle>(null);
/** Reference to the DOM element containing the virtualized list */
@@ -324,6 +347,15 @@ export function VirtualizedList<Item, Context = any>(props: IVirtualizedListProp
[tabIndexKey, isFocused, props.context],
);
// Combine internal range tracking with optional external callback
const handleRangeChanged = useCallback(
(range: ListRange) => {
setVisibleRange(range);
rangeChanged?.(range);
},
[rangeChanged],
);
return (
<Virtuoso
// note that either the container of direct children must be focusable to be axe
@@ -334,10 +366,11 @@ export function VirtualizedList<Item, Context = any>(props: IVirtualizedListProp
scrollerRef={scrollerRef}
onKeyDown={keyDownCallback}
context={listContext}
rangeChanged={setVisibleRange}
rangeChanged={handleRangeChanged}
// virtuoso errors internally if you pass undefined.
overscan={props.overscan || 0}
data={props.items}
totalCount={totalCount}
onFocus={onFocus}
onBlur={onBlur}
itemContent={getItemComponentInternal}