diff --git a/frontend/editor/src/core/components/pageTracks/PageTracks.module.css b/frontend/editor/src/core/components/pageTracks/PageTracks.module.css index 35221c310e..fbe688bc12 100644 --- a/frontend/editor/src/core/components/pageTracks/PageTracks.module.css +++ b/frontend/editor/src/core/components/pageTracks/PageTracks.module.css @@ -86,6 +86,10 @@ overflow-y: hidden; } +.laneHint { + pointer-events: none; +} + .laneEmpty { align-items: center; justify-content: center; diff --git a/frontend/editor/src/core/components/pageTracks/PageTracks.tsx b/frontend/editor/src/core/components/pageTracks/PageTracks.tsx index 1f222398ed..2983cb5444 100644 --- a/frontend/editor/src/core/components/pageTracks/PageTracks.tsx +++ b/frontend/editor/src/core/components/pageTracks/PageTracks.tsx @@ -116,6 +116,8 @@ export default function PageTracks() { [deletePages, selection.selectedIds], ); + const clearSelection = selection.clear; + // ── Drag and drop ──────────────────────────────────────────────────────── const sensors = useSensors( @@ -331,7 +333,13 @@ export default function PageTracks() { onDragEnd={handleDragEnd} onDragCancel={handleDragCancel} > -
+
{ + if (event.target === event.currentTarget) clearSelection(); + }} + > {workspace.order.map((fileId) => { const track = workspace.tracks[fileId]; if (!track) return null; @@ -351,6 +359,7 @@ export default function PageTracks() { thumbnails={thumbnails} onSelectPage={selection.selectPage} onSelectTrack={selection.selectTrack} + onClearSelection={clearSelection} onRotate={rotatePages} onDelete={deletePages} /> diff --git a/frontend/editor/src/core/components/pageTracks/TrackRow.tsx b/frontend/editor/src/core/components/pageTracks/TrackRow.tsx index 4ac2a8a605..1ecdd1f2d9 100644 --- a/frontend/editor/src/core/components/pageTracks/TrackRow.tsx +++ b/frontend/editor/src/core/components/pageTracks/TrackRow.tsx @@ -40,6 +40,8 @@ export interface TrackRowProps { modifiers: PageClickModifiers, ) => void; onSelectTrack: (fileId: FileId) => void; + /** Called when the click landed on empty lane surface, not on a page. */ + onClearSelection: () => void; onRotate: (pageIds: string[], delta: number) => void; onDelete: (pageIds: string[]) => void; } @@ -57,6 +59,7 @@ function TrackRowImpl({ thumbnails, onSelectPage, onSelectTrack, + onClearSelection, onRotate, onDelete, }: TrackRowProps) { @@ -168,15 +171,22 @@ function TrackRowImpl({
{ + if (event.target === event.currentTarget) onClearSelection(); + }} > {track.pages.length === 0 && ( - + {t( "pageTracks.emptyTrack", "No pages left. Drag pages here, or save to close this file.", diff --git a/frontend/editor/src/core/tests/stubbed/page-tracks.spec.ts b/frontend/editor/src/core/tests/stubbed/page-tracks.spec.ts index 884b9d378b..7b56b5cea3 100644 --- a/frontend/editor/src/core/tests/stubbed/page-tracks.spec.ts +++ b/frontend/editor/src/core/tests/stubbed/page-tracks.spec.ts @@ -360,4 +360,59 @@ test.describe("Page Editor tracks", () => { // Only the two clicked pages turn; the ones in between are untouched. expect(await readRotations(rotated, 4)).toEqual([90, 90, 0, 180]); }); + + test("clicking the empty space around the pages deselects everything", async ({ + page, + }) => { + await openPageEditor(page); + const rotated = track(page, "rotated-pages.pdf"); + const tiles = rotated.locator("[data-page-id]"); + await expect(tiles).toHaveCount(4, { timeout: 30_000 }); + const anySelected = page.locator('[data-page-id][data-selected="true"]'); + + await tiles.nth(0).click(); + await tiles.nth(2).click(); + await expect(anySelected).toHaveCount(2); + + // The lane runs past its last page; that surface is not a page. + const box = await rotated.locator("[data-track-lane]").boundingBox(); + if (!box) throw new Error("lane is not laid out"); + const last = await tiles.nth(3).boundingBox(); + if (!last) throw new Error("tile is not laid out"); + await page.mouse.click( + (last.x + last.width + box.x + box.width) / 2, + box.y + box.height / 2, + ); + await expect(anySelected).toHaveCount(0); + }); + + test("a drag that lands on empty lane space keeps the moved selection", async ({ + page, + }) => { + await openPageEditor(page); + const rotated = track(page, "rotated-pages.pdf"); + const sample = track(page, "sample.pdf"); + const tiles = rotated.locator("[data-page-id]"); + await expect(tiles).toHaveCount(4, { timeout: 30_000 }); + + await tiles.nth(0).click(); + await tiles.nth(1).click(); + await expect( + page.locator('[data-page-id][data-selected="true"]'), + ).toHaveCount(2); + + // sample.pdf has one page, so the lane past it is empty space: the drop + // releases over the lane, which is also where a deselect click would land. + await dragPageOnto( + page, + tiles.nth(0), + sample.locator("[data-page-id]").first(), + 0.9, + ); + + await expect(sample.locator("[data-page-id]")).toHaveCount(3); + await expect( + page.locator('[data-page-id][data-selected="true"]'), + ).toHaveCount(2); + }); });