Click off to deselect

This commit is contained in:
James Brunton
2026-08-24 10:04:27 +01:00
parent e2d1c4e2bc
commit afcf13c5c4
4 changed files with 80 additions and 2 deletions
@@ -86,6 +86,10 @@
overflow-y: hidden;
}
.laneHint {
pointer-events: none;
}
.laneEmpty {
align-items: center;
justify-content: center;
@@ -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}
>
<div className={styles.scroller} data-scrolling-container="true">
<div
className={styles.scroller}
data-scrolling-container="true"
onClick={(event) => {
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}
/>
@@ -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({
<div
ref={setNodeRef}
data-track-lane={track.fileId}
className={[
styles.lane,
track.pages.length === 0 ? styles.laneEmpty : "",
]
.filter(Boolean)
.join(" ")}
// Only a click on the lane itself, never one that bubbled up from a
// page: clicking a tile would otherwise select it and immediately
// clear it again.
onClick={(event) => {
if (event.target === event.currentTarget) onClearSelection();
}}
>
{track.pages.length === 0 && (
<span>
<span className={styles.laneHint}>
{t(
"pageTracks.emptyTrack",
"No pages left. Drag pages here, or save to close this file.",
@@ -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);
});
});