diff --git a/apps/web/src/viewmodels/room-list/RoomListViewViewModel.ts b/apps/web/src/viewmodels/room-list/RoomListViewViewModel.ts index ed1c305796b..2eaea3d246b 100644 --- a/apps/web/src/viewmodels/room-list/RoomListViewViewModel.ts +++ b/apps/web/src/viewmodels/room-list/RoomListViewViewModel.ts @@ -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, }); } diff --git a/apps/web/test/viewmodels/room-list/RoomListViewViewModel-test.tsx b/apps/web/test/viewmodels/room-list/RoomListViewViewModel-test.tsx index abdd6d10f7f..ceeb940669d 100644 --- a/apps/web/test/viewmodels/room-list/RoomListViewViewModel-test.tsx +++ b/apps/web/test/viewmodels/room-list/RoomListViewViewModel-test.tsx @@ -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", + ]); }); }); diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItemAccessibilityWrapper/RoomListItemAccessibilityWrapper.stories.tsx/flat-list-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItemAccessibilityWrapper/RoomListItemAccessibilityWrapper.stories.tsx/flat-list-auto.png new file mode 100644 index 00000000000..65d2b7e41de Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItemAccessibilityWrapper/RoomListItemAccessibilityWrapper.stories.tsx/flat-list-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItemAccessibilityWrapper/RoomListItemAccessibilityWrapper.stories.tsx/sections-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItemAccessibilityWrapper/RoomListItemAccessibilityWrapper.stories.tsx/sections-auto.png new file mode 100644 index 00000000000..65d2b7e41de Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItemAccessibilityWrapper/RoomListItemAccessibilityWrapper.stories.tsx/sections-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListSectionHeaderView/RoomListSectionHeaderView.stories.tsx/collapsed-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListSectionHeaderView/RoomListSectionHeaderView.stories.tsx/collapsed-auto.png new file mode 100644 index 00000000000..1a0616cbca9 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListSectionHeaderView/RoomListSectionHeaderView.stories.tsx/collapsed-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListSectionHeaderView/RoomListSectionHeaderView.stories.tsx/default-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListSectionHeaderView/RoomListSectionHeaderView.stories.tsx/default-auto.png new file mode 100644 index 00000000000..0f8bb587a60 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListSectionHeaderView/RoomListSectionHeaderView.stories.tsx/default-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListSectionHeaderView/RoomListSectionHeaderView.stories.tsx/first-header-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListSectionHeaderView/RoomListSectionHeaderView.stories.tsx/first-header-auto.png new file mode 100644 index 00000000000..63b3b3bea61 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListSectionHeaderView/RoomListSectionHeaderView.stories.tsx/first-header-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListSectionHeaderView/RoomListSectionHeaderView.stories.tsx/last-header-collapsed-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListSectionHeaderView/RoomListSectionHeaderView.stories.tsx/last-header-collapsed-auto.png new file mode 100644 index 00000000000..1a0616cbca9 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListSectionHeaderView/RoomListSectionHeaderView.stories.tsx/last-header-collapsed-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListSectionHeaderView/RoomListSectionHeaderView.stories.tsx/long-title-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListSectionHeaderView/RoomListSectionHeaderView.stories.tsx/long-title-auto.png new file mode 100644 index 00000000000..db8ade7e594 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListSectionHeaderView/RoomListSectionHeaderView.stories.tsx/long-title-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/large-flat-list-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/large-flat-list-auto.png new file mode 100644 index 00000000000..667be0e5c6c Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/large-flat-list-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/large-list-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/large-list-auto.png deleted file mode 100644 index eb90df57d38..00000000000 Binary files a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/large-list-auto.png and /dev/null differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/large-section-list-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/large-section-list-auto.png new file mode 100644 index 00000000000..9d43af46193 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/large-section-list-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/section-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/section-auto.png new file mode 100644 index 00000000000..d8992ad6e64 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/section-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/small-flat-list-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/small-flat-list-auto.png new file mode 100644 index 00000000000..df403e69184 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/small-flat-list-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/small-list-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/small-list-auto.png deleted file mode 100644 index d23566e44ae..00000000000 Binary files a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/small-list-auto.png and /dev/null differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/small-section-list-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/small-section-list-auto.png new file mode 100644 index 00000000000..97dcad563f7 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListView/RoomListView.stories.tsx/small-section-list-auto.png differ diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/VirtualizedRoomListView/VirtualizedRoomListView.stories.tsx/sections-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/VirtualizedRoomListView/VirtualizedRoomListView.stories.tsx/sections-auto.png new file mode 100644 index 00000000000..36b07b87ba5 Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/VirtualizedRoomListView/VirtualizedRoomListView.stories.tsx/sections-auto.png differ diff --git a/packages/shared-components/src/i18n/strings/en_EN.json b/packages/shared-components/src/i18n/strings/en_EN.json index 1a1dc79ccdd..1f6f01bdc25 100644 --- a/packages/shared-components/src/i18n/strings/en_EN.json +++ b/packages/shared-components/src/i18n/strings/en_EN.json @@ -122,6 +122,9 @@ "more_options": "More Options" }, "room_options": "Room Options", + "section_header": { + "toggle": "Toggle %(section)s section" + }, "show_message_previews": "Show message previews", "sort": "Sort", "sort_type": { diff --git a/packages/shared-components/src/index.ts b/packages/shared-components/src/index.ts index b29287fdd82..72409f9ebcd 100644 --- a/packages/shared-components/src/index.ts +++ b/packages/shared-components/src/index.ts @@ -35,8 +35,10 @@ export * from "./rich-list/RichItem"; export * from "./rich-list/RichList"; export * from "./room-list/RoomListHeaderView"; export * from "./room-list/RoomListSearchView"; +export * from "./room-list/RoomListSectionHeaderView"; export * from "./room-list/RoomListView"; export * from "./room-list/RoomListItemView"; +export * from "./room-list/RoomListItemAccessibilityWrapper"; export * from "./room-list/RoomListPrimaryFilters"; export * from "./room-list/VirtualizedRoomListView"; export * from "./timeline/DateSeparatorView/"; diff --git a/packages/shared-components/src/room-list/RoomListItemAccessibilityWrapper/RoomListItemAccessibilityWrapper.stories.tsx b/packages/shared-components/src/room-list/RoomListItemAccessibilityWrapper/RoomListItemAccessibilityWrapper.stories.tsx new file mode 100644 index 00000000000..d5e5c18b0a0 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItemAccessibilityWrapper/RoomListItemAccessibilityWrapper.stories.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 { fn } from "storybook/test"; + +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { RoomListItemAccessibilityWrapper } from "./RoomListItemAccessibilityWrapper"; +import { createMockRoomItemViewModel, renderAvatar } from "../story-mocks"; + +const meta = { + title: "Room List/RoomListItemAccessibiltyWrapper", + component: RoomListItemAccessibilityWrapper, + tags: ["autodocs"], + args: { + roomIndex: 0, + roomIndexInSection: 0, + roomCount: 10, + onFocus: fn(), + isFirstItem: false, + isLastItem: false, + renderAvatar, + isSelected: false, + isFocused: false, + vm: createMockRoomItemViewModel("!room:server", "Room name", 0), + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const FlatList: Story = { + args: { + isInFlatList: true, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export const Sections: Story = { + args: { + isInFlatList: false, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; diff --git a/packages/shared-components/src/room-list/RoomListItemAccessibilityWrapper/RoomListItemAccessibilityWrapper.tsx b/packages/shared-components/src/room-list/RoomListItemAccessibilityWrapper/RoomListItemAccessibilityWrapper.tsx new file mode 100644 index 00000000000..d5ffddc863d --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItemAccessibilityWrapper/RoomListItemAccessibilityWrapper.tsx @@ -0,0 +1,51 @@ +/* + * 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, { memo, type JSX } from "react"; + +import { RoomListItemView, type RoomListItemViewProps } from "../RoomListItemView"; +import { getItemAccessibleProps } from "../../utils/VirtualizedList"; + +export interface RoomListItemAccessibilityWrapperPros extends RoomListItemViewProps { + /** Index of this room in the list */ + roomIndex: number; + /** Index of this room in its section */ + roomIndexInSection: number; + /** Total number of rooms in the list */ + roomCount: number; + /** Whether the room list is displayed as a flat list */ + isInFlatList: boolean; +} + +/** + * Wrapper around RoomListItemView that adds accessibility props based on the room's position in the list and whether the list is flat or grouped. + * In a flat list, each item gets listbox item props. In a grouped list, each item gets treegrid cell props. + * + * @example + * `` + * + * ``` + */ +export const RoomListItemAccessibilityWrapper = memo(function RoomListItemAccessibilityWrapper({ + roomIndex, + roomCount, + roomIndexInSection, + isInFlatList, + ...rest +}: RoomListItemAccessibilityWrapperPros): JSX.Element { + const itemA11yProps = isInFlatList ? getItemAccessibleProps("listbox", roomIndex, roomCount) : { role: "gridcell" }; + const item = ; + + if (isInFlatList) return item; + return
{item}
; +}); diff --git a/packages/shared-components/src/room-list/RoomListItemAccessibilityWrapper/index.ts b/packages/shared-components/src/room-list/RoomListItemAccessibilityWrapper/index.ts new file mode 100644 index 00000000000..a301627949a --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItemAccessibilityWrapper/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { RoomListItemAccessibilityWrapper } from "./RoomListItemAccessibilityWrapper"; diff --git a/packages/shared-components/src/room-list/RoomListItemView/RoomListItemView.stories.tsx b/packages/shared-components/src/room-list/RoomListItemView/RoomListItemView.stories.tsx index b422647c33f..b371205ba78 100644 --- a/packages/shared-components/src/room-list/RoomListItemView/RoomListItemView.stories.tsx +++ b/packages/shared-components/src/room-list/RoomListItemView/RoomListItemView.stories.tsx @@ -15,14 +15,15 @@ import { useMockedViewModel } from "../../viewmodel"; import { withViewDocs } from "../../../.storybook/withViewDocs"; import { defaultSnapshot } from "./default-snapshot"; import { renderAvatar } from "../story-mocks"; +import { mockedActions } from "./mocked-actions"; type RoomListItemProps = RoomListItemSnapshot & RoomListItemActions & { isSelected: boolean; isFocused: boolean; onFocus: (room: Room, e: React.FocusEvent) => void; - roomIndex: number; - roomCount: number; + isFirstItem: boolean; + isLastItem: boolean; renderAvatar: (room: Room) => React.ReactElement; }; @@ -40,8 +41,8 @@ const RoomListItemWrapperImpl = ({ isSelected, isFocused, onFocus, - roomIndex, - roomCount, + isFirstItem, + isLastItem, renderAvatar: renderAvatarProp, ...rest }: RoomListItemProps): JSX.Element => { @@ -62,9 +63,10 @@ const RoomListItemWrapperImpl = ({ isSelected={isSelected} isFocused={isFocused} onFocus={onFocus} - roomIndex={roomIndex} - roomCount={roomCount} + isFirstItem={isFirstItem} + isLastItem={isLastItem} renderAvatar={renderAvatarProp} + role="option" /> ); }; @@ -76,28 +78,18 @@ const meta = { tags: ["autodocs"], decorators: [ (Story) => ( -
-
- -
+
+
), ], args: { ...defaultSnapshot, + ...mockedActions, isSelected: false, isFocused: false, - roomIndex: 1, - roomCount: 10, - onOpenRoom: fn(), - onMarkAsRead: fn(), - onMarkAsUnread: fn(), - onToggleFavorite: fn(), - onToggleLowPriority: fn(), - onInvite: fn(), - onCopyRoomLink: fn(), - onLeaveRoom: fn(), - onSetRoomNotifState: fn(), + isFirstItem: false, + isLastItem: false, onFocus: fn(), renderAvatar, }, @@ -263,14 +255,14 @@ export const WithZoom: Story = { export const FirstItem: Story = { args: { - roomIndex: 0, + isFirstItem: true, isSelected: true, }, }; export const LastItem: Story = { args: { - roomIndex: 9, + isLastItem: true, isSelected: true, }, }; diff --git a/packages/shared-components/src/room-list/RoomListItemView/RoomListItemView.tsx b/packages/shared-components/src/room-list/RoomListItemView/RoomListItemView.tsx index 1a7f75216bb..729f7b4ef59 100644 --- a/packages/shared-components/src/room-list/RoomListItemView/RoomListItemView.tsx +++ b/packages/shared-components/src/room-list/RoomListItemView/RoomListItemView.tsx @@ -119,10 +119,10 @@ export interface RoomListItemViewProps extends Omit void; - /** Index of this room in the list (for accessibility) */ - roomIndex: number; - /** Total number of rooms in the list (for accessibility) */ - roomCount: number; + /** Whether this is the first item in the list */ + isFirstItem: boolean; + /** Whether this is the last item in the list */ + isLastItem: boolean; /** Function to render the room avatar */ renderAvatar: (room: Room) => ReactNode; } @@ -136,8 +136,8 @@ export const RoomListItemView = memo(function RoomListItemView({ isSelected, isFocused, onFocus, - roomIndex, - roomCount, + isFirstItem, + isLastItem, renderAvatar, ...props }: RoomListItemViewProps): JSX.Element { @@ -153,60 +153,57 @@ export const RoomListItemView = memo(function RoomListItemView({ // Generate a11y label from notification state and room name const a11yLabel = getA11yLabel(item.name, item.notification); - const content = ( - ) => onFocus(item.id, e)} - tabIndex={isFocused ? 0 : -1} - {...props} - > - - {renderAvatar(item.room)} - - {/* We truncate the room name when too long. Title here is to show the full name on hover */} -
-
- {item.name} + return ( + + ) => onFocus(item.id, e)} + tabIndex={isFocused ? 0 : -1} + {...props} + > + + {renderAvatar(item.room)} + + {/* We truncate the room name when too long. Title here is to show the full name on hover */} +
+
+ {item.name} +
+ {item.messagePreview && ( + + {item.messagePreview} + + )}
- {item.messagePreview && ( - - {item.messagePreview} - + {(item.showMoreOptionsMenu || item.showNotificationMenu) && ( + )} -
- {(item.showMoreOptionsMenu || item.showNotificationMenu) && ( - - )} - {/* aria-hidden because we summarise the unread count/notification status in a11yLabel */} -
- -
+ {/* aria-hidden because we summarise the unread count/notification status in a11yLabel */} +
+ +
+ - + ); - - return {content}; }); diff --git a/packages/shared-components/src/room-list/RoomListItemView/__snapshots__/RoomListItemView.test.tsx.snap b/packages/shared-components/src/room-list/RoomListItemView/__snapshots__/RoomListItemView.test.tsx.snap index 1482f8c7e41..441589bd07d 100644 --- a/packages/shared-components/src/room-list/RoomListItemView/__snapshots__/RoomListItemView.test.tsx.snap +++ b/packages/shared-components/src/room-list/RoomListItemView/__snapshots__/RoomListItemView.test.tsx.snap @@ -3,134 +3,129 @@ exports[` > renders Bold story 1`] = `
-
- - -
- +
+ - -
+
+
`; @@ -138,134 +133,129 @@ exports[` > renders Bold story 1`] = ` exports[` > renders Default story 1`] = `
-
- - -
- +
+ - -
+ + `; @@ -273,152 +263,147 @@ exports[` > renders Default story 1`] = ` exports[` > renders Invitation story 1`] = `
-
- -
+
+ -
- + + +
+
- - + + `; @@ -426,128 +411,123 @@ exports[` > renders Invitation story 1`] = ` exports[` > renders NoMessagePreview story 1`] = `
-
- - -
- +
+ - -
+ + `; @@ -555,134 +535,129 @@ exports[` > renders NoMessagePreview story 1`] = ` exports[` > renders Selected story 1`] = `
-
- - -
- +
+ - -
+ + `; @@ -690,152 +665,147 @@ exports[` > renders Selected story 1`] = ` exports[` > renders UnsentMessage story 1`] = `
-
- -
+
+ -
- + + +
+
- - + + `; @@ -843,134 +813,129 @@ exports[` > renders UnsentMessage story 1`] = ` exports[` > renders WithHoverMenu story 1`] = `
-
- - -
- +
+ - -
+ + `; @@ -978,157 +943,152 @@ exports[` > renders WithHoverMenu story 1`] = ` exports[` > renders WithMention story 1`] = `
-
- - + Alice: Hey everyone!
- - -
+
+ `; @@ -1136,146 +1096,141 @@ exports[` > renders WithMention story 1`] = ` exports[` > renders WithNotification story 1`] = `
-
- - -
+ + +
+ +
+ - - + + `; diff --git a/packages/shared-components/src/room-list/RoomListItemView/mocked-actions.ts b/packages/shared-components/src/room-list/RoomListItemView/mocked-actions.ts new file mode 100644 index 00000000000..600674d7ef8 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListItemView/mocked-actions.ts @@ -0,0 +1,22 @@ +/* + * 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 { fn } from "storybook/test"; + +import { type RoomListItemActions } from "./RoomListItemView"; + +export const mockedActions: RoomListItemActions = { + onOpenRoom: fn(), + onMarkAsRead: fn(), + onMarkAsUnread: fn(), + onToggleFavorite: fn(), + onToggleLowPriority: fn(), + onInvite: fn(), + onCopyRoomLink: fn(), + onLeaveRoom: fn(), + onSetRoomNotifState: fn(), +}; diff --git a/packages/shared-components/src/room-list/RoomListSectionHeaderView/RoomListSectionHeaderView.module.css b/packages/shared-components/src/room-list/RoomListSectionHeaderView/RoomListSectionHeaderView.module.css new file mode 100644 index 00000000000..69d27a6f0a2 --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListSectionHeaderView/RoomListSectionHeaderView.module.css @@ -0,0 +1,74 @@ +/* + * 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. + */ + +.header { + /* Remove button default style */ + background: unset; + border: none; + text-align: unset; + + width: 100%; + cursor: pointer; + font: var(--cpd-font-body-md-regular); + color: var(--cpd-color-text-secondary); + padding: var(--cpd-space-1x) 0; + background-color: var(--cpd-color-bg-canvas-default); + + &:hover, + &:focus-visible { + color: var(--cpd-color-text-primary); + + svg { + fill: var(--cpd-color-icon-primary); + } + + .container { + background-color: var(--cpd-color-bg-action-tertiary-hovered); + } + } + + svg { + transition: transform 0.05s linear; + } + + @media (prefers-reduced-motion: reduce) { + svg { + transition: none; + } + } + + &[aria-expanded="true"] { + svg { + transform: rotate(90deg); + } + } +} + +.container { + margin: 0 var(--cpd-space-3x); + padding: var(--cpd-space-1-5x) var(--cpd-space-2x) var(--cpd-space-1-5x) var(--cpd-space-1x); + border-radius: 8px; + + svg { + flex-shrink: 0; + } +} + +.title { + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.firstHeader { + padding-top: 0; +} + +.lastHeader { + padding-bottom: 0; +} diff --git a/packages/shared-components/src/room-list/RoomListSectionHeaderView/RoomListSectionHeaderView.stories.tsx b/packages/shared-components/src/room-list/RoomListSectionHeaderView/RoomListSectionHeaderView.stories.tsx new file mode 100644 index 00000000000..3f761a17c9c --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListSectionHeaderView/RoomListSectionHeaderView.stories.tsx @@ -0,0 +1,109 @@ +/* + * 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 { + RoomListSectionHeaderView, + type RoomListSectionHeaderViewSnapshot, + type RoomListSectionHeaderActions, + type RoomListSectionHeaderViewProps, +} from "./RoomListSectionHeaderView"; +import { useMockedViewModel } from "../../viewmodel"; +import { withViewDocs } from "../../../.storybook/withViewDocs"; + +type RoomListSectionHeaderProps = RoomListSectionHeaderViewSnapshot & + RoomListSectionHeaderActions & + Omit; + +const RoomListSectionHeaderViewWrapperImpl = ({ + onClick, + onFocus, + isFocused, + sectionIndex, + sectionCount, + indexInList, + roomCountInSection, + ...rest +}: RoomListSectionHeaderProps): JSX.Element => { + const vm = useMockedViewModel(rest, { onClick }); + return ( + + ); +}; +const RoomListSectionHeaderViewWrapper = withViewDocs(RoomListSectionHeaderViewWrapperImpl, RoomListSectionHeaderView); + +const meta = { + title: "Room List/RoomListSectionHeaderView", + component: RoomListSectionHeaderViewWrapper, + tags: ["autodocs"], + args: { + id: "favourites", + title: "Favourites", + isExpanded: true, + isFocused: false, + onClick: fn(), + onFocus: fn(), + sectionIndex: 1, + sectionCount: 3, + roomCountInSection: 5, + indexInList: 3, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/rTaQE2nIUSLav4Tg3nozq7/Compound-Web-Components?node-id=10657-20703&p=f", + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const Collapsed: Story = { + args: { + isExpanded: false, + }, +}; + +export const LongTitle: Story = { + args: { + title: "This is a very long title that should be truncated with an ellipsis", + }, +}; + +export const FirstHeader: Story = { + args: { + sectionIndex: 0, + }, +}; + +export const LastHeaderCollapsed: Story = { + args: { + isExpanded: false, + sectionIndex: 2, + }, +}; diff --git a/packages/shared-components/src/room-list/RoomListSectionHeaderView/RoomListSectionHeaderView.test.tsx b/packages/shared-components/src/room-list/RoomListSectionHeaderView/RoomListSectionHeaderView.test.tsx new file mode 100644 index 00000000000..6ab7bcd236f --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListSectionHeaderView/RoomListSectionHeaderView.test.tsx @@ -0,0 +1,32 @@ +/* + * 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 } from "@test-utils"; +import { composeStories } from "@storybook/react-vite"; +import { describe, it, expect } from "vitest"; +import userEvent from "@testing-library/user-event"; + +import * as stories from "./RoomListSectionHeaderView.stories"; + +const { Default } = composeStories(stories); + +describe(" stories", () => { + it("renders Default story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("should call onClick when the header is clicked", async () => { + const user = userEvent.setup(); + + const { getByRole } = render(); + const button = getByRole("gridcell", { name: "Toggle Favourites section" }); + await user.click(button); + expect(Default.args.onClick).toHaveBeenCalled(); + }); +}); diff --git a/packages/shared-components/src/room-list/RoomListSectionHeaderView/RoomListSectionHeaderView.tsx b/packages/shared-components/src/room-list/RoomListSectionHeaderView/RoomListSectionHeaderView.tsx new file mode 100644 index 00000000000..5008aa3870e --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListSectionHeaderView/RoomListSectionHeaderView.tsx @@ -0,0 +1,121 @@ +/* + * 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, { memo, type JSX, type FocusEvent, type MouseEventHandler } from "react"; +import ChevronRightIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-right"; +import classNames from "classnames"; + +import { useViewModel, type ViewModel } from "../../viewmodel"; +import styles from "./RoomListSectionHeaderView.module.css"; +import { Flex } from "../../utils/Flex"; +import { useI18n } from "../../utils/i18nContext"; +import { getGroupHeaderAccessibleProps } from "../../utils/VirtualizedList"; + +/** + * The observable state snapshot for a room list section header. + */ +export interface RoomListSectionHeaderViewSnapshot { + /** Unique identifier for the section header (used for list keying) */ + id: string; + /** The display title of the section header. */ + title: string; + /** Whether the section is currently expanded. */ + isExpanded: boolean; +} + +/** + * Actions that can be performed on a room list section header. + */ +export interface RoomListSectionHeaderActions { + /** Handler invoked when the section header is clicked (toggles expand/collapse). */ + onClick: MouseEventHandler; +} + +/** + * The view model type for the room list section header, combining its snapshot and actions. + */ +export type RoomListSectionHeaderViewModel = ViewModel; + +/** + * Props for {@link RoomListSectionHeaderView}. + */ +export interface RoomListSectionHeaderViewProps { + /** The view model driving the section header's state and actions. */ + vm: RoomListSectionHeaderViewModel; + /** Whether this header currently has focus within the roving tab index. */ + isFocused: boolean; + /** Callback invoked when the header receives focus. */ + onFocus: (headerId: string, e: FocusEvent) => void; + /** Index of this section in the list, sections and rooms included */ + indexInList: number; + /** Index of this section in the list related to the others sections */ + sectionIndex: number; + /** Total number of sections in the list */ + sectionCount: number; + /** Number of rooms in this section */ + roomCountInSection: number; +} + +/** + * A collapsible section header in the room list. + * + * Renders a button that displays the section title alongside a chevron icon + * indicating the current expand/collapse state. Clicking the header toggles + * the section's expanded state via the view model. + * + * @example + * ```tsx + * setFocusedHeader(sectionId)} + * sectionIndex={index} + * sectionCount={totalSections} + * roomCountInSection={roomCount} + * /> + * ``` + */ +export const RoomListSectionHeaderView = memo(function RoomListSectionHeaderView({ + vm, + isFocused, + onFocus, + indexInList, + sectionIndex, + sectionCount, + roomCountInSection, +}: Readonly): JSX.Element { + const { translate: _t } = useI18n(); + const { id, title, isExpanded } = useViewModel(vm); + const isLastSection = sectionIndex === sectionCount - 1; + + return ( +
+ +
+ ); +}); diff --git a/packages/shared-components/src/room-list/RoomListSectionHeaderView/__snapshots__/RoomListSectionHeaderView.test.tsx.snap b/packages/shared-components/src/room-list/RoomListSectionHeaderView/__snapshots__/RoomListSectionHeaderView.test.tsx.snap new file mode 100644 index 00000000000..753bfe632bd --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListSectionHeaderView/__snapshots__/RoomListSectionHeaderView.test.tsx.snap @@ -0,0 +1,50 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[` stories > renders Default story 1`] = ` +
+
+
+ +
+
+
+`; diff --git a/packages/shared-components/src/room-list/RoomListSectionHeaderView/index.ts b/packages/shared-components/src/room-list/RoomListSectionHeaderView/index.ts new file mode 100644 index 00000000000..29d15c98bbd --- /dev/null +++ b/packages/shared-components/src/room-list/RoomListSectionHeaderView/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { RoomListSectionHeaderView } from "./RoomListSectionHeaderView"; +export type { + RoomListSectionHeaderViewModel, + RoomListSectionHeaderViewSnapshot, + RoomListSectionHeaderActions, +} from "./RoomListSectionHeaderView"; diff --git a/packages/shared-components/src/room-list/RoomListView/RoomListView.stories.tsx b/packages/shared-components/src/room-list/RoomListView/RoomListView.stories.tsx index 05fb9af6cc7..4f500621167 100644 --- a/packages/shared-components/src/room-list/RoomListView/RoomListView.stories.tsx +++ b/packages/shared-components/src/room-list/RoomListView/RoomListView.stories.tsx @@ -18,8 +18,11 @@ import { renderAvatar, createGetRoomItemViewModel, mockRoomIds, - smallListRoomIds, - largeListRoomIds, + mockSections, + createGetSectionHeaderViewModel, + mockSmallListSections, + mockLargeListSections, + mockLargeListRoomIds, } from "../story-mocks"; type RoomListViewProps = RoomListSnapshot & RoomListViewActions & { renderAvatar: (room: Room) => React.ReactElement }; @@ -32,6 +35,7 @@ const RoomListViewWrapperImpl = ({ createChatRoom, createRoom, getRoomItemViewModel, + getSectionHeaderViewModel, updateVisibleRooms, renderAvatar: renderAvatarProp, ...rest @@ -41,6 +45,7 @@ const RoomListViewWrapperImpl = ({ createChatRoom, createRoom, getRoomItemViewModel, + getSectionHeaderViewModel, updateVisibleRooms, }); return ; @@ -81,15 +86,17 @@ const meta = { spaceId: "!space:server", filterKeys: undefined, }, - roomIds: mockRoomIds, + sections: mockSections, canCreateRoom: true, // Action properties (callbacks) onToggleFilter: fn(), createChatRoom: fn(), createRoom: fn(), getRoomItemViewModel: createGetRoomItemViewModel(mockRoomIds), + getSectionHeaderViewModel: createGetSectionHeaderViewModel(mockSections.map((section) => section.id)), updateVisibleRooms: fn(), renderAvatar, + isFlatList: true, }, parameters: { design: { @@ -104,6 +111,12 @@ type Story = StoryObj; export const Default: Story = {}; +export const Section: Story = { + args: { + isFlatList: false, + }, +}; + export const Loading: Story = { args: { isLoadingRooms: true, @@ -148,7 +161,6 @@ export const WithSelection: Story = { export const EmptyFavouriteFilter: Story = { args: { isRoomListEmpty: true, - roomIds: [], filterIds: ["favourite", "people"], activeFilterId: "favourite", }, @@ -157,7 +169,6 @@ export const EmptyFavouriteFilter: Story = { export const EmptyPeopleFilter: Story = { args: { isRoomListEmpty: true, - roomIds: [], filterIds: ["people", "rooms"], activeFilterId: "people", }, @@ -166,7 +177,6 @@ export const EmptyPeopleFilter: Story = { export const EmptyRoomsFilter: Story = { args: { isRoomListEmpty: true, - roomIds: [], filterIds: ["rooms", "people"], activeFilterId: "rooms", }, @@ -175,7 +185,6 @@ export const EmptyRoomsFilter: Story = { export const EmptyUnreadFilter: Story = { args: { isRoomListEmpty: true, - roomIds: [], filterIds: ["unread", "people"], activeFilterId: "unread", }, @@ -184,7 +193,6 @@ export const EmptyUnreadFilter: Story = { export const EmptyInvitesFilter: Story = { args: { isRoomListEmpty: true, - roomIds: [], filterIds: ["invites", "people"], activeFilterId: "invites", }, @@ -193,7 +201,7 @@ export const EmptyInvitesFilter: Story = { export const EmptyMentionsFilter: Story = { args: { isRoomListEmpty: true, - roomIds: [], + filterIds: ["mentions", "people"], activeFilterId: "mentions", }, @@ -202,22 +210,37 @@ export const EmptyMentionsFilter: Story = { export const EmptyLowPriorityFilter: Story = { args: { isRoomListEmpty: true, - roomIds: [], filterIds: ["low_priority", "people"], activeFilterId: "low_priority", }, }; -export const SmallList: Story = { +export const SmallFlatList: Story = { + args: { + sections: mockSmallListSections, + }, +}; + +export const LargeFlatList: Story = { + args: { + sections: mockLargeListSections, + getRoomItemViewModel: createGetRoomItemViewModel(mockLargeListRoomIds), + getSectionHeaderViewModel: createGetSectionHeaderViewModel(mockLargeListSections.map((section) => section.id)), + }, +}; + +export const SmallSectionList: Story = { args: { - roomIds: smallListRoomIds, - getRoomItemViewModel: createGetRoomItemViewModel(smallListRoomIds), + isFlatList: false, + sections: mockSmallListSections, }, }; -export const LargeList: Story = { +export const LargeSectionList: Story = { args: { - roomIds: largeListRoomIds, - getRoomItemViewModel: createGetRoomItemViewModel(largeListRoomIds), + isFlatList: false, + sections: mockLargeListSections, + getRoomItemViewModel: createGetRoomItemViewModel(mockLargeListRoomIds), + getSectionHeaderViewModel: createGetSectionHeaderViewModel(mockLargeListSections.map((section) => section.id)), }, }; diff --git a/packages/shared-components/src/room-list/RoomListView/RoomListView.test.tsx b/packages/shared-components/src/room-list/RoomListView/RoomListView.test.tsx index fcafa061242..380600541a2 100644 --- a/packages/shared-components/src/room-list/RoomListView/RoomListView.test.tsx +++ b/packages/shared-components/src/room-list/RoomListView/RoomListView.test.tsx @@ -20,8 +20,10 @@ const { Empty, EmptyWithoutCreatePermission, WithActiveFilter, - SmallList, - LargeList, + SmallFlatList, + LargeFlatList, + SmallSectionList, + LargeSectionList, EmptyFavouriteFilter, EmptyPeopleFilter, EmptyRoomsFilter, @@ -67,13 +69,23 @@ describe("", () => { expect(container).toMatchSnapshot(); }); - it("renders SmallList story", () => { - const { container } = renderWithMockContext(); + it("renders SmallFlatList story", () => { + const { container } = renderWithMockContext(); expect(container).toMatchSnapshot(); }); - it("renders LargeList story", () => { - const { container } = renderWithMockContext(); + it("renders LargeFlatList story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("renders SmallSectionList story", () => { + const { container } = renderWithMockContext(); + expect(container).toMatchSnapshot(); + }); + + it("renders LargeSectionList story", () => { + const { container } = renderWithMockContext(); expect(container).toMatchSnapshot(); }); diff --git a/packages/shared-components/src/room-list/RoomListView/RoomListView.tsx b/packages/shared-components/src/room-list/RoomListView/RoomListView.tsx index e72e9fb3ec9..3fd4450d0b2 100644 --- a/packages/shared-components/src/room-list/RoomListView/RoomListView.tsx +++ b/packages/shared-components/src/room-list/RoomListView/RoomListView.tsx @@ -13,6 +13,14 @@ import { RoomListLoadingSkeleton } from "./RoomListLoadingSkeleton"; import { RoomListEmptyStateView } from "./RoomListEmptyStateView"; import { VirtualizedRoomListView, type RoomListViewState } from "../VirtualizedRoomListView"; import { type Room, type RoomItemViewModel } from "../RoomListItemView"; +import { type RoomListSectionHeaderViewModel } from "../RoomListSectionHeaderView"; + +export type RoomListSection = { + /** Unique identifier for the section */ + id: string; + /** Array of room IDs that belong to this section */ + roomIds: string[]; +}; /** * Snapshot for the room list view @@ -28,14 +36,16 @@ export type RoomListSnapshot = { activeFilterId?: FilterId; /** Room list state */ roomListState: RoomListViewState; - /** Array of room IDs for virtualization */ - roomIds: string[]; + /** Array of sections in the room list */ + sections: RoomListSection[]; /** Optional description for the empty state */ emptyStateDescription?: string; /** Optional action element for the empty state */ emptyStateAction?: ReactNode; /** Whether the user can create rooms */ canCreateRoom?: boolean; + /** Whether the room list is displayed as a flat list */ + isFlatList: boolean; }; /** @@ -52,6 +62,8 @@ export interface RoomListViewActions { getRoomItemViewModel: (roomId: string) => RoomItemViewModel; /** Called when the visible range changes (virtualization API) */ updateVisibleRooms: (startIndex: number, endIndex: number) => void; + /** Get view model for a specific section header (virtualization API) */ + getSectionHeaderViewModel: (sectionId: string) => RoomListSectionHeaderViewModel; } /** diff --git a/packages/shared-components/src/room-list/RoomListView/__snapshots__/RoomListView.test.tsx.snap b/packages/shared-components/src/room-list/RoomListView/__snapshots__/RoomListView.test.tsx.snap index f2e461abc55..e6bfaa8dad1 100644 --- a/packages/shared-components/src/room-list/RoomListView/__snapshots__/RoomListView.test.tsx.snap +++ b/packages/shared-components/src/room-list/RoomListView/__snapshots__/RoomListView.test.tsx.snap @@ -2808,7 +2808,7 @@ exports[` > renders EmptyFavouriteFilter story 1`] = `
@@ -2867,7 +2867,7 @@ exports[` > renders EmptyInvitesFilter story 1`] = `
@@ -2930,7 +2930,7 @@ exports[` > renders EmptyLowPriorityFilter story 1`] = `
@@ -2993,7 +2993,7 @@ exports[` > renders EmptyMentionsFilter story 1`] = `
@@ -3056,7 +3056,7 @@ exports[` > renders EmptyPeopleFilter story 1`] = `
@@ -3115,7 +3115,7 @@ exports[` > renders EmptyRoomsFilter story 1`] = `
@@ -3174,7 +3174,7 @@ exports[` > renders EmptyUnreadFilter story 1`] = `
@@ -3326,7 +3326,7 @@ exports[` > renders EmptyWithoutCreatePermission story 1`] = `
`; -exports[` > renders LargeList story 1`] = ` +exports[` > renders LargeFlatList story 1`] = `
> renders LargeList story 1`] = `
@@ -3459,11 +3459,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_mp_" + aria-labelledby="_r_l9_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_mn_" + id="radix-_r_l7_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -3491,11 +3491,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_n0_" + aria-labelledby="_r_lg_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_mu_" + id="radix-_r_le_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -3602,11 +3602,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_n7_" + aria-labelledby="_r_ln_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_n5_" + id="radix-_r_ll_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -3634,11 +3634,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_ne_" + aria-labelledby="_r_lu_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_nc_" + id="radix-_r_ls_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -3730,11 +3730,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_nl_" + aria-labelledby="_r_m5_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_nj_" + id="radix-_r_m3_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -3762,11 +3762,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_ns_" + aria-labelledby="_r_mc_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_nq_" + id="radix-_r_ma_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -3852,11 +3852,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_o3_" + aria-labelledby="_r_mj_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_o1_" + id="radix-_r_mh_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -3884,11 +3884,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_oa_" + aria-labelledby="_r_mq_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_o8_" + id="radix-_r_mo_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -3980,11 +3980,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_oh_" + aria-labelledby="_r_n1_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_of_" + id="radix-_r_mv_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -4012,11 +4012,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_oo_" + aria-labelledby="_r_n8_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_om_" + id="radix-_r_n6_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -4102,11 +4102,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_ov_" + aria-labelledby="_r_nf_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_ot_" + id="radix-_r_nd_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -4134,11 +4134,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_p6_" + aria-labelledby="_r_nm_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_p4_" + id="radix-_r_nk_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -4253,11 +4253,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_pd_" + aria-labelledby="_r_nt_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_pb_" + id="radix-_r_nr_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -4285,11 +4285,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_pk_" + aria-labelledby="_r_o4_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_pi_" + id="radix-_r_o2_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -4375,11 +4375,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_pr_" + aria-labelledby="_r_ob_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_pp_" + id="radix-_r_o9_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -4407,11 +4407,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_q2_" + aria-labelledby="_r_oi_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_q0_" + id="radix-_r_og_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -4503,11 +4503,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_q9_" + aria-labelledby="_r_op_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_q7_" + id="radix-_r_on_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -4535,11 +4535,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_qg_" + aria-labelledby="_r_p0_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_qe_" + id="radix-_r_ou_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -4625,11 +4625,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_qn_" + aria-labelledby="_r_p7_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_ql_" + id="radix-_r_p5_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -4657,11 +4657,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_qu_" + aria-labelledby="_r_pe_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_qs_" + id="radix-_r_pc_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -4753,11 +4753,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_r5_" + aria-labelledby="_r_pl_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_r3_" + id="radix-_r_pj_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -4785,11 +4785,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_rc_" + aria-labelledby="_r_ps_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_ra_" + id="radix-_r_pq_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -4898,11 +4898,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_rj_" + aria-labelledby="_r_q3_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_rh_" + id="radix-_r_q1_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -4930,11 +4930,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_rq_" + aria-labelledby="_r_qa_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_ro_" + id="radix-_r_q8_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -5026,11 +5026,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_s1_" + aria-labelledby="_r_qh_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_rv_" + id="radix-_r_qf_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -5058,11 +5058,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_s8_" + aria-labelledby="_r_qo_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_s6_" + id="radix-_r_qm_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -5148,11 +5148,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_sf_" + aria-labelledby="_r_qv_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_sd_" + id="radix-_r_qt_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -5180,11 +5180,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_sm_" + aria-labelledby="_r_r6_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_sk_" + id="radix-_r_r4_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -5276,11 +5276,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_st_" + aria-labelledby="_r_rd_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_sr_" + id="radix-_r_rb_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -5308,11 +5308,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_t4_" + aria-labelledby="_r_rk_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_t2_" + id="radix-_r_ri_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -5398,11 +5398,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_tb_" + aria-labelledby="_r_rr_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_t9_" + id="radix-_r_rp_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -5430,11 +5430,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_ti_" + aria-labelledby="_r_s2_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_tg_" + id="radix-_r_s0_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -5549,11 +5549,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_tp_" + aria-labelledby="_r_s9_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_tn_" + id="radix-_r_s7_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -5581,11 +5581,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_u0_" + aria-labelledby="_r_sg_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_tu_" + id="radix-_r_se_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -5671,11 +5671,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_u7_" + aria-labelledby="_r_sn_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_u5_" + id="radix-_r_sl_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -5703,11 +5703,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_ue_" + aria-labelledby="_r_su_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_uc_" + id="radix-_r_ss_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -5799,11 +5799,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_ul_" + aria-labelledby="_r_t5_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_uj_" + id="radix-_r_t3_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -5831,11 +5831,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_us_" + aria-labelledby="_r_tc_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_uq_" + id="radix-_r_ta_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -5921,11 +5921,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_v3_" + aria-labelledby="_r_tj_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_v1_" + id="radix-_r_th_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -5953,11 +5953,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_va_" + aria-labelledby="_r_tq_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_v8_" + id="radix-_r_to_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -6049,11 +6049,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_vh_" + aria-labelledby="_r_u1_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_vf_" + id="radix-_r_tv_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -6081,11 +6081,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_vo_" + aria-labelledby="_r_u8_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_vm_" + id="radix-_r_u6_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -6194,11 +6194,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_vv_" + aria-labelledby="_r_uf_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_vt_" + id="radix-_r_ud_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -6226,11 +6226,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_106_" + aria-labelledby="_r_um_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_104_" + id="radix-_r_uk_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -6322,11 +6322,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_10d_" + aria-labelledby="_r_ut_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_10b_" + id="radix-_r_ur_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -6354,11 +6354,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_10k_" + aria-labelledby="_r_v4_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_10i_" + id="radix-_r_v2_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -6444,11 +6444,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_10r_" + aria-labelledby="_r_vb_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_10p_" + id="radix-_r_v9_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -6476,11 +6476,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_112_" + aria-labelledby="_r_vi_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_110_" + id="radix-_r_vg_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -6572,11 +6572,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_119_" + aria-labelledby="_r_vp_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_117_" + id="radix-_r_vn_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -6604,11 +6604,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_11g_" + aria-labelledby="_r_100_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_11e_" + id="radix-_r_vu_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -6694,11 +6694,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_11n_" + aria-labelledby="_r_107_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_11l_" + id="radix-_r_105_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -6726,11 +6726,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_11u_" + aria-labelledby="_r_10e_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_11s_" + id="radix-_r_10c_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -6845,11 +6845,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_125_" + aria-labelledby="_r_10l_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_123_" + id="radix-_r_10j_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -6877,11 +6877,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_12c_" + aria-labelledby="_r_10s_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_12a_" + id="radix-_r_10q_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -6967,11 +6967,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_12j_" + aria-labelledby="_r_113_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_12h_" + id="radix-_r_111_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -6999,11 +6999,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_12q_" + aria-labelledby="_r_11a_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_12o_" + id="radix-_r_118_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -7095,11 +7095,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_131_" + aria-labelledby="_r_11h_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_12v_" + id="radix-_r_11f_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -7127,11 +7127,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_138_" + aria-labelledby="_r_11o_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_136_" + id="radix-_r_11m_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -7217,11 +7217,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_13f_" + aria-labelledby="_r_11v_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_13d_" + id="radix-_r_11t_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -7249,11 +7249,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_13m_" + aria-labelledby="_r_126_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_13k_" + id="radix-_r_124_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -7345,11 +7345,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_13t_" + aria-labelledby="_r_12d_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_13r_" + id="radix-_r_12b_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -7377,11 +7377,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_144_" + aria-labelledby="_r_12k_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_142_" + id="radix-_r_12i_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -7490,11 +7490,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_14b_" + aria-labelledby="_r_12r_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_149_" + id="radix-_r_12p_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -7522,11 +7522,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_14i_" + aria-labelledby="_r_132_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_14g_" + id="radix-_r_130_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -7618,11 +7618,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_14p_" + aria-labelledby="_r_139_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_14n_" + id="radix-_r_137_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -7650,11 +7650,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_150_" + aria-labelledby="_r_13g_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_14u_" + id="radix-_r_13e_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -7740,11 +7740,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_157_" + aria-labelledby="_r_13n_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_155_" + id="radix-_r_13l_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -7772,11 +7772,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_15e_" + aria-labelledby="_r_13u_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_15c_" + id="radix-_r_13s_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -7868,11 +7868,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_15l_" + aria-labelledby="_r_145_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_15j_" + id="radix-_r_143_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -7900,11 +7900,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_15s_" + aria-labelledby="_r_14c_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_15q_" + id="radix-_r_14a_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -7990,11 +7990,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_163_" + aria-labelledby="_r_14j_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_161_" + id="radix-_r_14h_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -8022,11 +8022,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_16a_" + aria-labelledby="_r_14q_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_168_" + id="radix-_r_14o_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -8141,11 +8141,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_16h_" + aria-labelledby="_r_151_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_16f_" + id="radix-_r_14v_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -8173,11 +8173,11 @@ exports[` > renders LargeList story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_16o_" + aria-labelledby="_r_158_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_16m_" + id="radix-_r_156_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -8216,7 +8216,7 @@ exports[` > renders LargeList story 1`] = `
`; -exports[` > renders Loading story 1`] = ` +exports[` > renders LargeSectionList story 1`] = `
> renders Loading story 1`] = `
@@ -8274,497 +8274,5026 @@ exports[` > renders Loading story 1`] = `
-
-
-`; - -exports[` > renders SmallList story 1`] = ` -
-
-
+ aria-label="Room list" + aria-rowcount="103" + data-testid="room-list" + data-virtuoso-scroller="true" + role="treegrid" + style="height: 100%; outline: none; overflow-y: auto; position: relative;" + tabindex="0" + >
- - - - +
+ +
+
-
-
- -
+
+
+ + +
+ -
-
- + +
- + +
+ -
+ +
+
+
+
+ - +
+
+ + +
+ - -
- + +
- + +
+
+ +
+
+
+
+ +
+ +
+ +
+ +
+
+ - + +
+
- - - + +
- + +
+ + + + + +
+
+ + +
+ +
+ + + +
+
+ + +
+ +
+ + + +
+
+ + +
+ +
+ + + +
+
+ + +
+ +
+ + + + +
+
+ + +
+ +
+ + + +
+
+ + +
+ +
+ + + +
+
+ + +
+ +
+ + + +
+
+ + +
+ +
+ + + +
+
+ + +
+ +
+ + + + +
+
+ + +
+ +
+ + + +
+
+ + +
+ +
+ + + +
+
+ + +
+ +
+ + + +
+
+ + +
+ +
+ + + +
+
+ + +
+ +
+ + + + +
+
+ + +
+ +
+ + + +
+
+ + +
+ +
+ + + +
+
+ +
+
+
+
+ + +
+ +
+ + + +
+
+ + +
+ +
+ + + +
+
+ + +
+ +
+ + + + +
+
+ + +
+ +
+ + + +
+
+ + +
+ +
+ + + +
+
+ + +
+ +
+ + + +
+
+ + +
+ +
+ + + +
+
+ + +
+ +
+ + + + +
+
+ + +
+ +
+ + + +
+
+ + +
+ +
+ + + +
+
+ + +
+ +
+ + + +
+
+ + +
+ +
+ + + + + + + + +`; + +exports[` > renders Loading story 1`] = ` +
+
+
+
+
+ + + + +
+
+
+
+
+
+`; + +exports[` > renders SmallFlatList story 1`] = ` +
+
+
+
+
+ + + + +
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ + +
+ +
+ +
+
+
+
+ + +
+ +
+ +
+
+
+
+ +
+
+
+
+
+ + +`; + exports[` > renders WithActiveFilter story 1`] = `
React.ReactElement }; -// Use first 10 room IDs for this story -const storyRoomIds = mockRoomIds.slice(0, 10); - // Wrapper component that creates a mocked ViewModel const RoomListWrapperImpl = ({ onToggleFilter, createChatRoom, createRoom, getRoomItemViewModel, + getSectionHeaderViewModel, updateVisibleRooms, renderAvatar: renderAvatarProp, ...rest @@ -37,6 +41,7 @@ const RoomListWrapperImpl = ({ createChatRoom, createRoom, getRoomItemViewModel, + getSectionHeaderViewModel, updateVisibleRooms, }); @@ -65,15 +70,17 @@ const meta = { isRoomListEmpty: false, filterIds: mockFilterIds, activeFilterId: undefined, - roomIds: storyRoomIds, + sections: mock10RoomsSections, roomListState: defaultRoomListState, canCreateRoom: true, onToggleFilter: fn(), createChatRoom: fn(), createRoom: fn(), - getRoomItemViewModel: createGetRoomItemViewModel(storyRoomIds), + getRoomItemViewModel: createGetRoomItemViewModel(mock10RoomsIds), + getSectionHeaderViewModel: createGetSectionHeaderViewModel(mock10RoomsSections.map((section) => section.id)), updateVisibleRooms: fn(), renderAvatar, + isFlatList: true, }, parameters: { design: { @@ -94,3 +101,9 @@ export default meta; type Story = StoryObj; export const Default: Story = {}; + +export const Sections: Story = { + args: { + isFlatList: false, + }, +}; diff --git a/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.tsx b/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.tsx index 71cd554ecc6..63f7761d95d 100644 --- a/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.tsx +++ b/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.tsx @@ -9,11 +9,18 @@ import React, { useCallback, useMemo, useRef, type JSX, type ReactNode } from "r import { type ScrollIntoViewLocation } from "react-virtuoso"; import { isEqual } from "lodash"; -import { RoomListItemView, type Room } from "../RoomListItemView"; +import { type Room } from "../RoomListItemView"; import { useViewModel } from "../../viewmodel"; import { _t } from "../../utils/i18n"; -import { FlatVirtualizedList, type VirtualizedListContext } from "../../utils/VirtualizedList"; -import type { RoomListViewModel } from "../RoomListView"; +import { + FlatVirtualizedList, + getContainerAccessibleProps, + type VirtualizedListContext, +} from "../../utils/VirtualizedList"; +import type { RoomListSnapshot, RoomListViewModel } from "../RoomListView"; +import { GroupedVirtualizedList } from "../../utils/VirtualizedList"; +import { RoomListSectionHeaderView } from "../RoomListSectionHeaderView"; +import { RoomListItemAccessibilityWrapper } from "../RoomListItemAccessibilityWrapper"; /** * Filter key type - opaque string type for filter identifiers @@ -59,7 +66,24 @@ const ROOM_LIST_ITEM_HEIGHT = 52; /** * Type for context used in ListView */ -type Context = { spaceId: string; filterKeys: FilterKey[] | undefined }; +type Context = { + /** Space ID for context tracking */ + spaceId: string; + /** Active filter keys for context tracking */ + filterKeys: FilterKey[] | undefined; + /** Active room index for keyboard navigation */ + activeRoomIndex: number | undefined; + /** Sections of the room list */ + sections: RoomListSnapshot["sections"]; + /** Total number of rooms in the list */ + roomCount: number; + /** Number of sections in the list */ + sectionCount: number; + /** Room list view model */ + vm: RoomListViewModel; + /** List is in flat or section mode */ + isFlatList: boolean; +}; /** * Amount to extend the top and bottom of the viewport by. @@ -83,11 +107,23 @@ const EXTENDED_VIEWPORT_HEIGHT = 25 * ROOM_LIST_ITEM_HEIGHT; */ export function VirtualizedRoomListView({ vm, renderAvatar, onKeyDown }: VirtualizedRoomListViewProps): JSX.Element { const snapshot = useViewModel(vm); - const { roomListState, roomIds } = snapshot; + const { roomListState, sections, isFlatList } = snapshot; const activeRoomIndex = roomListState.activeRoomIndex; const lastSpaceId = useRef(undefined); const lastFilterKeys = useRef(undefined); + const roomIds = useMemo(() => sections.flatMap((section) => section.roomIds), [sections]); const roomCount = roomIds.length; + const sectionCount = sections.length; + const totalCount = roomCount + sectionCount; + + const groups = useMemo( + () => + sections.map((section) => ({ + header: section.id, + items: section.roomIds, + })), + [sections], + ); /** * Callback when the visible range changes @@ -110,7 +146,9 @@ export function VirtualizedRoomListView({ vm, renderAvatar, onKeyDown }: Virtual roomId: string, context: VirtualizedListContext, onFocus: (item: string, e: React.FocusEvent) => void, + roomIndexInSection: number, ): JSX.Element => { + const { activeRoomIndex, roomCount, vm, isFlatList } = context.context; const isSelected = activeRoomIndex === index; const roomItemVM = vm.getRoomItemViewModel(roomId); @@ -118,8 +156,11 @@ export function VirtualizedRoomListView({ vm, renderAvatar, onKeyDown }: Virtual // This matches the old RoomList implementation's roving tabindex pattern const isFocused = context.focused && context.tabIndexKey === roomId; + const isFirstItem = index === 0; + const isLastItem = index === roomCount - 1; + return ( - + ); + }, + [renderAvatar], + ); + + /** + * Get the item component for a specific index in a grouped list + * Since we have sections, we can calculate the room's index within its section and pass it to getItemComponent + * Gets the room's view model and passes it to RoomListItemView + */ + const getItemComponentForGroupedList = useCallback( + ( + index: number, + roomId: string, + context: VirtualizedListContext, + onFocus: (item: string, e: React.FocusEvent) => void, + groupIndex: number, + ): JSX.Element => { + const { sections } = context.context; + const roomIndexInSection = sections[groupIndex].roomIds.findIndex((id) => id === roomId); + return getItemComponent(index, roomId, context, onFocus, roomIndexInSection); + }, + [getItemComponent], + ); + + /** + * Get the item component for a specific index in a flat list + * Since we don't have sections, we can pass 0 for the room's index within its section to getItemComponent + * Gets the room's view model and passes it to RoomListItemView + */ + const getItemComponentForFlatList = useCallback( + ( + index: number, + roomId: string, + context: VirtualizedListContext, + onFocus: (item: string, e: React.FocusEvent) => void, + ): JSX.Element => { + // For a flat list, we don't have sections, so roomIndexInSection is unused and can be set to 0 + return getItemComponent(index, roomId, context, onFocus, 0); + }, + [getItemComponent], + ); + + /** + * Get the group header component for a specific group + */ + const getGroupHeaderComponent = useCallback( + ( + groupIndex: number, + headerId: string, + context: VirtualizedListContext, + onFocus: (header: string, e: React.FocusEvent) => void, + ): JSX.Element => { + const { vm, sectionCount, sections } = context.context; + const sectionHeaderVM = vm.getSectionHeaderViewModel(headerId); + const indexInList = sections + .slice(0, groupIndex) + // +1 for each section header + .reduce((acc, section) => acc + section.roomIds.length + 1, 0); + const roomCountInSection = sections[groupIndex].roomIds.length; + + // 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 === headerId; + + 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 getItemKey = useCallback((item: string): string => item, []); + + /** + * Get the key for a group header + * We are passing the section ID as the header key, which is a string, so we can return it directly + */ + const getHeaderKey = useCallback((header: string): string => header, []); const context = useMemo( - () => ({ spaceId: roomListState.spaceId || "", filterKeys: roomListState.filterKeys }), - [roomListState.spaceId, roomListState.filterKeys], + () => ({ + spaceId: roomListState.spaceId || "", + filterKeys: roomListState.filterKeys, + sections, + activeRoomIndex, + roomCount, + sectionCount, + vm, + isFlatList, + }), + [ + roomListState.spaceId, + roomListState.filterKeys, + sections, + activeRoomIndex, + roomCount, + sectionCount, + vm, + isFlatList, + ], ); /** @@ -173,26 +315,51 @@ export function VirtualizedRoomListView({ vm, renderAvatar, onKeyDown }: Virtual [activeRoomIndex], ); + const isItemFocusable = useCallback(() => true, []); + const isGroupHeaderFocusable = useCallback(() => true, []); + const increaseViewportBy = useMemo( + () => ({ + top: EXTENDED_VIEWPORT_HEIGHT, + bottom: EXTENDED_VIEWPORT_HEIGHT, + }), + [], + ); + + const commonProps = { + context, + scrollIntoViewOnChange, + // If fixedItemHeight is not set and initialTopMostItemIndex=undefined, virtuoso crashes + // If we don't set it, it works + ...(activeRoomIndex !== undefined ? { initialTopMostItemIndex: activeRoomIndex } : {}), + ["data-testid"]: "room-list", + ["aria-label"]: _t("room_list|list_title"), + getItemKey, + isItemFocusable, + rangeChanged, + onKeyDown, + increaseViewportBy, + }; + + if (isFlatList) { + return ( + + ); + } + return ( - true} - rangeChanged={rangeChanged} - onKeyDown={onKeyDown} - increaseViewportBy={{ - bottom: EXTENDED_VIEWPORT_HEIGHT, - top: EXTENDED_VIEWPORT_HEIGHT, - }} + + {...commonProps} + {...getContainerAccessibleProps("treegrid", totalCount)} + groups={groups} + getHeaderKey={getHeaderKey} + getGroupHeaderComponent={getGroupHeaderComponent} + getItemComponent={getItemComponentForGroupedList} + isGroupHeaderFocusable={isGroupHeaderFocusable} /> ); } diff --git a/packages/shared-components/src/room-list/story-mocks.tsx b/packages/shared-components/src/room-list/story-mocks.tsx index 1958a06419c..156858edcd1 100644 --- a/packages/shared-components/src/room-list/story-mocks.tsx +++ b/packages/shared-components/src/room-list/story-mocks.tsx @@ -9,6 +9,8 @@ import React from "react"; import { fn } from "storybook/test"; import { type Room, type RoomItemViewModel, type RoomListItemSnapshot, RoomNotifState } from "./RoomListItemView"; +import { type RoomListSectionHeaderViewModel } from "./RoomListSectionHeaderView"; +import { MockViewModel } from "../viewmodel"; /** * Mock avatar component for stories @@ -100,6 +102,23 @@ export const createMockRoomSnapshot = (id: string, name: string, index: number): roomNotifState: RoomNotifState.AllMessages, }); +export function createMockRoomItemViewModel(roomId: string, name: string, index: number): RoomItemViewModel { + const snapshot = createMockRoomSnapshot(roomId, name, index); + return { + getSnapshot: () => snapshot, + subscribe: fn(), + onOpenRoom: fn(), + onMarkAsRead: fn(), + onMarkAsUnread: fn(), + onToggleFavorite: fn(), + onToggleLowPriority: fn(), + onInvite: fn(), + onCopyRoomLink: fn(), + onLeaveRoom: fn(), + onSetRoomNotifState: fn(), + }; +} + /** * Create a mock getRoomItemViewModel function for stories */ @@ -107,31 +126,60 @@ export const createGetRoomItemViewModel = (roomIds: string[]): ((roomId: string) 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); + viewModels.set(roomId, createMockRoomItemViewModel(roomId, name, index)); }); return (roomId: string) => viewModels.get(roomId)!; }; +export const createGetSectionHeaderViewModel = ( + sectionIds: string[], +): ((sectionId: string) => RoomListSectionHeaderViewModel) => { + const viewModels = new Map(); + sectionIds.forEach((sectionId) => { + const snapshot = { + id: sectionId, + title: sectionId[0].toUpperCase() + sectionId.slice(1), + isExpanded: true, + }; + const vm = new MockViewModel(snapshot) as unknown as RoomListSectionHeaderViewModel; + Object.assign(vm, { + onClick: fn(), + onFocus: fn(), + }); + + viewModels.set(sectionId, vm); + }); + + return (sectionId: string) => viewModels.get(sectionId)!; +}; + /** * Mock room IDs for different list sizes */ +export const mock10RoomsIds = Array.from({ length: 10 }, (_, i) => `!room${i}:server`); +export const mock10RoomsSections = [ + { id: "favourites", roomIds: mock10RoomsIds.slice(0, 3) }, + { id: "chats", roomIds: mock10RoomsIds.slice(3, 4) }, + { id: "low-priority", roomIds: mock10RoomsIds.slice(4) }, +]; + 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`); +export const mockSections = [ + { id: "favourites", roomIds: mockRoomIds.slice(0, 5) }, + { id: "chats", roomIds: mockRoomIds.slice(5, 15) }, + { id: "low-priority", roomIds: mockRoomIds.slice(15) }, +]; + +export const mockSmallListRoomIds = mockRoomIds.slice(0, 5); +export const mockSmallListSections = [ + { id: "favourites", roomIds: mockSmallListRoomIds.slice(0, 2) }, + { id: "chats", roomIds: mockSmallListRoomIds.slice(2, 0) }, +]; + +export const mockLargeListRoomIds = Array.from({ length: 100 }, (_, i) => `!room${i}:server`); +export const mockLargeListSections = [ + { id: "favourites", roomIds: mockLargeListRoomIds.slice(0, 23) }, + { id: "chats", roomIds: mockLargeListRoomIds.slice(23, 52) }, + { id: "low-priority", roomIds: mockLargeListRoomIds.slice(52) }, +]; diff --git a/packages/shared-components/vitest.config.ts b/packages/shared-components/vitest.config.ts index b28a1718e34..7b0f22f9e09 100644 --- a/packages/shared-components/vitest.config.ts +++ b/packages/shared-components/vitest.config.ts @@ -153,11 +153,7 @@ export default defineConfig({ ], }, optimizeDeps: { - include: [ - "vite-plugin-node-polyfills/shims/buffer", - "vite-plugin-node-polyfills/shims/process", - "react-virtuoso", - ], + include: ["vite-plugin-node-polyfills/shims/buffer", "vite-plugin-node-polyfills/shims/process"], }, resolve: { alias: {