Room list: add drag and drop of sections to reorder them (#33606)
* feat(rls): centralize room-list section ordering via getOrderedSections Introduce a single source of truth for the ordered list of section tags (defaults + custom) in section.ts. RoomListStoreV3 no longer hard-codes the default tag order — it asks section.ts. * feat(vm): add section reorder logic to RoomListStoreV3 and view model * refactor(sc): extract RoomListSectionHeaderContent from RoomListSectionHeaderView * feat(sc): add drag-and-drop reordering for room-list sections * test(sc): update existing tests * test: add new tests * test(sc): add snapshot test for overlay * test(e2e): add playright test for dnd of sections * feat: tweak opacity * fix: section detection when dragged * feat: use relative position in section order * chore: move style to room list section header view * test: add e2e test for moving section before another * feat: make favourite and low priority no draggable * test: remove deprecated e2e test * fix: type correctly dnd provider and hooks * fix: use String instead of casting to string * fix: wrong a11y attributes on section header * test: fix keyboard navigation e2e tests * feat: use custom a11y announcement for dnd * fix: add aria-hidden to the overlay * chore: remove duplicated code in a11Y announcement * fix: increase keyboard drag offset * fix: tests * fix: virtuoso computed item key * fix: aria-expanded double annoncement why dragging * fix: re-add screen reader instructions * chore: remove unused import * fix: reduce keyboard drag offset * fix: improve text readback on unread section * feat: use a custom a11y plugin instead of buitin a11y plugin * chore: formatting * test: fix incorrect tests * chore: use randomUUID * fix: try to make test working * fix: put back mock * fix: circular import * Revert "fix: circular import" This reverts commit 6c69313ade9ed2ab17a4622c692b435fbdb94ad2. * chore: add @dnd-kit/abstract to optimizeDeps.include * test: fix e2e tests * fix: disable interaction with section when dragging * fix: be more explicit if the section will be dropped before or after * fix: add info that space is dropping too * fix: scroll to dropped section * test: fix virtualized room list test * chore: update lang * chore: fix dead code analyze * test: add provider section story * fix: lang * fix: again lang... * fix: improve voice over on chrome * test: upate snapshot * fix: add readback when a section is over a non droppable element
This commit is contained in:
+36
-1
@@ -9,7 +9,15 @@ import { type Page } from "@playwright/test";
|
||||
import { closeReleaseAnnouncement, rejectToast } from "@element-hq/element-web-playwright-common";
|
||||
|
||||
import { expect, test } from "../../../element-web-test";
|
||||
import { assertRoomInSection, dragRoomToSection, getRoomList, getRoomListHeader, getSectionHeader } from "./utils";
|
||||
import {
|
||||
assertRoomInSection,
|
||||
assertSectionsOrder,
|
||||
dragRoomToSection,
|
||||
dragSectionToSection,
|
||||
getRoomList,
|
||||
getRoomListHeader,
|
||||
getSectionHeader,
|
||||
} from "./utils";
|
||||
|
||||
test.describe("Room list custom sections", () => {
|
||||
test.use({
|
||||
@@ -287,6 +295,33 @@ test.describe("Room list custom sections", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Section reordering via dnd", () => {
|
||||
test("should reorder custom sections via dnd", async ({ page, app }) => {
|
||||
await app.client.createRoom({ name: "my room" });
|
||||
await createCustomSection(page, "Work");
|
||||
await createCustomSection(page, "Personal");
|
||||
|
||||
// Default placement: custom sections sit at the top of Chats
|
||||
await assertSectionsOrder(page, ["Work", "Personal", "Chats"]);
|
||||
|
||||
// Moves Work after Chats
|
||||
await dragSectionToSection(page, "Work", "Chats");
|
||||
await assertSectionsOrder(page, ["Personal", "Chats", "Work"]);
|
||||
});
|
||||
|
||||
test("should insert a section before the target when dragging up", async ({ page, app }) => {
|
||||
await app.client.createRoom({ name: "my room" });
|
||||
await createCustomSection(page, "Work");
|
||||
await createCustomSection(page, "Personal");
|
||||
|
||||
await assertSectionsOrder(page, ["Work", "Personal", "Chats"]);
|
||||
|
||||
// Personal sits below Work, so dragging it onto Work inserts it before Work.
|
||||
await dragSectionToSection(page, "Personal", "Work");
|
||||
await assertSectionsOrder(page, ["Personal", "Work", "Chats"]);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Adding a room to a custom section", () => {
|
||||
test("should add a room to a custom section via the More Options menu", async ({ page, app }) => {
|
||||
await app.client.createRoom({ name: "my room" });
|
||||
|
||||
@@ -30,8 +30,8 @@ export function getRoomListHeader(page: Page): Locator {
|
||||
* @param isUnread Whether to look for the unread version of the section header
|
||||
*/
|
||||
export function getSectionHeader(page: Page, sectionName: string, isUnread = false): Locator {
|
||||
return getRoomList(page).getByRole("gridcell", {
|
||||
name: isUnread ? `Toggle ${sectionName} section with unread room(s)` : `Toggle ${sectionName} section`,
|
||||
return getRoomList(page).getByRole("button", {
|
||||
name: isUnread ? `Toggle ${sectionName} section with unread rooms` : `Toggle ${sectionName} section`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -106,6 +106,60 @@ async function getBoundingBox(
|
||||
return box;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag and drop a section header onto another section header. The dragged section is moved
|
||||
* relative to the target: dropped before the target when dragging up, after the target when
|
||||
* dragging down. Because the dnd start handler collapses every section, the layout changes
|
||||
* once the drag activates — so the target position is recomputed after activation rather
|
||||
* than cached up-front.
|
||||
*/
|
||||
export async function dragSectionToSection(
|
||||
page: Page,
|
||||
sourceSectionName: string,
|
||||
targetSectionName: string,
|
||||
): Promise<void> {
|
||||
const source = getSectionHeader(page, sourceSectionName);
|
||||
const sourceBox = await source.boundingBox();
|
||||
if (!sourceBox) throw new Error(`Source section ${sourceSectionName} has no bounding box`);
|
||||
|
||||
const sourceX = sourceBox.x + sourceBox.width / 2;
|
||||
const sourceY = sourceBox.y + sourceBox.height / 2;
|
||||
|
||||
// Grab the section header
|
||||
await page.mouse.move(sourceX, sourceY);
|
||||
await page.mouse.down();
|
||||
// Move past the 5px PointerSensor activation threshold so the drag actually starts.
|
||||
// This triggers onSectionDragStart, which collapses all sections.
|
||||
await page.mouse.move(sourceX, sourceY + 10, { steps: 5 });
|
||||
|
||||
// Re-query the target now that the layout has reflowed.
|
||||
const target = getSectionHeader(page, targetSectionName);
|
||||
const targetBox = await target.boundingBox();
|
||||
if (!targetBox) throw new Error(`Target section ${targetSectionName} has no bounding box`);
|
||||
const targetY = targetBox.y + targetBox.height / 2;
|
||||
|
||||
// Move onto the (possibly relocated) target section header and drop.
|
||||
await page.mouse.move(sourceX, targetY, { steps: 10 });
|
||||
await page.mouse.up();
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the displayed section headers appear in the given top-to-bottom order.
|
||||
*/
|
||||
export async function assertSectionsOrder(page: Page, expectedOrder: string[]): Promise<void> {
|
||||
const positions: Array<{ name: string; y: number }> = [];
|
||||
for (const name of expectedOrder) {
|
||||
const header = getSectionHeader(page, name);
|
||||
await expect(header).toBeVisible();
|
||||
const box = await header.boundingBox();
|
||||
if (!box) throw new Error(`Section ${name} has no bounding box`);
|
||||
positions.push({ name, y: box.y });
|
||||
}
|
||||
for (let i = 1; i < positions.length; i++) {
|
||||
expect(positions[i].y).toBeGreaterThan(positions[i - 1].y);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the primary filters container
|
||||
* @param page
|
||||
|
||||
Reference in New Issue
Block a user