PagePane: drops in a row edge zone silently no-op (gap slots collapse before the drop-time recompute) #127

Open
opened 2026-07-19 09:58:10 +02:00 by dominik.polakovics · 0 comments

This was generated by AI while landing a PR.

Pre-existing bug — surfaced by the real-browser smoke test during #124 / PR #126, and reproduced bit-for-bit on a main build, so it does not originate from that refactor. Filed separately here because PR #126's body claimed it had been filed and it had not; the finding would otherwise be lost in a merged PR description.

Symptom

In PagePane, a drag that is released while the pointer sits in a row's before/after edge zone can silently do nothing — no move, no error, no indicator left behind. The element simply stays put.

Dropping on the gap slots — the primary affordance, the explicit insertion points between rows — lands correctly. Only the row-edge zones are affected, which makes this easy to miss: the feature appears to work until a user aims at a row edge instead of a gap.

Mechanism (as diagnosed during the smoke test)

  1. The drop handler clears drag state first.
  2. Clearing drag state un-renders the gap slots, which only exist during a drag.
  3. The gap slots collapsing shifts the layout — every row below moves up by the collapsed height.
  4. PagePane's drop-time zone recompute then calls getBoundingClientRect() on the row, reading the post-shift rect while using the drop event's original clientY.
  5. The recomputed zone no longer matches where the user actually released, and the verdict comes back not-ok, so the move is skipped.

The drop-time recompute is deliberate and correct in intent — the pointer genuinely may cross a zone boundary since the last dragover painted the indicator. The bug is that it recomputes against geometry that the clear step has already invalidated.

Where to look

  • web/src/components/PagePane.tsxrowDrop, which calls rowZoneAt(e, item) inside the reorder.drop action, i.e. after the primitive has cleared all drag state.
  • web/src/dragReorder.tsdrop() clears state and then runs the action. This ordering is intentional (actions receive the drag value as an argument precisely so they never re-read a cleared signal), so a fix likely belongs on the PagePane side rather than in the primitive.

Likely fix directions, for whoever triages: capture the row rect (or the resolved zone) before clearing, and hand it to the action — or defer the visual collapse until after the verdict resolves. Both need weighing against the reason the recompute exists.

Why jsdom cannot catch this

The failure is entirely geometric: it depends on real layout, real getBoundingClientRect() values, and a real reflow when the gap slots un-render. jsdom renders every rect zero-height, so the shift is zero and the recompute always agrees with the original zone. Any regression test for this needs the repo's playwright-core + nix Chromium recipe against harness serve.

Scope note

TreePane also does a drop-time zone recompute, but its rows do not collapse on clear, so it is not obviously affected. Worth confirming during triage rather than assuming.

Related: #124 (the primitive extraction), PR #126 (where this was found).

> *This was generated by AI while landing a PR.* **Pre-existing bug** — surfaced by the real-browser smoke test during #124 / PR #126, and **reproduced bit-for-bit on a `main` build**, so it does not originate from that refactor. Filed separately here because PR #126's body claimed it had been filed and it had not; the finding would otherwise be lost in a merged PR description. ## Symptom In PagePane, a drag that is released while the pointer sits in a **row's before/after edge zone** can silently do nothing — no move, no error, no indicator left behind. The element simply stays put. Dropping on the **gap slots** — the primary affordance, the explicit insertion points between rows — lands correctly. Only the row-edge zones are affected, which makes this easy to miss: the feature appears to work until a user aims at a row edge instead of a gap. ## Mechanism (as diagnosed during the smoke test) 1. The drop handler clears drag state first. 2. Clearing drag state un-renders the gap slots, which only exist during a drag. 3. The gap slots collapsing **shifts the layout** — every row below moves up by the collapsed height. 4. PagePane's drop-time zone recompute then calls `getBoundingClientRect()` on the row, reading the **post-shift** rect while using the drop event's original `clientY`. 5. The recomputed zone no longer matches where the user actually released, and the verdict comes back not-`ok`, so the move is skipped. The drop-time recompute is deliberate and correct in intent — the pointer genuinely may cross a zone boundary since the last `dragover` painted the indicator. The bug is that it recomputes against geometry that the clear step has already invalidated. ## Where to look - `web/src/components/PagePane.tsx` — `rowDrop`, which calls `rowZoneAt(e, item)` inside the `reorder.drop` action, i.e. *after* the primitive has cleared all drag state. - `web/src/dragReorder.ts` — `drop()` clears state and then runs the action. This ordering is intentional (actions receive the drag value as an argument precisely so they never re-read a cleared signal), so a fix likely belongs on the PagePane side rather than in the primitive. Likely fix directions, for whoever triages: capture the row rect (or the resolved zone) *before* clearing, and hand it to the action — or defer the visual collapse until after the verdict resolves. Both need weighing against the reason the recompute exists. ## Why jsdom cannot catch this The failure is entirely geometric: it depends on real layout, real `getBoundingClientRect()` values, and a real reflow when the gap slots un-render. jsdom renders every rect zero-height, so the shift is zero and the recompute always agrees with the original zone. Any regression test for this needs the repo's playwright-core + nix Chromium recipe against `harness serve`. ## Scope note TreePane also does a drop-time zone recompute, but its rows do not collapse on clear, so it is not obviously affected. Worth confirming during triage rather than assuming. Related: #124 (the primitive extraction), PR #126 (where this was found).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/deckle#127
No description provided.