diff --git a/package.json b/package.json index 417539d005..aa21689838 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,6 @@ "@matrix-org/spec": "^1.7.0", "@sentry/browser": "^10.0.0", "@types/png-chunks-extract": "^1.0.2", - "@types/react-virtualized": "^9.21.30", "@vector-im/compound-design-tokens": "^6.0.0", "@vector-im/compound-web": "^8.1.2", "@vector-im/matrix-wysiwyg": "2.39.0", @@ -153,8 +152,7 @@ "react-focus-lock": "^2.5.1", "react-string-replace": "^1.1.1", "react-transition-group": "^4.4.1", - "react-virtualized": "^9.22.5", - "react-virtuoso": "^4.12.6", + "react-virtuoso": "^4.14.0", "rfc4648": "^1.4.0", "sanitize-filename": "^1.6.3", "sanitize-html": "2.17.0", diff --git a/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts b/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts index 0e92e734c5..09dc010d3b 100644 --- a/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts +++ b/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts @@ -68,7 +68,7 @@ test.describe("Room list filters and sort", () => { So we expect 'Old Room' to show up in the room list. */ const roomListView = getRoomList(page); - const oldRoomTile = roomListView.getByRole("gridcell", { name: "Open room Old Room" }); + const oldRoomTile = roomListView.getByRole("option", { name: "Open room Old Room" }); await expect(oldRoomTile).toBeVisible(); /* @@ -139,8 +139,9 @@ test.describe("Room list filters and sort", () => { // Open the non-favourite room const roomListView = getRoomList(page); - const tile = roomListView.getByRole("gridcell", { name: "Open room room-non-fav" }); - await tile.scrollIntoViewIfNeeded(); + const tile = roomListView.getByRole("option", { name: "Open room room-non-fav" }); + // item may not be in the DOM using scrollListToBottom rather than scrollIntoViewIfNeeded + await app.scrollListToBottom(roomListView); await tile.click(); // Enable Favourite filter @@ -151,7 +152,7 @@ test.describe("Room list filters and sort", () => { // Ensure the room list is not scrolled const isScrolledDown = await page - .getByRole("grid", { name: "Room list" }) + .getByRole("listbox", { name: "Room list", exact: true }) .evaluate((e) => e.scrollTop !== 0); expect(isScrolledDown).toStrictEqual(false); }); @@ -227,37 +228,37 @@ test.describe("Room list filters and sort", () => { await primaryFilters.getByRole("option", { name: "Unread" }).click(); // only one room should be visible - await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible(); - await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible(); - await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(4); + await expect(roomList.getByRole("option", { name: "unread dm" })).toBeVisible(); + await expect(roomList.getByRole("option", { name: "unread room" })).toBeVisible(); + await expect.poll(() => roomList.locator("role=option").count()).toBe(4); await expect(primaryFilters).toMatchScreenshot("unread-primary-filters.png"); await primaryFilters.getByRole("option", { name: "People" }).click(); - await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible(); - await expect(roomList.getByRole("gridcell", { name: "invited room" })).toBeVisible(); - await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(2); + await expect(roomList.getByRole("option", { name: "unread dm" })).toBeVisible(); + await expect(roomList.getByRole("option", { name: "invited room" })).toBeVisible(); + await expect.poll(() => roomList.locator("role=option").count()).toBe(2); await primaryFilters.getByRole("option", { name: "Rooms" }).click(); - await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible(); - await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible(); - await expect(roomList.getByRole("gridcell", { name: "empty room" })).toBeVisible(); - await expect(roomList.getByRole("gridcell", { name: "room with mention" })).toBeVisible(); - await expect(roomList.getByRole("gridcell", { name: "Low prio room" })).toBeVisible(); - await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(5); + await expect(roomList.getByRole("option", { name: "unread room" })).toBeVisible(); + await expect(roomList.getByRole("option", { name: "favourite room" })).toBeVisible(); + await expect(roomList.getByRole("option", { name: "empty room" })).toBeVisible(); + await expect(roomList.getByRole("option", { name: "room with mention" })).toBeVisible(); + await expect(roomList.getByRole("option", { name: "Low prio room" })).toBeVisible(); + await expect.poll(() => roomList.locator("role=option").count()).toBe(5); await getFilterExpandButton(page).click(); await primaryFilters.getByRole("option", { name: "Favourite" }).click(); - await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible(); - await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(1); + await expect(roomList.getByRole("option", { name: "favourite room" })).toBeVisible(); + await expect.poll(() => roomList.locator("role=option").count()).toBe(1); await primaryFilters.getByRole("option", { name: "Mentions" }).click(); - await expect(roomList.getByRole("gridcell", { name: "room with mention" })).toBeVisible(); - await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(1); + await expect(roomList.getByRole("option", { name: "room with mention" })).toBeVisible(); + await expect.poll(() => roomList.locator("role=option").count()).toBe(1); await primaryFilters.getByRole("option", { name: "Invites" }).click(); - await expect(roomList.getByRole("gridcell", { name: "invited room" })).toBeVisible(); - await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(1); + await expect(roomList.getByRole("option", { name: "invited room" })).toBeVisible(); + await expect.poll(() => roomList.locator("role=option").count()).toBe(1); await getFilterCollapseButton(page).click(); await expect(primaryFilters.locator("role=option").first()).toHaveText("Invites"); @@ -268,6 +269,7 @@ test.describe("Room list filters and sort", () => { { tag: "@screenshot" }, async ({ page, app, bot }) => { const roomListView = getRoomList(page); + const primaryFilters = getPrimaryFilters(page); // Let's configure unread dm room so that we only get notification for mentions and keywords await app.viewRoomById(unReadDmId); @@ -276,20 +278,20 @@ test.describe("Room list filters and sort", () => { await app.settings.closeDialog(); // Let's open a room other than unread room or unread dm - await roomListView.getByRole("gridcell", { name: "Open room favourite room" }).click(); + await roomListView.getByRole("option", { name: "Open room favourite room" }).click(); // Let's make the bot send a new message in both rooms await bot.sendMessage(unReadDmId, "Hello!"); await bot.sendMessage(unReadRoomId, "Hello!"); // Let's activate the unread filter now - await page.getByRole("option", { name: "Unread" }).click(); + await primaryFilters.getByRole("option", { name: "Unread" }).click(); // Unread filter should only show unread room and not unread dm! - const unreadDm = roomListView.getByRole("gridcell", { name: "Open room unread room" }); + const unreadDm = roomListView.getByRole("option", { name: "Open room unread room" }); await expect(unreadDm).toBeVisible(); await expect(unreadDm).toMatchScreenshot("unread-dm.png"); - await expect(roomListView.getByRole("gridcell", { name: "Open room unread dm" })).not.toBeVisible(); + await expect(roomListView.getByRole("option", { name: "Open room unread dm" })).not.toBeVisible(); }, ); @@ -299,7 +301,7 @@ test.describe("Room list filters and sort", () => { await getRoomOptionsMenu(page).click(); await page.getByRole("menuitemradio", { name: "A-Z" }).click(); - await expect(roomListView.getByRole("gridcell").first()).toHaveText(/empty room/); + await expect(roomListView.getByRole("option").first()).toHaveText(/empty room/); }); test("should move room to the top on message when sorting by activity", async ({ page, bot }) => { @@ -307,7 +309,7 @@ test.describe("Room list filters and sort", () => { await bot.sendMessage(unReadDmId, "Hello!"); - await expect(roomListView.getByRole("gridcell").first()).toHaveText(/unread dm/); + await expect(roomListView.getByRole("option").first()).toHaveText(/unread dm/); }); }); diff --git a/playwright/e2e/left-panel/room-list-panel/room-list-panel.spec.ts b/playwright/e2e/left-panel/room-list-panel/room-list-panel.spec.ts index d0503e2caf..bc1387cbce 100644 --- a/playwright/e2e/left-panel/room-list-panel/room-list-panel.spec.ts +++ b/playwright/e2e/left-panel/room-list-panel/room-list-panel.spec.ts @@ -38,7 +38,7 @@ test.describe("Room list panel", () => { test("should render the room list panel", { tag: "@screenshot" }, async ({ page, app, user }) => { const roomListView = getRoomListView(page); // Wait for the last room to be visible - await expect(roomListView.getByRole("gridcell", { name: "Open room room19" })).toBeVisible(); + await expect(roomListView.getByRole("option", { name: "Open room room19" })).toBeVisible(); await expect(roomListView).toMatchScreenshot("room-list-panel.png"); }); diff --git a/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts b/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts index a91a0c38d0..2d62737b85 100644 --- a/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts +++ b/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts @@ -43,31 +43,35 @@ test.describe("Room list", () => { test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user }) => { const roomListView = getRoomList(page); - await expect(roomListView.getByRole("gridcell", { name: "Open room room29" })).toBeVisible(); + await expect(roomListView.getByRole("option", { name: "Open room room29" })).toBeVisible(); await expect(roomListView).toMatchScreenshot("room-list.png"); // Put focus on the room list - await roomListView.getByRole("gridcell", { name: "Open room room29" }).click(); + await roomListView.getByRole("option", { name: "Open room room29" }).click(); // Scroll to the end of the room list - await app.scrollListToBottom(page.locator(".mx_RoomList_List")); + await app.scrollListToBottom(roomListView); + + // scrollListToBottom seems to leave the mouse hovered over the list, move it away. + await page.getByRole("button", { name: "User menu" }).hover(); + await expect(roomListView).toMatchScreenshot("room-list-scrolled.png"); }); test("should open the room when it is clicked", async ({ page, app, user }) => { const roomListView = getRoomList(page); - await roomListView.getByRole("gridcell", { name: "Open room room29" }).click(); + await roomListView.getByRole("option", { name: "Open room room29" }).click(); await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible(); }); test("should open the context menu", { tag: "@screenshot" }, async ({ page, app, user }) => { const roomListView = getRoomList(page); - await roomListView.getByRole("gridcell", { name: "Open room room29" }).click({ button: "right" }); + await roomListView.getByRole("option", { name: "Open room room29" }).click({ button: "right" }); await expect(page.getByRole("menu", { name: "More Options" })).toBeVisible(); }); test("should open the more options menu", { tag: "@screenshot" }, async ({ page, app, user }) => { const roomListView = getRoomList(page); - const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" }); + const roomItem = roomListView.getByRole("option", { name: "Open room room29" }); await roomItem.hover(); await expect(roomItem).toMatchScreenshot("room-list-item-hover.png"); @@ -97,7 +101,7 @@ test.describe("Room list", () => { test("should open the notification options menu", { tag: "@screenshot" }, async ({ page, app, user }) => { const roomListView = getRoomList(page); - const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" }); + const roomItem = roomListView.getByRole("option", { name: "Open room room29" }); await roomItem.hover(); await expect(roomItem).toMatchScreenshot("room-list-item-hover.png"); @@ -117,10 +121,10 @@ test.describe("Room list", () => { await expect(roomItem.getByTestId("notification-decoration")).not.toBeVisible(); // Put focus on the room list - await roomListView.getByRole("gridcell", { name: "Open room room28" }).click(); + await roomListView.getByRole("option", { name: "Open room room28" }).click(); // Scroll to the end of the room list - await app.scrollListToBottom(page.locator(".mx_RoomList_List")); + await app.scrollListToBottom(roomListView); // The room decoration should have the muted icon await expect(roomItem.getByTestId("notification-decoration")).toBeVisible(); @@ -139,25 +143,25 @@ test.describe("Room list", () => { test("should scroll to the current room", async ({ page, app, user }) => { const roomListView = getRoomList(page); // Put focus on the room list - await roomListView.getByRole("gridcell", { name: "Open room room29" }).click(); + await roomListView.getByRole("option", { name: "Open room room29" }).click(); // Scroll to the end of the room list - await app.scrollListToBottom(page.locator(".mx_RoomList_List")); + await app.scrollListToBottom(roomListView); - await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible(); - await roomListView.getByRole("gridcell", { name: "Open room room0" }).click(); + await expect(roomListView.getByRole("option", { name: "Open room room0" })).toBeVisible(); + await roomListView.getByRole("option", { name: "Open room room0" }).click(); const filters = page.getByRole("listbox", { name: "Room list filters" }); await filters.getByRole("option", { name: "People" }).click(); - await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).not.toBeVisible(); + await expect(roomListView.getByRole("option", { name: "Open room room0" })).not.toBeVisible(); await filters.getByRole("option", { name: "People" }).click(); - await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible(); + await expect(roomListView.getByRole("option", { name: "Open room room0" })).toBeVisible(); }); test.describe("Shortcuts", () => { test("should select the next room", async ({ page, app, user }) => { const roomListView = getRoomList(page); - await roomListView.getByRole("gridcell", { name: "Open room room29" }).click(); + await roomListView.getByRole("option", { name: "Open room room29" }).click(); await page.keyboard.press("Alt+ArrowDown"); await expect(page.getByRole("heading", { name: "room28", level: 1 })).toBeVisible(); @@ -165,7 +169,7 @@ test.describe("Room list", () => { test("should select the previous room", async ({ page, app, user }) => { const roomListView = getRoomList(page); - await roomListView.getByRole("gridcell", { name: "Open room room28" }).click(); + await roomListView.getByRole("option", { name: "Open room room28" }).click(); await page.keyboard.press("Alt+ArrowUp"); await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible(); @@ -173,7 +177,7 @@ test.describe("Room list", () => { test("should select the last room", async ({ page, app, user }) => { const roomListView = getRoomList(page); - await roomListView.getByRole("gridcell", { name: "Open room room29" }).click(); + await roomListView.getByRole("option", { name: "Open room room29" }).click(); await page.keyboard.press("Alt+ArrowUp"); await expect(page.getByRole("heading", { name: "room0", level: 1 })).toBeVisible(); @@ -187,7 +191,7 @@ test.describe("Room list", () => { await bot.joinRoom(roomId); await bot.sendMessage(roomId, "I am a robot. Beep."); - await roomListView.getByRole("gridcell", { name: "Open room room20" }).click(); + await roomListView.getByRole("option", { name: "Open room room20" }).click(); await page.keyboard.press("Alt+Shift+ArrowDown"); @@ -199,8 +203,8 @@ test.describe("Room list", () => { test("should navigate to the room list", async ({ page, app, user }) => { const roomListView = getRoomList(page); - const room29 = roomListView.getByRole("gridcell", { name: "Open room room29" }); - const room28 = roomListView.getByRole("gridcell", { name: "Open room room28" }); + const room29 = roomListView.getByRole("option", { name: "Open room room29" }); + const room28 = roomListView.getByRole("option", { name: "Open room room28" }); // open the room await room29.click(); @@ -219,7 +223,7 @@ test.describe("Room list", () => { test("should navigate to the notification menu", async ({ page, app, user }) => { const roomListView = getRoomList(page); - const room29 = roomListView.getByRole("gridcell", { name: "Open room room29" }); + const room29 = roomListView.getByRole("option", { name: "Open room room29" }); const moreButton = room29.getByRole("button", { name: "More options" }); const notificationButton = room29.getByRole("button", { name: "Notification options" }); @@ -258,7 +262,7 @@ test.describe("Room list", () => { await page.getByRole("button", { name: "User menu" }).focus(); const roomListView = getRoomList(page); - const publicRoom = roomListView.getByRole("gridcell", { name: "public room" }); + const publicRoom = roomListView.getByRole("option", { name: "public room" }); await expect(publicRoom).toBeVisible(); await expect(publicRoom).toMatchScreenshot("room-list-item-public.png"); @@ -268,7 +272,7 @@ test.describe("Room list", () => { // @ts-ignore Visibility enum is not accessible await app.client.createRoom({ name: "low priority room", visibility: "public" }); const roomListView = getRoomList(page); - const publicRoom = roomListView.getByRole("gridcell", { name: "low priority room" }); + const publicRoom = roomListView.getByRole("option", { name: "low priority room" }); // Make room low priority await publicRoom.hover(); @@ -293,7 +297,7 @@ test.describe("Room list", () => { await page.getByRole("button", { name: "Create video room" }).click(); const roomListView = getRoomList(page); - const videoRoom = roomListView.getByRole("gridcell", { name: "video room" }); + const videoRoom = roomListView.getByRole("option", { name: "video room" }); // focus the user menu to avoid to have hover decoration await page.getByRole("button", { name: "User menu" }).focus(); @@ -312,7 +316,7 @@ test.describe("Room list", () => { invite: [user.userId], is_direct: true, }); - const invitedRoom = roomListView.getByRole("gridcell", { name: "invited room" }); + const invitedRoom = roomListView.getByRole("option", { name: "invited room" }); await expect(invitedRoom).toBeVisible(); await expect(invitedRoom).toMatchScreenshot("room-list-item-invited.png"); }); @@ -327,7 +331,7 @@ test.describe("Room list", () => { await bot.sendMessage(roomId, "I am a robot. Beep."); await bot.sendMessage(roomId, "I am a robot. Beep."); - const room = roomListView.getByRole("gridcell", { name: "2 notifications" }); + const room = roomListView.getByRole("option", { name: "2 notifications" }); await expect(room).toBeVisible(); await expect(room.getByTestId("notification-decoration")).toHaveText("2"); await expect(room).toMatchScreenshot("room-list-item-notification.png"); @@ -358,7 +362,7 @@ test.describe("Room list", () => { ); await bot.sendMessage(roomId, "I am a robot. Beep."); - const room = roomListView.getByRole("gridcell", { name: "mention" }); + const room = roomListView.getByRole("option", { name: "mention" }); await expect(room).toBeVisible(); await expect(room).toMatchScreenshot("room-list-item-mention.png"); }); @@ -379,7 +383,7 @@ test.describe("Room list", () => { await bot.joinRoom(roomId); await bot.sendMessage(roomId, "I am a robot. Beep."); - const room = roomListView.getByRole("gridcell", { name: "activity" }); + const room = roomListView.getByRole("option", { name: "activity" }); await expect(room.getByText("I am a robot. Beep.")).toBeVisible(); await expect(room).toMatchScreenshot("room-list-item-message-preview.png"); }); @@ -406,7 +410,7 @@ test.describe("Room list", () => { await app.viewRoomById(otherRoomId); await bot.sendMessage(roomId, "I am a robot. Beep."); - const room = roomListView.getByRole("gridcell", { name: "activity" }); + const room = roomListView.getByRole("option", { name: "activity" }); await expect(room.getByTestId("notification-decoration")).toBeVisible(); await expect(room).toMatchScreenshot("room-list-item-activity.png"); }); @@ -418,7 +422,7 @@ test.describe("Room list", () => { await app.client.inviteUser(roomId, bot.credentials.userId); await bot.joinRoom(roomId); - const room = roomListView.getByRole("gridcell", { name: "mark as unread" }); + const room = roomListView.getByRole("option", { name: "mark as unread" }); await room.hover(); await room.getByRole("button", { name: "More Options" }).click(); await page.getByRole("menuitem", { name: "mark as unread" }).click(); @@ -441,7 +445,7 @@ test.describe("Room list", () => { await page.getByText("Off").click(); await app.settings.closeDialog(); - const room = roomListView.getByRole("gridcell", { name: "silent" }); + const room = roomListView.getByRole("option", { name: "silent" }); await expect(room.getByTestId("notification-decoration")).toBeVisible(); await expect(room).toMatchScreenshot("room-list-item-silent.png"); }); diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unread-dm-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unread-dm-linux.png index 13577e0a1b..6968a4f134 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unread-dm-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-filter-sort.spec.ts/unread-dm-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png index 1e0add7b56..3795176be2 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-smallscreen-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-smallscreen-linux.png index 0e363b3747..f21e92a373 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-smallscreen-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list-panel.spec.ts/room-list-panel-smallscreen-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-activity-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-activity-linux.png index bc1aa9f4f1..ac0ee1ad6c 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-activity-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-activity-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-hover-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-hover-linux.png index 1315363e7e..a5403f2d01 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-hover-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-hover-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-hover-silent-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-hover-silent-linux.png index b400beac7c..963caeacda 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-hover-silent-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-hover-silent-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-invited-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-invited-linux.png index 44d90bac34..b024c0729c 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-invited-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-invited-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-low-priority-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-low-priority-linux.png index fe5ef29ecf..ce53a39e88 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-low-priority-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-low-priority-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-mark-as-unread-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-mark-as-unread-linux.png index 86032973a3..ee778c6871 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-mark-as-unread-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-mark-as-unread-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-mention-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-mention-linux.png index b134c90d3a..4b3c7fc1d1 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-mention-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-mention-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-message-preview-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-message-preview-linux.png index 8970c20cb5..7042a7078e 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-message-preview-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-message-preview-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-notification-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-notification-linux.png index 0c99720d01..4a92165c46 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-notification-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-notification-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png index 511d6b246b..4c29ce00e2 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-linux.png index 85b47b0f12..648222b325 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-selection-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-selection-linux.png index 8c1493c006..2b53b40113 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-selection-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-notification-options-selection-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-public-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-public-linux.png index 65683ec463..ad64e0c526 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-public-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-public-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-silent-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-silent-linux.png index cf8bb5a058..d9deb6cb1c 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-silent-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-silent-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-video-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-video-linux.png index e6d3d50e93..c3d6f5f952 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-video-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-video-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-linux.png index 8fab88fc7f..896af9eff7 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-linux.png differ diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-scrolled-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-scrolled-linux.png index 85a30b02f2..81f3af0cb6 100644 Binary files a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-scrolled-linux.png and b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-scrolled-linux.png differ diff --git a/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss b/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss index ac58a69bef..06ffe532d7 100644 --- a/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss +++ b/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss @@ -15,40 +15,44 @@ * |-------------------------------------------------------| */ .mx_RoomListItemView { - all: unset; + /* Remove button default style */ + background: unset; + border: none; + padding: 0; + text-align: unset; + cursor: pointer; + height: 48px; + width: 100%; - .mx_RoomListItemView_container { - padding-left: var(--cpd-space-3x); - font: var(--cpd-font-body-md-regular); + padding-left: var(--cpd-space-3x); + font: var(--cpd-font-body-md-regular); + + .mx_RoomListItemView_content { height: 100%; + flex: 1; + /* The border is only under the room name and the future hover menu */ + border-bottom: var(--cpd-border-width-0-5) solid var(--cpd-color-bg-subtle-secondary); + box-sizing: border-box; + min-width: 0; + padding-right: var(--cpd-space-5x); - .mx_RoomListItemView_content { - height: 100%; - flex: 1; - /* The border is only under the room name and the future hover menu */ - border-bottom: var(--cpd-border-width-0-5) solid var(--cpd-color-bg-subtle-secondary); - box-sizing: border-box; + .mx_RoomListItemView_text { min-width: 0; - padding-right: var(--cpd-space-5x); + } - .mx_RoomListItemView_text { - min-width: 0; - } + .mx_RoomListItemView_roomName { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } - .mx_RoomListItemView_roomName { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .mx_RoomListItemView_messagePreview { - font: var(--cpd-font-body-sm-regular); - color: var(--cpd-color-text-secondary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } + .mx_RoomListItemView_messagePreview { + font: var(--cpd-font-body-sm-regular); + color: var(--cpd-color-text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } } } @@ -57,7 +61,7 @@ background-color: var(--cpd-color-bg-action-secondary-hovered); } -.mx_RoomListItemView_menu_open .mx_RoomListItemView_container .mx_RoomListItemView_content { +.mx_RoomListItemView_menu_open .mx_RoomListItemView_content { /** * The figma uses 16px padding (--cpd-space-4x) but due to https://github.com/element-hq/compound-web/issues/331 * the icon size of the menu is 18px instead of 20px with a different internal padding diff --git a/src/Keyboard.ts b/src/Keyboard.ts index de7ab059c6..5a7e7b59f1 100644 --- a/src/Keyboard.ts +++ b/src/Keyboard.ts @@ -79,3 +79,12 @@ export function isOnlyCtrlOrCmdKeyEvent(ev: React.KeyboardEvent | KeyboardEvent) return ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey; } } + +/** + * Checks if the given keyboard event is a modified key event (i.e., if any modifier keys are active). + * @param ev The keyboard event to check + * @returns True if the event is a modified key event, false otherwise + */ +export function isModifiedKeyEvent(ev: React.KeyboardEvent | KeyboardEvent): boolean { + return ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey; +} diff --git a/src/components/utils/ListView.tsx b/src/components/utils/ListView.tsx index 5212d84b61..b11a3d638c 100644 --- a/src/components/utils/ListView.tsx +++ b/src/components/utils/ListView.tsx @@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details. import React, { useRef, type JSX, useCallback, useEffect, useState } from "react"; import { type VirtuosoHandle, type ListRange, Virtuoso, type VirtuosoProps } from "react-virtuoso"; +import { isModifiedKeyEvent, Key } from "../../Keyboard"; /** * Context object passed to each list item containing the currently focused key * and any additional context data from the parent component. @@ -34,6 +35,7 @@ export interface IListViewProps * @param index - The index of the item in the list * @param item - The data item to render * @param context - The context object containing the focused key and any additional data + * @param onFocus - A callback that is required to be called when the item component receives focus * @returns JSX element representing the rendered item */ getItemComponent: ( @@ -62,6 +64,14 @@ export interface IListViewProps * @return The key to use for focusing the item */ getItemKey: (item: Item) => string; + /** + * Callback function to handle key down events on the list container. + * ListView handles keyboard navigation for focus(up, down, home, end, pageUp, pageDown) + * and stops propagation otherwise the event bubbles and this callback is called for the use of the parent. + * @param e - The keyboard event + * @returns + */ + onKeyDown?: (e: React.KeyboardEvent) => void; } /** @@ -73,7 +83,7 @@ export interface IListViewProps */ export function ListView(props: IListViewProps): React.ReactElement { // Extract our custom props to avoid conflicts with Virtuoso props - const { items, getItemComponent, isItemFocusable, getItemKey, context, ...virtuosoProps } = props; + const { items, getItemComponent, isItemFocusable, getItemKey, context, onKeyDown, ...virtuosoProps } = props; /** Reference to the Virtuoso component for programmatic scrolling */ const virtuosoHandleRef = useRef(null); /** Reference to the DOM element containing the virtualized list */ @@ -125,7 +135,7 @@ export function ListView(props: IListViewProps(props: IListViewProps { - if (!e) return; // Guard against null/undefined events - + (e: React.KeyboardEvent) => { const currentIndex = tabIndexKey ? keyToIndexMap.get(tabIndexKey) : undefined; - let handled = false; - if (e.code === "ArrowUp" && currentIndex !== undefined) { - scrollToItem(currentIndex - 1, false); - handled = true; - } else if (e.code === "ArrowDown" && currentIndex !== undefined) { - scrollToItem(currentIndex + 1, true); - handled = true; - } else if (e.code === "Home") { - scrollToIndex(0); - handled = true; - } else if (e.code === "End") { - scrollToIndex(items.length - 1); - handled = true; - } else if (e.code === "PageDown" && visibleRange && currentIndex !== undefined) { - const numberDisplayed = visibleRange.endIndex - visibleRange.startIndex; - scrollToItem(Math.min(currentIndex + numberDisplayed, items.length - 1), true, `start`); - handled = true; - } else if (e.code === "PageUp" && visibleRange && currentIndex !== undefined) { - const numberDisplayed = visibleRange.endIndex - visibleRange.startIndex; - scrollToItem(Math.max(currentIndex - numberDisplayed, 0), false, `start`); - handled = true; + + // Guard against null/undefined events and modified keys which we don't want to handle here but do + // at the settings level shortcuts(E.g. Select next room, etc ) + if (e || !isModifiedKeyEvent(e)) { + if (e.code === Key.ARROW_UP && currentIndex !== undefined) { + scrollToItem(currentIndex - 1, false); + handled = true; + } else if (e.code === Key.ARROW_DOWN && currentIndex !== undefined) { + scrollToItem(currentIndex + 1, true); + handled = true; + } else if (e.code === Key.HOME) { + scrollToIndex(0); + handled = true; + } else if (e.code === Key.END) { + scrollToIndex(items.length - 1); + handled = true; + } else if (e.code === Key.PAGE_DOWN && visibleRange && currentIndex !== undefined) { + const numberDisplayed = visibleRange.endIndex - visibleRange.startIndex; + scrollToItem(Math.min(currentIndex + numberDisplayed, items.length - 1), true, `start`); + handled = true; + } else if (e.code === Key.PAGE_UP && visibleRange && currentIndex !== undefined) { + const numberDisplayed = visibleRange.endIndex - visibleRange.startIndex; + scrollToItem(Math.max(currentIndex - numberDisplayed, 0), false, `start`); + handled = true; + } } if (handled) { e.stopPropagation(); e.preventDefault(); + } else { + onKeyDown?.(e); } }, - [scrollToIndex, scrollToItem, tabIndexKey, keyToIndexMap, visibleRange, items], + [scrollToIndex, scrollToItem, tabIndexKey, keyToIndexMap, visibleRange, items, onKeyDown], ); /** @@ -251,8 +265,12 @@ export function ListView(props: IListViewProps { - setIsFocused(false); + const onBlur = useCallback((event: React.FocusEvent): void => { + // Only set isFocused to false if the focus is moving outside the list + // This prevents the list from losing focus when interacting with menus inside it + if (!event.currentTarget.contains(event.relatedTarget)) { + setIsFocused(false); + } }, []); const listContext: ListContext = { @@ -264,8 +282,8 @@ export function ListView(props: IListViewProps( SpaceStore.instance, @@ -88,7 +89,7 @@ export function useRoomListViewModel(): RoomListViewState { return { isLoadingRooms, - rooms, + roomsResult, canCreateRoom, createRoom, createChatRoom, diff --git a/src/components/viewmodels/roomlist/useFilteredRooms.tsx b/src/components/viewmodels/roomlist/useFilteredRooms.tsx index 736f75aa57..9a1730a735 100644 --- a/src/components/viewmodels/roomlist/useFilteredRooms.tsx +++ b/src/components/viewmodels/roomlist/useFilteredRooms.tsx @@ -5,12 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; -import type { Room } from "matrix-js-sdk/src/matrix"; import { FilterKey } from "../../../stores/room-list-v3/skip-list/filters"; import { _t, _td, type TranslationKey } from "../../../languageHandler"; -import RoomListStoreV3, { LISTS_LOADED_EVENT, LISTS_UPDATE_EVENT } from "../../../stores/room-list-v3/RoomListStoreV3"; +import RoomListStoreV3, { + LISTS_LOADED_EVENT, + LISTS_UPDATE_EVENT, + type RoomsResult, +} from "../../../stores/room-list-v3/RoomListStoreV3"; import { useEventEmitter } from "../../../hooks/useEventEmitter"; import SpaceStore from "../../../stores/spaces/SpaceStore"; import { UPDATE_SELECTED_SPACE } from "../../../stores/spaces"; @@ -35,7 +38,7 @@ export interface PrimaryFilter { interface FilteredRooms { primaryFilters: PrimaryFilter[]; isLoadingRooms: boolean; - rooms: Room[]; + roomsResult: RoomsResult; /** * The currently active primary filter. * If no primary filter is active, this will be undefined. @@ -63,12 +66,12 @@ export function useFilteredRooms(): FilteredRooms { */ const [primaryFilter, setPrimaryFilter] = useState(); - const [rooms, setRooms] = useState(() => RoomListStoreV3.instance.getSortedRoomsInActiveSpace()); + const [roomsResult, setRoomsResult] = useState(() => RoomListStoreV3.instance.getSortedRoomsInActiveSpace()); const [isLoadingRooms, setIsLoadingRooms] = useState(() => RoomListStoreV3.instance.isLoadingRooms); const updateRoomsFromStore = useCallback((filters: FilterKey[] = []): void => { const newRooms = RoomListStoreV3.instance.getSortedRoomsInActiveSpace(filters); - setRooms(newRooms); + setRoomsResult(newRooms); }, []); // Reset filters when active space changes @@ -77,9 +80,15 @@ export function useFilteredRooms(): FilteredRooms { const filterUndefined = (array: (FilterKey | undefined)[]): FilterKey[] => array.filter((f) => f !== undefined) as FilterKey[]; - const getAppliedFilters = (): FilterKey[] => { + const getAppliedFilters = useCallback((): FilterKey[] => { return filterUndefined([primaryFilter]); - }; + }, [primaryFilter]); + + useEffect(() => { + // Update the rooms state when the primary filter changes + const filters = getAppliedFilters(); + updateRoomsFromStore(filters); + }, [getAppliedFilters, updateRoomsFromStore]); useEventEmitter(RoomListStoreV3.instance, LISTS_UPDATE_EVENT, () => { const filters = getAppliedFilters(); @@ -122,6 +131,6 @@ export function useFilteredRooms(): FilteredRooms { isLoadingRooms, primaryFilters, activePrimaryFilter, - rooms, + roomsResult, }; } diff --git a/src/components/viewmodels/roomlist/useStickyRoomList.tsx b/src/components/viewmodels/roomlist/useStickyRoomList.tsx index 06feb58581..ad8a72b8b0 100644 --- a/src/components/viewmodels/roomlist/useStickyRoomList.tsx +++ b/src/components/viewmodels/roomlist/useStickyRoomList.tsx @@ -14,6 +14,7 @@ import { Action } from "../../../dispatcher/actions"; import type { Room } from "matrix-js-sdk/src/matrix"; import type { Optional } from "matrix-events-sdk"; import SpaceStore from "../../../stores/spaces/SpaceStore"; +import { type RoomsResult } from "../../../stores/room-list-v3/RoomListStoreV3"; function getIndexByRoomId(rooms: Room[], roomId: Optional): number | undefined { const index = rooms.findIndex((room) => room.roomId === roomId); @@ -67,11 +68,11 @@ function getRoomsWithStickyRoom( return { newIndex: oldIndex, newRooms }; } -interface StickyRoomListResult { +export interface StickyRoomListResult { /** - * List of rooms with sticky active room. + * The rooms result with the active sticky room applied */ - rooms: Room[]; + roomsResult: RoomsResult; /** * Index of the active room in the room list. */ @@ -85,10 +86,10 @@ interface StickyRoomListResult { * @param rooms list of rooms * @see {@link StickyRoomListResult} details what this hook returns.. */ -export function useStickyRoomList(rooms: Room[]): StickyRoomListResult { - const [listState, setListState] = useState<{ index: number | undefined; roomsWithStickyRoom: Room[] }>({ - index: undefined, - roomsWithStickyRoom: rooms, +export function useStickyRoomList(roomsResult: RoomsResult): StickyRoomListResult { + const [listState, setListState] = useState({ + activeIndex: getIndexByRoomId(roomsResult.rooms, SdkContextClass.instance.roomViewStore.getRoomId()), + roomsResult: roomsResult, }); const currentSpaceRef = useRef(SpaceStore.instance.activeSpace); @@ -97,13 +98,18 @@ export function useStickyRoomList(rooms: Room[]): StickyRoomListResult { (newRoomId: string | null, isRoomChange: boolean = false) => { setListState((current) => { const activeRoomId = newRoomId ?? SdkContextClass.instance.roomViewStore.getRoomId(); - const newActiveIndex = getIndexByRoomId(rooms, activeRoomId); - const oldIndex = current.index; - const { newIndex, newRooms } = getRoomsWithStickyRoom(rooms, oldIndex, newActiveIndex, isRoomChange); - return { index: newIndex, roomsWithStickyRoom: newRooms }; + const newActiveIndex = getIndexByRoomId(roomsResult.rooms, activeRoomId); + const oldIndex = current.activeIndex; + const { newIndex, newRooms } = getRoomsWithStickyRoom( + roomsResult.rooms, + oldIndex, + newActiveIndex, + isRoomChange, + ); + return { activeIndex: newIndex, roomsResult: { ...roomsResult, rooms: newRooms } }; }); }, - [rooms], + [roomsResult], ); // Re-calculate the index when the active room has changed. @@ -115,20 +121,19 @@ export function useStickyRoomList(rooms: Room[]): StickyRoomListResult { useEffect(() => { let newRoomId: string | null = null; let isRoomChange = false; - const newSpace = SpaceStore.instance.activeSpace; - if (currentSpaceRef.current !== newSpace) { + if (currentSpaceRef.current !== roomsResult.spaceId) { /* If the space has changed, we check if we can immediately set the active index to the last opened room in that space. Otherwise, we might see a flicker because of the delay between the space change event and active room change dispatch. */ - newRoomId = SpaceStore.instance.getLastSelectedRoomIdForSpace(newSpace); + newRoomId = SpaceStore.instance.getLastSelectedRoomIdForSpace(roomsResult.spaceId); isRoomChange = true; - currentSpaceRef.current = newSpace; + currentSpaceRef.current = roomsResult.spaceId; } updateRoomsAndIndex(newRoomId, isRoomChange); - }, [rooms, updateRoomsAndIndex]); + }, [roomsResult, updateRoomsAndIndex]); - return { activeIndex: listState.index, rooms: listState.roomsWithStickyRoom }; + return listState; } diff --git a/src/components/views/rooms/RoomListPanel/RoomList.tsx b/src/components/views/rooms/RoomListPanel/RoomList.tsx index f5f610f5ae..e35118c146 100644 --- a/src/components/views/rooms/RoomListPanel/RoomList.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomList.tsx @@ -5,13 +5,16 @@ * Please see LICENSE files in the repository root for full details. */ -import React, { useCallback, type JSX } from "react"; -import { AutoSizer, List, type ListRowProps } from "react-virtualized"; +import React, { useCallback, useRef, type JSX } from "react"; +import { type Room } from "matrix-js-sdk/src/matrix"; +import { type ScrollIntoViewLocation } from "react-virtuoso"; +import { isEqual } from "lodash"; import { type RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel"; import { _t } from "../../../../languageHandler"; import { RoomListItemView } from "./RoomListItemView"; -import { RovingTabIndexProvider } from "../../../../accessibility/RovingTabIndex"; +import { type ListContext, ListView } from "../../../utils/ListView"; +import { type FilterKey } from "../../../../stores/room-list-v3/skip-list/filters"; import { getKeyBindingsManager } from "../../../../KeyBindingsManager"; import { KeyBindingAction } from "../../../../accessibility/KeyboardShortcuts"; import { Landmark, LandmarkNavigation } from "../../../../accessibility/LandmarkNavigation"; @@ -26,55 +29,93 @@ interface RoomListProps { /** * A virtualized list of rooms. */ -export function RoomList({ vm: { rooms, activeIndex } }: RoomListProps): JSX.Element { - const roomRendererMemoized = useCallback( - ({ key, index, style }: ListRowProps) => ( - - ), - [rooms, activeIndex], +export function RoomList({ vm: { roomsResult, activeIndex } }: RoomListProps): JSX.Element { + const lastSpaceId = useRef(undefined); + const lastFilterKeys = useRef(undefined); + const roomCount = roomsResult.rooms.length; + const getItemComponent = useCallback( + ( + index: number, + item: Room, + context: ListContext<{ + spaceId: string; + filterKeys: FilterKey[] | undefined; + }>, + onFocus: (e: React.FocusEvent) => void, + ): JSX.Element => { + const itemKey = item.roomId; + const isRovingItem = itemKey === context.tabIndexKey; + const isFocused = isRovingItem && context.focused; + const isSelected = activeIndex === index; + return ( + + ); + }, + [activeIndex, roomCount], ); - // The first div is needed to make the virtualized list take all the remaining space and scroll correctly + const getItemKey = useCallback((item: Room): string => { + return item.roomId; + }, []); + + const scrollIntoViewOnChange = useCallback( + (params: { + context: ListContext<{ spaceId: string; filterKeys: FilterKey[] | undefined }>; + }): ScrollIntoViewLocation | null | undefined | false | void => { + const { spaceId, filterKeys } = params.context.context; + const shouldScrollIndexIntoView = + lastSpaceId.current !== spaceId || !isEqual(lastFilterKeys.current, filterKeys); + lastFilterKeys.current = filterKeys; + lastSpaceId.current = spaceId; + + if (shouldScrollIndexIntoView) { + return { + align: `start`, + index: activeIndex || 0, + behavior: "auto", + }; + } + return false; + }, + [activeIndex], + ); + + const keyDownCallback = useCallback((ev: React.KeyboardEvent) => { + const navAction = getKeyBindingsManager().getNavigationAction(ev); + if (navAction === KeyBindingAction.NextLandmark || navAction === KeyBindingAction.PreviousLandmark) { + LandmarkNavigation.findAndFocusNextLandmark( + Landmark.ROOM_LIST, + navAction === KeyBindingAction.PreviousLandmark, + ); + ev.stopPropagation(); + ev.preventDefault(); + return; + } + }, []); + return ( - - {({ onKeyDownHandler }) => ( -
{ - const navAction = getKeyBindingsManager().getNavigationAction(ev); - if ( - navAction === KeyBindingAction.NextLandmark || - navAction === KeyBindingAction.PreviousLandmark - ) { - LandmarkNavigation.findAndFocusNextLandmark( - Landmark.ROOM_LIST, - navAction === KeyBindingAction.PreviousLandmark, - ); - ev.stopPropagation(); - ev.preventDefault(); - return; - } - onKeyDownHandler(ev); - }} - > - - {({ height, width }) => ( - - )} - -
- )} -
+ true} + onKeyDown={keyDownCallback} + /> ); } diff --git a/src/components/views/rooms/RoomListPanel/RoomListItemMenuView.tsx b/src/components/views/rooms/RoomListPanel/RoomListItemMenuView.tsx index 66107074b7..60643a4441 100644 --- a/src/components/views/rooms/RoomListPanel/RoomListItemMenuView.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomListItemMenuView.tsx @@ -97,7 +97,10 @@ interface MoreOptionContentProps { export function MoreOptionContent({ vm }: MoreOptionContentProps): JSX.Element { return ( - <> +
e.stopPropagation()} + > {vm.canMarkAsRead && ( evt.stopPropagation()} hideChevron={true} /> - +
); } @@ -196,54 +199,59 @@ function NotificationMenu({ vm, setMenuOpen }: NotificationMenuProps): JSX.Eleme const checkComponent = ; return ( - { - setOpen(isOpen); - setMenuOpen(isOpen); - }} - title={_t("room_list|notification_options")} - showTitle={false} - align="start" - trigger={} +
e.stopPropagation()} > - vm.setRoomNotifState(RoomNotifState.AllMessages)} - onClick={(evt) => evt.stopPropagation()} + { + setOpen(isOpen); + setMenuOpen(isOpen); + }} + title={_t("room_list|notification_options")} + showTitle={false} + align="start" + trigger={} > - {vm.isNotificationAllMessage && checkComponent} - - vm.setRoomNotifState(RoomNotifState.AllMessagesLoud)} - onClick={(evt) => evt.stopPropagation()} - > - {vm.isNotificationAllMessageLoud && checkComponent} - - vm.setRoomNotifState(RoomNotifState.MentionsOnly)} - onClick={(evt) => evt.stopPropagation()} - > - {vm.isNotificationMentionOnly && checkComponent} - - vm.setRoomNotifState(RoomNotifState.Mute)} - onClick={(evt) => evt.stopPropagation()} - > - {vm.isNotificationMute && checkComponent} - - + vm.setRoomNotifState(RoomNotifState.AllMessages)} + onClick={(evt) => evt.stopPropagation()} + > + {vm.isNotificationAllMessage && checkComponent} + + vm.setRoomNotifState(RoomNotifState.AllMessagesLoud)} + onClick={(evt) => evt.stopPropagation()} + > + {vm.isNotificationAllMessageLoud && checkComponent} + + vm.setRoomNotifState(RoomNotifState.MentionsOnly)} + onClick={(evt) => evt.stopPropagation()} + > + {vm.isNotificationMentionOnly && checkComponent} + + vm.setRoomNotifState(RoomNotifState.Mute)} + onClick={(evt) => evt.stopPropagation()} + > + {vm.isNotificationMute && checkComponent} + +
+ ); } diff --git a/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx b/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx index a124c33851..81e63e6f6f 100644 --- a/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -import React, { type JSX, memo, useCallback, useRef, useState } from "react"; +import React, { type JSX, memo, useCallback, useEffect, useRef, useState } from "react"; import { type Room } from "matrix-js-sdk/src/matrix"; import classNames from "classnames"; @@ -14,7 +14,6 @@ import { Flex } from "../../../../shared-components/utils/Flex"; import { RoomListItemMenuView } from "./RoomListItemMenuView"; import { NotificationDecoration } from "../NotificationDecoration"; import { RoomAvatarView } from "../../avatars/RoomAvatarView"; -import { useRovingTabIndex } from "../../../../accessibility/RovingTabIndex"; import { RoomListItemContextMenuView } from "./RoomListItemContextMenuView"; interface RoomListItemViewProps extends React.HTMLAttributes { @@ -26,6 +25,22 @@ interface RoomListItemViewProps extends React.HTMLAttributes * Whether the room is selected */ isSelected: boolean; + /** + * Whether the room is focused + */ + isFocused: boolean; + /** + * A callback that indicates the item has received focus + */ + onFocus: (e: React.FocusEvent) => void; + /** + * The index of the room in the list + */ + roomIndex: number; + /** + * The total number of rooms in the list + */ + roomCount: number; } /** @@ -34,18 +49,19 @@ interface RoomListItemViewProps extends React.HTMLAttributes export const RoomListItemView = memo(function RoomListItemView({ room, isSelected, + isFocused, + onFocus, + roomIndex: index, + roomCount: count, ...props }: RoomListItemViewProps): JSX.Element { - const buttonRef = useRef(null); - const [onFocus, isActive, ref] = useRovingTabIndex(buttonRef); - + const ref = useRef(null); const vm = useRoomListItemViewModel(room); - - const [isHover, setIsHoverWithDelay] = useIsHover(); + const [isHover, setHover] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false); // The compound menu in RoomListItemMenuView needs to be rendered when the hover menu is shown // Using display: none; and then display:flex when hovered in CSS causes the menu to be misaligned - const showHoverDecoration = isMenuOpen || isHover; + const showHoverDecoration = isMenuOpen || isFocused || isHover; const showHoverMenu = showHoverDecoration && vm.showHoverMenu; const closeMenu = useCallback(() => { @@ -54,8 +70,15 @@ export const RoomListItemView = memo(function RoomListItemView({ setTimeout(() => setIsMenuOpen(false), 10); }, []); + useEffect(() => { + if (isFocused) { + ref.current?.focus({ preventScroll: true, focusVisible: true }); + } + }, [isFocused]); + const content = ( - + ); if (!vm.showContextMenu) return content; @@ -140,33 +159,3 @@ export const RoomListItemView = memo(function RoomListItemView({ ); }); - -/** - * Custom hook to manage the hover state of the room list item - * If the timeout is set, it will set the hover state after the timeout - * If the timeout is not set, it will set the hover state immediately - * When the set method is called, it will clear any existing timeout - * - * @returns {boolean} isHover - The hover state - */ -function useIsHover(): [boolean, (value: boolean, timeout?: number) => void] { - const [isHover, setIsHover] = useState(false); - // Store the timeout ID - const timeoutRef = useRef(undefined); - - const setIsHoverWithDelay = useCallback((value: boolean, timeout?: number): void => { - // Clear the timeout if it exists - clearTimeout(timeoutRef.current); - - // No delay, set the value immediately - if (timeout === undefined) { - setIsHover(value); - return; - } - - // Set a timeout to set the value after the delay - timeoutRef.current = setTimeout(() => setIsHover(value), timeout); - }, []); - - return [isHover, setIsHoverWithDelay]; -} diff --git a/src/components/views/rooms/RoomListPanel/RoomListView.tsx b/src/components/views/rooms/RoomListPanel/RoomListView.tsx index f6d99625aa..a99312be53 100644 --- a/src/components/views/rooms/RoomListPanel/RoomListView.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomListView.tsx @@ -17,7 +17,7 @@ import { RoomListPrimaryFilters } from "./RoomListPrimaryFilters"; */ export function RoomListView(): JSX.Element { const vm = useRoomListViewModel(); - const isRoomListEmpty = vm.rooms.length === 0; + const isRoomListEmpty = vm.roomsResult.rooms.length === 0; let listBody; if (vm.isLoadingRooms) { listBody =
; diff --git a/src/stores/room-list-v3/RoomListStoreV3.ts b/src/stores/room-list-v3/RoomListStoreV3.ts index 92de63285e..7933b1fc85 100644 --- a/src/stores/room-list-v3/RoomListStoreV3.ts +++ b/src/stores/room-list-v3/RoomListStoreV3.ts @@ -22,7 +22,7 @@ import { AlphabeticSorter } from "./skip-list/sorters/AlphabeticSorter"; import { readReceiptChangeIsFor } from "../../utils/read-receipts"; import { EffectiveMembership, getEffectiveMembership, getEffectiveMembershipTag } from "../../utils/membership"; import SpaceStore from "../spaces/SpaceStore"; -import { UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../spaces"; +import { type SpaceKey, UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../spaces"; import { FavouriteFilter } from "./skip-list/filters/FavouriteFilter"; import { UnreadFilter } from "./skip-list/filters/UnreadFilter"; import { PeopleFilter } from "./skip-list/filters/PeopleFilter"; @@ -56,6 +56,16 @@ export enum RoomListStoreV3Event { ListsLoaded = "lists_loaded", } +// The result object for returning rooms from the store +export type RoomsResult = { + // The ID of the active space queried + spaceId: SpaceKey; + // The filter queried + filterKeys?: FilterKey[]; + // The resulting list of rooms + rooms: Room[]; +}; + export const LISTS_UPDATE_EVENT = RoomListStoreV3Event.ListsUpdate; export const LISTS_LOADED_EVENT = RoomListStoreV3Event.ListsLoaded; /** @@ -107,9 +117,15 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient { * @param filterKeys Optional array of filters that the rooms must match against. */ - public getSortedRoomsInActiveSpace(filterKeys?: FilterKey[]): Room[] { - if (this.roomSkipList?.initialized) return Array.from(this.roomSkipList.getRoomsInActiveSpace(filterKeys)); - else return []; + public getSortedRoomsInActiveSpace(filterKeys?: FilterKey[]): RoomsResult { + const spaceId = SpaceStore.instance.activeSpace; + if (this.roomSkipList?.initialized) + return { + spaceId: spaceId, + filterKeys, + rooms: Array.from(this.roomSkipList.getRoomsInActiveSpace(filterKeys)), + }; + else return { spaceId: spaceId, filterKeys, rooms: [] }; } /** diff --git a/test/unit-tests/components/viewmodels/roomlist/RoomListHeaderViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/RoomListHeaderViewModel-test.tsx index f9fe45202a..eb92208fc9 100644 --- a/test/unit-tests/components/viewmodels/roomlist/RoomListHeaderViewModel-test.tsx +++ b/test/unit-tests/components/viewmodels/roomlist/RoomListHeaderViewModel-test.tsx @@ -209,7 +209,7 @@ describe("useRoomListHeaderViewModel", () => { const rooms = range(10).map((i) => mkStubRoom(`foo${i}:matrix.org`, `Foo ${i}`, undefined)); const fn = jest .spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace") - .mockImplementation(() => [...rooms]); + .mockImplementation(() => ({ spaceId: "home", rooms: [...rooms] })); return { rooms, fn }; } diff --git a/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx index 9607cf9218..3cc411b399 100644 --- a/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx +++ b/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx @@ -30,7 +30,7 @@ describe("RoomListViewModel", () => { const rooms = range(10).map((i) => mkStubRoom(`foo${i}:matrix.org`, `Foo ${i}`, undefined)); const fn = jest .spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace") - .mockImplementation(() => [...rooms]); + .mockImplementation(() => ({ spaceId: "home", rooms: [...rooms] })); return { rooms, fn }; } @@ -42,9 +42,9 @@ describe("RoomListViewModel", () => { const { rooms } = mockAndCreateRooms(); const { result: vm } = renderHook(() => useRoomListViewModel()); - expect(vm.current.rooms).toHaveLength(10); + expect(vm.current.roomsResult.rooms).toHaveLength(10); for (const room of rooms) { - expect(vm.current.rooms).toContain(room); + expect(vm.current.roomsResult.rooms).toContain(room); } }); @@ -57,7 +57,7 @@ describe("RoomListViewModel", () => { await act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT)); await waitFor(() => { - expect(vm.current.rooms).toContain(newRoom); + expect(vm.current.roomsResult.rooms).toContain(newRoom); }); }); @@ -176,7 +176,7 @@ describe("RoomListViewModel", () => { describe("Sticky room and active index", () => { function expectActiveRoom(vm: ReturnType, i: number, roomId: string) { expect(vm.activeIndex).toEqual(i); - expect(vm.rooms[i].roomId).toEqual(roomId); + expect(vm.roomsResult.rooms[i].roomId).toEqual(roomId); } it("active index is calculated with the last opened room in a space", () => { @@ -187,9 +187,9 @@ describe("RoomListViewModel", () => { const rooms = range(10).map((i) => mkStubRoom(`foo${i}:matrix.org`, `Foo ${i}`, undefined)); // Let's say all the rooms are in space1 - const roomsInSpace1 = [...rooms]; + const roomsInSpace1 = { spaceId: currentSpace, rooms: [...rooms] }; // Let's say all rooms with even index are in space 2 - const roomsInSpace2 = [...rooms].filter((_, i) => i % 2 === 0); + const roomsInSpace2 = { spaceId: "!space2:matrix.org", rooms: [...rooms].filter((_, i) => i % 2 === 0) }; jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockImplementation(() => currentSpace === "!space1:matrix.org" ? roomsInSpace1 : roomsInSpace2, ); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/EmptyRoomList-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/EmptyRoomList-test.tsx index cac0f62cba..92466f685c 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/EmptyRoomList-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomListPanel/EmptyRoomList-test.tsx @@ -19,7 +19,7 @@ describe("", () => { beforeEach(() => { vm = { isLoadingRooms: false, - rooms: [], + roomsResult: { spaceId: "home", rooms: [] }, primaryFilters: [], createRoom: jest.fn(), createChatRoom: jest.fn(), diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomList-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomList-test.tsx index 983df2168e..c38292e89b 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/RoomList-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomListPanel/RoomList-test.tsx @@ -9,28 +9,25 @@ import React from "react"; import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import { render } from "jest-matrix-react"; import { fireEvent } from "@testing-library/dom"; +import { VirtuosoMockContext } from "react-virtuoso"; -import { mkRoom, stubClient, withClientContextRenderOptions } from "../../../../../test-utils"; import { type RoomListViewState } from "../../../../../../src/components/viewmodels/roomlist/RoomListViewModel"; import { RoomList } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomList"; import DMRoomMap from "../../../../../../src/utils/DMRoomMap"; +import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext"; import { Landmark, LandmarkNavigation } from "../../../../../../src/accessibility/LandmarkNavigation"; +import { mkRoom, stubClient } from "../../../../../test-utils"; describe("", () => { let matrixClient: MatrixClient; let vm: RoomListViewState; beforeEach(() => { - // Needed to render the virtualized list in rtl tests - // https://github.com/bvaughn/react-virtualized/issues/493#issuecomment-640084107 - jest.spyOn(HTMLElement.prototype, "offsetHeight", "get").mockReturnValue(1500); - jest.spyOn(HTMLElement.prototype, "offsetWidth", "get").mockReturnValue(1500); - matrixClient = stubClient(); const rooms = Array.from({ length: 10 }, (_, i) => mkRoom(matrixClient, `room${i}`)); vm = { isLoadingRooms: false, - rooms, + roomsResult: { spaceId: "home", rooms }, primaryFilters: [], createRoom: jest.fn(), createChatRoom: jest.fn(), @@ -44,7 +41,18 @@ describe("", () => { }); it("should render a room list", () => { - const { asFragment } = render(, withClientContextRenderOptions(matrixClient)); + const { asFragment } = render(, { + wrapper: ({ children }) => ( + + + <>{children} + + + ), + }); + // At the moment the context prop on Virtuoso gets rendered in the dom as "[object Object]". + // This is a general issue with the react-virtuoso library. + // TODO: Update the snapshot when the following issue is resolved: https://github.com/petyosi/react-virtuoso/issues/1281 expect(asFragment()).toMatchSnapshot(); }); @@ -53,7 +61,15 @@ describe("", () => { { shortcut: { key: "F6", ctrlKey: true }, isPreviousLandmark: false, label: "NextLandmark" }, ])("should navigate to the landmark on NextLandmark.$label action", ({ shortcut, isPreviousLandmark }) => { const spyFindLandmark = jest.spyOn(LandmarkNavigation, "findAndFocusNextLandmark").mockReturnValue(); - const { getByTestId } = render(, withClientContextRenderOptions(matrixClient)); + const { getByTestId } = render(, { + wrapper: ({ children }) => ( + + + <>{children} + + + ), + }); const roomList = getByTestId("room-list"); fireEvent.keyDown(roomList, shortcut); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx index a97e5217ef..fab1dc58fc 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx @@ -28,6 +28,20 @@ describe("", () => { let defaultValue: RoomListItemViewState; let matrixClient: MatrixClient; let room: Room; + + const renderRoomListItem = (props: Partial> = {}) => { + const defaultProps = { + room, + isSelected: false, + isFocused: false, + onFocus: jest.fn(), + roomIndex: 0, + roomCount: 1, + }; + + return render(, withClientContextRenderOptions(matrixClient)); + }; + beforeEach(() => { matrixClient = stubClient(); room = mkRoom(matrixClient, "room1"); @@ -60,7 +74,10 @@ describe("", () => { test("should render a room item", () => { const onClick = jest.fn(); - const { asFragment } = render(); + const { asFragment } = renderRoomListItem({ + onClick, + roomCount: 0, + }); expect(asFragment()).toMatchSnapshot(); }); @@ -68,15 +85,17 @@ describe("", () => { defaultValue.messagePreview = "The message looks like this"; const onClick = jest.fn(); - const { asFragment } = render(); + const { asFragment } = renderRoomListItem({ + onClick, + }); expect(asFragment()).toMatchSnapshot(); }); test("should call openRoom when clicked", async () => { const user = userEvent.setup(); - render(); + renderRoomListItem(); - await user.click(screen.getByRole("button", { name: `Open room ${room.name}` })); + await user.click(screen.getByRole("option", { name: `Open room ${room.name}` })); expect(defaultValue.openRoom).toHaveBeenCalled(); }); @@ -84,8 +103,9 @@ describe("", () => { mocked(useRoomListItemViewModel).mockReturnValue({ ...defaultValue, showHoverMenu: true }); const user = userEvent.setup(); - render(, withClientContextRenderOptions(matrixClient)); - const listItem = screen.getByRole("button", { name: `Open room ${room.name}` }); + renderRoomListItem(); + + const listItem = screen.getByRole("option", { name: `Open room ${room.name}` }); expect(screen.queryByRole("button", { name: "More Options" })).toBeNull(); await user.hover(listItem); @@ -93,19 +113,33 @@ describe("", () => { }); test("should hover decoration if focused", async () => { - const user = userEvent.setup(); - render(, withClientContextRenderOptions(matrixClient)); - const listItem = screen.getByRole("button", { name: `Open room ${room.name}` }); - await user.click(listItem); - expect(listItem).toHaveClass("mx_RoomListItemView_hover"); + const { rerender } = renderRoomListItem({ + isFocused: true, + }); - await user.tab(); - await waitFor(() => expect(listItem).not.toHaveClass("mx_RoomListItemView_hover")); + const listItem = screen.getByRole("option", { name: `Open room ${room.name}` }); + expect(listItem).toHaveClass("flex mx_RoomListItemView mx_RoomListItemView_hover"); + + rerender( + , + ); + + await waitFor(() => expect(listItem).not.toHaveClass("flex mx_RoomListItemView mx_RoomListItemView_hover")); }); test("should be selected if isSelected=true", async () => { - const { asFragment } = render(); - expect(screen.queryByRole("button", { name: `Open room ${room.name}` })).toHaveAttribute( + const { asFragment } = renderRoomListItem({ + isSelected: true, + }); + + expect(screen.queryByRole("option", { name: `Open room ${room.name}` })).toHaveAttribute( "aria-selected", "true", ); @@ -118,7 +152,8 @@ describe("", () => { showNotificationDecoration: true, }); - const { asFragment } = render(); + const { asFragment } = renderRoomListItem(); + expect(screen.getByTestId("notification-decoration")).toBeInTheDocument(); expect(asFragment()).toMatchSnapshot(); }); @@ -131,8 +166,9 @@ describe("", () => { showNotificationDecoration: true, }); - render(); - const listItem = screen.getByRole("button", { name: `Open room ${room.name}` }); + renderRoomListItem(); + + const listItem = screen.getByRole("option", { name: `Open room ${room.name}` }); await user.hover(listItem); expect(screen.queryByRole("notification-decoration")).toBeNull(); @@ -146,8 +182,9 @@ describe("", () => { showContextMenu: true, }); - render(, withClientContextRenderOptions(matrixClient)); - const button = screen.getByRole("button", { name: `Open room ${room.name}` }); + renderRoomListItem(); + + const button = screen.getByRole("option", { name: `Open room ${room.name}` }); await user.pointer([{ target: button }, { keys: "[MouseRight]", target: button }]); await waitFor(() => expect(screen.getByRole("menu")).toBeInTheDocument()); // Menu should close diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListView-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListView-test.tsx index 4ebe6fc4c7..0081c6f350 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListView-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListView-test.tsx @@ -23,7 +23,7 @@ jest.mock("../../../../../../src/components/viewmodels/roomlist/RoomListViewMode describe("", () => { const defaultValue: RoomListViewState = { isLoadingRooms: false, - rooms: [], + roomsResult: { spaceId: "home", rooms: [] }, primaryFilters: [], createRoom: jest.fn(), createChatRoom: jest.fn(), @@ -56,10 +56,10 @@ describe("", () => { it("should render a room list", () => { mocked(useRoomListViewModel).mockReturnValue({ ...defaultValue, - rooms: [mkRoom(matrixClient, "testing room")], + roomsResult: { spaceId: "home", rooms: [mkRoom(matrixClient, "testing room")] }, }); render(); - expect(screen.getByRole("grid", { name: "Room list" })).toBeInTheDocument(); + expect(screen.getByRole("listbox", { name: "Room list" })).toBeInTheDocument(); }); }); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomList-test.tsx.snap b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomList-test.tsx.snap index 005694994e..fcbaa9110e 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomList-test.tsx.snap +++ b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomList-test.tsx.snap @@ -3,531 +3,556 @@ exports[` should render a room list 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
`; diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemMenuView-test.tsx.snap b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemMenuView-test.tsx.snap index 7f3a3739c1..4fae31120b 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemMenuView-test.tsx.snap +++ b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemMenuView-test.tsx.snap @@ -38,41 +38,43 @@ exports[` should render the more options menu 1`] = `
-
- + + + + +
+ +
`; @@ -115,41 +117,43 @@ exports[` should render the notification options menu 1` - + + + + + + + `; diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemView-test.tsx.snap b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemView-test.tsx.snap index 94c4fe4fed..180bfd5259 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemView-test.tsx.snap +++ b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListItemView-test.tsx.snap @@ -4,47 +4,46 @@ exports[` should be selected if isSelected=true 1`] = ` @@ -120,47 +118,46 @@ exports[` should render a room item 1`] = `