Room list: add sections to shared components (#32735)

* feat: add section header

* refactor: remove index and role related to list box from RoomListItemView

* feat: add wrapper to RoomListItemView to handle different accessiblity pattern

* feat: add section support to VirtualizedRoomListView

* feat: add sections support to RoomListView

* test: add screenshot for sections header

* test: add/update screenshots for sections

* feat: force flat list on view model

This is an intermediary step before implementing sections in the vm. We
force the flat list but we use the underneath the view supporting
sections.

* test: update RoomListViewModel test

* test: fix breaking test

* chore: rename `getSectionViewModel` to `getSectionHeaderViewModel`

* chore: add missing `RoomListItemAccessibilityWrapper` export

* chore: merge `react` imports

* chore: simplify and add comment to `getItemKey` and `getHeaderKey`

* chore add comments to `getItemComponent` variants

* chore: fix typo in example doc
This commit is contained in:
Florian Duros
2026-03-16 16:40:12 +00:00
committed by GitHub
parent d18aa31d7d
commit ee5d2609df
41 changed files with 7505 additions and 1700 deletions
@@ -61,6 +61,8 @@ export class RoomListViewViewModel
const roomsResult = RoomListStoreV3.instance.getSortedRoomsInActiveSpace(undefined);
const canCreateRoom = hasCreateRoomRights(props.client, activeSpace);
const filterIds = [...filterKeyToIdMap.values()];
const roomIds = roomsResult.rooms.map((room) => room.roomId);
const sections = [{ id: "all", roomIds }];
super(props, {
// Initial view state - start with empty, will populate in async init
@@ -73,7 +75,9 @@ export class RoomListViewViewModel
spaceId: roomsResult.spaceId,
filterKeys: undefined,
},
roomIds: roomsResult.rooms.map((room) => room.roomId),
// Until we implement sections, this view model only supports the flat list mode
isFlatList: true,
sections,
canCreateRoom,
});
@@ -195,6 +199,15 @@ export class RoomListViewViewModel
return viewModel;
}
/**
* Not implemented - this view model does not support sections.
* Flat list mode is forced so this method is never be called.
* @throw Error if called
*/
public getSectionHeaderViewModel(): never {
throw new Error("Sections are not supported in this room list");
}
/**
* Update which rooms are currently visible.
* Called by the view when scroll position changes.
@@ -408,6 +421,7 @@ export class RoomListViewViewModel
// Build the complete state atomically to ensure consistency
// roomIds and roomListState must always be in sync
const roomIds = this.roomIds;
const sections = [{ id: "all", roomIds }];
// Update filter keys - only update if they have actually changed to prevent unnecessary re-renders of the room list
const previousFilterKeys = this.snapshot.current.roomListState.filterKeys;
@@ -428,7 +442,7 @@ export class RoomListViewViewModel
isRoomListEmpty,
activeFilterId,
roomListState,
roomIds,
sections,
});
}
@@ -66,7 +66,7 @@ describe("RoomListViewViewModel", () => {
viewModel = new RoomListViewViewModel({ client: matrixClient });
const snapshot = viewModel.getSnapshot();
expect(snapshot.roomIds).toEqual(["!room1:server", "!room2:server", "!room3:server"]);
expect(snapshot.sections[0].roomIds).toEqual(["!room1:server", "!room2:server", "!room3:server"]);
expect(snapshot.isRoomListEmpty).toBe(false);
expect(snapshot.isLoadingRooms).toBe(false);
expect(snapshot.roomListState.spaceId).toBe("home");
@@ -82,7 +82,7 @@ describe("RoomListViewViewModel", () => {
viewModel = new RoomListViewViewModel({ client: matrixClient });
expect(viewModel.getSnapshot().roomIds).toEqual([]);
expect(viewModel.getSnapshot().sections[0].roomIds).toEqual([]);
expect(viewModel.getSnapshot().isRoomListEmpty).toBe(true);
});
@@ -106,7 +106,7 @@ describe("RoomListViewViewModel", () => {
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
expect(viewModel.getSnapshot().roomIds).toEqual([
expect(viewModel.getSnapshot().sections[0].roomIds).toEqual([
"!room1:server",
"!room2:server",
"!room3:server",
@@ -156,7 +156,7 @@ describe("RoomListViewViewModel", () => {
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
expect(viewModel.getSnapshot().roomListState.spaceId).toBe("!space:server");
expect(viewModel.getSnapshot().roomIds).toEqual(["!room1:server", "!room2:server"]);
expect(viewModel.getSnapshot().sections[0].roomIds).toEqual(["!room1:server", "!room2:server"]);
});
it("should clear view models when space changes", () => {
@@ -240,7 +240,7 @@ describe("RoomListViewViewModel", () => {
// Active room should still be at index 1 (sticky behavior)
expect(viewModel.getSnapshot().roomListState.activeRoomIndex).toBe(1);
expect(viewModel.getSnapshot().roomIds[1]).toBe("!room2:server");
expect(viewModel.getSnapshot().sections[0].roomIds[1]).toBe("!room2:server");
});
it("should not apply sticky behavior when user changes rooms", async () => {
@@ -283,7 +283,7 @@ describe("RoomListViewViewModel", () => {
viewModel.onToggleFilter("unread");
expect(viewModel.getSnapshot().activeFilterId).toBe("unread");
expect(viewModel.getSnapshot().roomIds).toEqual(["!room1:server"]);
expect(viewModel.getSnapshot().sections[0].roomIds).toEqual(["!room1:server"]);
});
it("should toggle filter off", () => {
@@ -307,7 +307,11 @@ describe("RoomListViewViewModel", () => {
viewModel.onToggleFilter("unread");
expect(viewModel.getSnapshot().activeFilterId).toBeUndefined();
expect(viewModel.getSnapshot().roomIds).toEqual(["!room1:server", "!room2:server", "!room3:server"]);
expect(viewModel.getSnapshot().sections[0].roomIds).toEqual([
"!room1:server",
"!room2:server",
"!room3:server",
]);
});
});