Room list: drag and drop rooms into sections (#33366)
* chore: add dnd kit deps * chore: patch dnd kit to fix ts error * feat(sc): add drag-and-drop to room list item and wrapper * feat(sc): make the room list header a droppable element * feat(sc): add dnd to room list view * feat(tags): can tag room as CHAT * feat(vm): implement `changeRoomSection` * feat(sc): disable dragging in flat list * fix: disable keyboard navigation when dragging element * test(sc): update snapshots * test(sc): add dnd test * test(e2e): add e2e tests for room drag and drop * test(vm): add tests for changeRoomSection * fix: remove focus visible when dropping with the mouse * test(playwright): update existing screenshots * chore(sc): move numbers out of main build The Ew RecorderWorklet imports shared component bundle. However if the bundle uses some deps using document/window which, the worklet will not work. The solution is to put the used functions into a separate bundle. * doc(sc): add subpath import into README * doc: typo barrel/bundle * test: improve test expect * refactor: add utils to section tag * fix: incorrect check in tagRoom * fix: add doc about dndkit tunning
This commit is contained in:
+1
-17
@@ -8,7 +8,7 @@
|
||||
import { type Page } from "@playwright/test";
|
||||
|
||||
import { expect, test } from "../../../element-web-test";
|
||||
import { getRoomList, getRoomListHeader, getSectionHeader } from "./utils";
|
||||
import { assertRoomInSection, getRoomList, getRoomListHeader, getSectionHeader } from "./utils";
|
||||
|
||||
test.describe("Room list custom sections", () => {
|
||||
test.use({
|
||||
@@ -40,22 +40,6 @@ test.describe("Room list custom sections", () => {
|
||||
await expect(dialog).not.toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a room is nested under a specific section using the treegrid aria-level hierarchy.
|
||||
* Section header rows sit at aria-level=1; room rows nested within a section sit at aria-level=2.
|
||||
* Verifies that the closest preceding aria-level=1 row is the expected section header.
|
||||
*/
|
||||
async function assertRoomInSection(page: Page, sectionName: string, roomName: string): Promise<void> {
|
||||
const roomList = getRoomList(page);
|
||||
const roomRow = roomList.getByRole("row", { name: `Open room ${roomName}` });
|
||||
// Room row must be at aria-level=2 (i.e. inside a section)
|
||||
await expect(roomRow).toHaveAttribute("aria-level", "2");
|
||||
// The closest preceding aria-level=1 row must be the expected section header.
|
||||
// XPath preceding:: axis returns nodes before the context in document order; [1] picks the nearest one.
|
||||
const closestSectionHeader = roomRow.locator(`xpath=preceding::*[@role="row" and @aria-level="1"][1]`);
|
||||
await expect(closestSectionHeader).toContainText(sectionName);
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page, app, user }) => {
|
||||
// The notification toast is displayed above the search section
|
||||
await app.closeNotificationToast();
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import { expect, test } from "../../../element-web-test";
|
||||
import { getPrimaryFilters, getRoomList, getSectionHeader } from "./utils";
|
||||
import { assertRoomInSection, dragRoomToSection, getPrimaryFilters, getRoomList, getSectionHeader } from "./utils";
|
||||
|
||||
test.describe("Room list sections", () => {
|
||||
test.use({
|
||||
@@ -182,6 +182,37 @@ test.describe("Room list sections", () => {
|
||||
roomItem = roomList.getByRole("row", { name: "Open room my room" });
|
||||
await expect(roomItem).toBeVisible();
|
||||
});
|
||||
|
||||
test("should move a room from Chats to Favourites when using dnd", async ({ page, app }) => {
|
||||
await app.client.createRoom({ name: "my room" });
|
||||
|
||||
const favouriteId = await app.client.createRoom({ name: "favourite room" });
|
||||
await app.client.evaluate(async (client, roomId) => {
|
||||
await client.setRoomTag(roomId, "m.favourite");
|
||||
}, favouriteId);
|
||||
|
||||
await dragRoomToSection(page, "my room", "Favourites");
|
||||
await assertRoomInSection(page, "Favourites", "my room");
|
||||
});
|
||||
|
||||
test("should move a room from Favourites to Chats when using dnd", async ({ page, app }) => {
|
||||
const favouriteId = await app.client.createRoom({ name: "my room" });
|
||||
await app.client.evaluate(async (client, roomId) => {
|
||||
await client.setRoomTag(roomId, "m.favourite");
|
||||
}, favouriteId);
|
||||
|
||||
// Create a second favourite room to ensure we stay in section mode (not flat list)
|
||||
const favouriteId2 = await app.client.createRoom({ name: "favourite room" });
|
||||
await app.client.evaluate(async (client, roomId) => {
|
||||
await client.setRoomTag(roomId, "m.favourite");
|
||||
}, favouriteId2);
|
||||
|
||||
// Ensure the Chats section is visible by creating a room in it
|
||||
await app.client.createRoom({ name: "room in chats" });
|
||||
|
||||
await dragRoomToSection(page, "my room", "Chats");
|
||||
await assertRoomInSection(page, "Chats", "my room");
|
||||
});
|
||||
});
|
||||
|
||||
test("should show unread indicator on section header", async ({ page, app, bot }) => {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type Locator, type Page } from "@playwright/test";
|
||||
import { expect, type Locator, type Page } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Get the room list
|
||||
@@ -35,6 +35,49 @@ export function getSectionHeader(page: Page, sectionName: string, isUnread = fal
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a room is nested under a specific section using the treegrid aria-level hierarchy.
|
||||
* Section header rows sit at aria-level=1; room rows nested within a section sit at aria-level=2.
|
||||
* Verifies that the closest preceding aria-level=1 row is the expected section header.
|
||||
*/
|
||||
export async function assertRoomInSection(page: Page, sectionName: string, roomName: string): Promise<void> {
|
||||
const roomList = getRoomList(page);
|
||||
const roomRow = roomList.getByRole("row", { name: `Open room ${roomName}` });
|
||||
// Room row must be at aria-level=2 (i.e. inside a section)
|
||||
await expect(roomRow).toHaveAttribute("aria-level", "2");
|
||||
// The closest preceding aria-level=1 row must be the expected section header.
|
||||
// XPath preceding:: axis returns nodes before the context in document order; [1] picks the nearest one.
|
||||
const closestSectionHeader = roomRow.locator(`xpath=preceding::*[@role="row" and @aria-level="1"][1]`);
|
||||
await expect(closestSectionHeader).toContainText(sectionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag and drop a room row onto a section header
|
||||
* @param page
|
||||
* @param roomName
|
||||
* @param sectionName
|
||||
*/
|
||||
export async function dragRoomToSection(page: Page, roomName: string, sectionName: string): Promise<void> {
|
||||
const sourceRow = getRoomList(page).getByRole("row", { name: `Open room ${roomName}` });
|
||||
const source = sourceRow.locator("button").first();
|
||||
const target = getSectionHeader(page, sectionName);
|
||||
|
||||
const sourceBox = await source.boundingBox();
|
||||
const targetBox = await target.boundingBox();
|
||||
|
||||
const sourceX = sourceBox.x + sourceBox.width / 2;
|
||||
const sourceY = sourceBox.y + sourceBox.height / 2;
|
||||
const targetY = targetBox.y + targetBox.height / 2;
|
||||
|
||||
// Grab the room
|
||||
await page.mouse.move(sourceX, sourceY);
|
||||
await page.mouse.down();
|
||||
// Move the room on the section header
|
||||
await page.mouse.move(sourceX, targetY, { steps: 10 });
|
||||
// Drop the room
|
||||
await page.mouse.up();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the primary filters container
|
||||
* @param page
|
||||
|
||||
Reference in New Issue
Block a user