Implement collapsible panels for the new room list (#32742)
* Add `react-resizable-panels` library * Implement a custom SeparatorView * Add a `LeftResizablePanelView` * Add a custom `GroupView` * Export everything from shared-components * Make it possible to track width/collapse state through settings * Add a view model to drive the views * Render views without disrupting the old room list * Fix lint error * Disable user interaction on collapsed panel * Prevent widgets fron hijacking pointer events * Expand to full width on separator click * Separator should be shown when focused via keyboard * Update tests * Use data-attribute for hover * Write stories for SeperatorView * Write vite tests for SeparatorView * Write tests for LeftResizablePanelView * More tests * Fix lint errors * Fix flakey border on the roomlst * Fix storybook axe violation * Update snapshots * Fix playwright tests * Fix sonarcloud issues * Use translated string * Add better js-doc comments * Rename `ResizerSnapshot` to `ResizerViewSnapshot` * Externalize react-resizable-panels * Link figma designs to stories * Write playwright tests * Update screenshots * Fix lint errors * Update more screenshots * Update more screenshots * Fix flaky toast test * Update apps/web/playwright/e2e/crypto/toasts.spec.ts Co-authored-by: Andy Balaam <andy.balaam@matrix.org> * Fix indentation --------- Co-authored-by: Andy Balaam <andy.balaam@matrix.org>
@@ -49,6 +49,11 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
).toBeVisible();
|
||||
};
|
||||
|
||||
const scrollToBottomOfTimeline = async (page: Page) => {
|
||||
await page.locator(".mx_RoomView_MessageList").click();
|
||||
await page.mouse.wheel(0, 100);
|
||||
};
|
||||
|
||||
/**
|
||||
* Take snapshots of mx_EventTile_last on each layout, outputting log for reference/debugging.
|
||||
* @param detail The snapshot name. Used for outputting logs too.
|
||||
@@ -113,10 +118,12 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
`,
|
||||
mask: [page.getByTestId("audio-player-seek")],
|
||||
clip: undefined,
|
||||
hideJumpToBottomButton: true,
|
||||
};
|
||||
|
||||
// Take a snapshot of mx_EventTile_last on IRC layout
|
||||
screenshotOptions.clip = await page.locator(".mx_EventTile_last").boundingBox();
|
||||
await scrollToBottomOfTimeline(page);
|
||||
await expect(page).toMatchScreenshot(`${detail.replaceAll(" ", "-")}-irc-layout.png`, screenshotOptions);
|
||||
|
||||
// Take a snapshot on modern/group layout
|
||||
@@ -125,6 +132,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
await groupTile.locator(".mx_MessageTimestamp").click();
|
||||
await checkPlayerVisibility(groupTile);
|
||||
screenshotOptions.clip = await page.locator(".mx_EventTile_last").boundingBox();
|
||||
await scrollToBottomOfTimeline(page);
|
||||
await expect(page).toMatchScreenshot(`${detail.replaceAll(" ", "-")}-group-layout.png`, screenshotOptions);
|
||||
|
||||
// Take a snapshot on bubble layout
|
||||
@@ -133,6 +141,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
await bubbleTile.locator(".mx_MessageTimestamp").click();
|
||||
await checkPlayerVisibility(bubbleTile);
|
||||
screenshotOptions.clip = await page.locator(".mx_EventTile_last").boundingBox();
|
||||
await scrollToBottomOfTimeline(page);
|
||||
await expect(page).toMatchScreenshot(`${detail.replaceAll(" ", "-")}-bubble-layout.png`, screenshotOptions);
|
||||
};
|
||||
|
||||
|
||||
@@ -11,6 +11,16 @@ import { test, expect } from "../../element-web-test";
|
||||
import { createBot, deleteCachedSecrets, disableKeyBackup, logIntoElement, logIntoElementAndVerify } from "./utils";
|
||||
import { type Bot } from "../../pages/bot";
|
||||
|
||||
// Mask the background of the screenshot to avoid failing the test just because some
|
||||
// other component has changed its rendering.
|
||||
const screenshotOptions = {
|
||||
css: `
|
||||
.mx_ToastContainer {
|
||||
background-color: magenta !important;
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
test.describe("Key storage out of sync toast", () => {
|
||||
let recoveryKey: GeneratedSecretStorageKey;
|
||||
|
||||
@@ -37,15 +47,10 @@ test.describe("Key storage out of sync toast", () => {
|
||||
// playwright only evaluates the 'first()' call initially, not subsequent times it checks, so
|
||||
// it would always be checking the same toast, even if another one is now the first.
|
||||
await expect(page.getByRole("alert")).toHaveCount(2);
|
||||
// Mask the background of the screenshot to avoid failing the test just because some
|
||||
// other component have changed its rendering.
|
||||
await expect(page.getByRole("alert").first()).toMatchScreenshot("key-storage-out-of-sync-toast.png", {
|
||||
css: `
|
||||
.mx_ToastContainer {
|
||||
background-color: magenta !important;
|
||||
}
|
||||
`,
|
||||
});
|
||||
await expect(page.getByRole("alert").first()).toMatchScreenshot(
|
||||
"key-storage-out-of-sync-toast.png",
|
||||
screenshotOptions,
|
||||
);
|
||||
|
||||
await page.getByRole("button", { name: "Enter recovery key" }).click();
|
||||
|
||||
@@ -209,7 +214,10 @@ test.describe("Verify this device toast", () => {
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Verify this device" })).toBeVisible();
|
||||
|
||||
await expect(page.locator(".mx_ToastContainer")).toMatchScreenshot("verify-this-device.png");
|
||||
await expect(page.locator(".mx_ToastContainer")).toMatchScreenshot(
|
||||
"verify-this-device.png",
|
||||
screenshotOptions,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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 { test, expect } from "../../../element-web-test";
|
||||
import type { Locator, Page } from "playwright-core";
|
||||
|
||||
test.describe("Collapsible Room list", () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page, app, user }) => {
|
||||
await app.closeNotificationToast();
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await app.client.createRoom({ name: `room${i}` });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Resize the panel and return the bounding box
|
||||
* @param pixels The number of pixels by which to resize the panel
|
||||
*/
|
||||
async function resize(page: Page, pixels: number): ReturnType<Locator["boundingBox"]> {
|
||||
const leftPanelLocator = page.getByTestId("left-panel");
|
||||
const boundingBox = await leftPanelLocator.boundingBox();
|
||||
|
||||
// Move mouse 2px to the right of the left-panel, this should be region that the user drags to resize the panel.
|
||||
const mouseX = boundingBox.x + boundingBox.width + 2;
|
||||
const mouseY = boundingBox.y + boundingBox.height / 2;
|
||||
|
||||
await page.mouse.move(mouseX, mouseY);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(mouseX + pixels, mouseY);
|
||||
|
||||
return boundingBox;
|
||||
}
|
||||
|
||||
test("should be possible to expand/contract the room list", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
await expect(page).toMatchScreenshot("room-list-collapse-default.png");
|
||||
const leftPanelLocator = page.getByTestId("left-panel");
|
||||
|
||||
// Contract the panel
|
||||
let previousBoundingBox = await resize(page, -50);
|
||||
let currentBoundingBox = await leftPanelLocator.boundingBox();
|
||||
expect(currentBoundingBox.width).toBeCloseTo(previousBoundingBox.width - 50, 0);
|
||||
|
||||
// Expand the panel
|
||||
previousBoundingBox = await resize(page, 30);
|
||||
currentBoundingBox = await leftPanelLocator.boundingBox();
|
||||
expect(currentBoundingBox.width).toBeCloseTo(previousBoundingBox.width + 30, 0);
|
||||
});
|
||||
|
||||
test(
|
||||
"should be possible to fully collapse and expand the left panel",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user }) => {
|
||||
const leftPanelLocator = page.getByTestId("left-panel");
|
||||
|
||||
// Collapse the panel
|
||||
await resize(page, -300);
|
||||
let currentBoundingBox = await leftPanelLocator.boundingBox();
|
||||
expect(currentBoundingBox.width).toStrictEqual(0);
|
||||
|
||||
// Expect te separator to be shown
|
||||
const separator = page.getByRole("separator", { name: "Click or drag to expand" });
|
||||
await expect(separator).toBeInViewport();
|
||||
await expect(page).toMatchScreenshot("room-list-collapse-fully-collapsed.png");
|
||||
|
||||
// Should be possible to expand by clicking on the separator
|
||||
await separator.click();
|
||||
currentBoundingBox = await leftPanelLocator.boundingBox();
|
||||
expect(currentBoundingBox.width).toBeGreaterThan(365);
|
||||
|
||||
// Collapse the panel again
|
||||
await resize(page, -300);
|
||||
|
||||
// Check that the panel can be expanded by dragging the separator
|
||||
const separatorBoundingBox = await separator.boundingBox();
|
||||
const mouseX = separatorBoundingBox.x + separatorBoundingBox.width / 2;
|
||||
const mouseY = separatorBoundingBox.y + separatorBoundingBox.height / 2;
|
||||
await page.mouse.move(mouseX, mouseY);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(mouseX + 400, mouseY);
|
||||
expect(currentBoundingBox.width).toBeGreaterThan(365);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -73,7 +73,15 @@ export const test = base.extend<TestFixtures>({
|
||||
|
||||
axe: async ({ axe }, use) => {
|
||||
// Exclude floating UI for now
|
||||
await use(axe.exclude("[data-floating-ui-portal]"));
|
||||
axe = axe.exclude("[data-floating-ui-portal]");
|
||||
/**
|
||||
* Bunch of axe violations arise from this separator not being a part of any
|
||||
* aria landmarks.
|
||||
* But fixing this involves restructuring the existing landmarks.
|
||||
* Ignore temporarily until we fix this.
|
||||
*/
|
||||
axe = axe.exclude(".mx_Separator");
|
||||
await use(axe);
|
||||
},
|
||||
|
||||
app: async ({ page }, use) => {
|
||||
|
||||
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 315 B After Width: | Height: | Size: 318 B |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 441 B After Width: | Height: | Size: 441 B |
|
Before Width: | Height: | Size: 393 B After Width: | Height: | Size: 392 B |
|
Before Width: | Height: | Size: 393 B After Width: | Height: | Size: 392 B |
|
Before Width: | Height: | Size: 372 B After Width: | Height: | Size: 373 B |
|
Before Width: | Height: | Size: 393 B After Width: | Height: | Size: 392 B |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |