fix(desktop): refuse sidebar drags from a press with no button held - #5619
Open
kunsanglee wants to merge 1 commit into
Open
fix(desktop): refuse sidebar drags from a press with no button held#5619kunsanglee wants to merge 1 commit into
kunsanglee wants to merge 1 commit into
Conversation
Clicking a channel in the sidebar sometimes started a drag nobody asked for: a preview followed the cursor and the channel never opened. dnd-kit's PointerSensor decides to start a drag from pointer travel alone and never re-reads the button state, so a `pointerup` the page does not receive leaves the sensor armed. The next cursor move begins a drag, and because dnd-kit installs a capture-phase `click` blocker as soon as a drag activates, the click that move belonged to is swallowed. The macOS webview reports tap-to-click exactly that way. Instrumenting it over 38 gestures: a tap is recognised after the finger has already lifted, so `pointerdown` arrives with `buttons: 0` (34 of 38) and the matching `pointerup` is deferred — 356ms to over 1.2s, and 8 times not within 700ms at all. A deliberate press-and-hold reports `buttons: 1` throughout. The two never crossed: every measured `buttons: 0` press was a click, and all three deliberate drags started from `buttons: 1`. So the sensor declines at that moment rather than arming. dnd-kit only instantiates a sensor when the activator returns exactly `true`, and reads `activators` off the sensor class, so a subclass that checks `buttons` before delegating to the base handler means no session, no click blocker, and nothing to unwind. The base handler still does the rest, including the primary/left-button checks and `onActivation`. The community rail gets the same sensor: a swallowed click there fails a community switch, which is heavier than a missed channel. Covered by e2e rather than a unit test because the behaviour needs a real DOM and dnd-kit's sensor pipeline, and each test reproduces the production sequence — a `pointerdown` reporting no button held, which `page.mouse.down()` cannot produce — then asserts the following click still selects its target. Validated by mutation in both directions: dropping the button check fails both refused-press tests, and refusing every press instead fails the deliberate-drag tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: kunsanglee <85242378+kunsanglee@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Clicking a channel in the sidebar sometimes started a drag nobody asked for: a
preview followed the cursor and the channel never opened.
dnd-kit's
PointerSensordecides to start a drag from pointer travel alone andnever re-reads the button state, so a
pointerupthe page does not receiveleaves the sensor armed. The next cursor move begins a drag, and because dnd-kit
installs a capture-phase
clickblocker as soon as a drag activates(
@dnd-kit/core6.3.1, inhandleStart), the click that move belonged to isswallowed. The channel selection never runs.
The releases really do go missing. I instrumented the macOS webview and
logged every pointer event over 38 gestures of ordinary sidebar use:
pointerdownpointerupbuttons: 1buttons: 0A tap is recognised after the finger has already lifted, so the webview reports
pointerdownwith no button pressed and defers the matchingpointerup. Duringthat window dnd-kit is still armed, and moving the cursor a few pixels toward
the next channel starts a drag.
The two gesture shapes never crossed in the measurements: every
buttons: 0press was a click, every
buttons: 1press was a real drag. So the fix is tonot start. dnd-kit only instantiates a sensor when the activator returns exactly
true, and readsactivatorsoff the sensor class, so a subclass that checksbuttonsbefore delegating to the base handler means no session, no clickblocker, and nothing to unwind.
Key decisions
earlier revision watched for button-free movement and dispatched a synthetic
pointercancel. That was 45 lines and it could tear down a drag that wasgoing fine, on the strength of a single move event from the same webview whose
button reporting the change exists to distrust. Declining at
pointerdowncannot do that, and it also sidesteps dnd-kit deferring its document-listener
teardown by 50ms, which leaves the click blocker up for a moment after a
cancel.
buttons !== 0, not a bitmask test. The base handler already rejectsnon-primary and non-left presses; the only new claim is that a press with no
button pressed is not a press. Touch contact and pen contact both report a
button, so neither is affected.
onActivationand the primary/left checks keep coming from dnd-kit.
was 21–170px, so no threshold value separates these gestures; raising it would
have been an unrelated change that fixed nothing.
swallowed click means a failed community switch, which is heavier than a
missed channel.
One case is deliberately not covered: a drag that starts with a button held and
loses its release later. It never appeared in the measurements, and Esc
already cancels a live drag, so nothing is built for it until it is reported.
Related issue
None found. I searched open issues and PRs for
drag,sidebar,pointerup,dnd-kitandclick drag sidebar— no existing report of this behaviour.Adjacent, not duplicates:
direction (missing capability), untouched here.
test(desktop): stabilize keyboard community reorder. Touchesdesktop/tests/e2e/community-rail.spec.ts, the same file this PR appends atest to, so one of the two will need a trivial rebase.
Testing
just cipasses locally.Regression coverage is e2e rather than a unit test because the behaviour needs a
real DOM and dnd-kit's full sensor pipeline; a
*.test.mjsbeside the modulecould only re-assert the code it is testing.
Each test reproduces the production sequence — a
pointerdownreporting nobutton held, which
page.mouse.down()cannot produce — and asserts what theuser loses, that the following click selects its target:
sidebar-drag-activation.spec.ts— a refused press does not arm a drag or eatthe next channel click; and a real press-and-drag still commits its section
reorder right after one was refused.
community-rail.spec.ts— the same for the rail, asserting the communityswitch lands.
Each test was validated by mutation, both directions, checking the build
exit code separately so a type error could not silently serve a stale bundle:
sidebar-drag-activation, and the existing rail reorder test atcommunity-rail.spec.ts:1005)Manual verification on macOS, in two passes. The instrumented 38-gesture session
above was taken with an earlier revision of this change; it is where the
buttonsseparation and the count of 23 come from. This revision was thenchecked by using a dev build normally — the recording below is that check.
Recording
Nothing renders differently, so before/after screenshots would be identical
pixels; these are the interaction instead, from a dev build on macOS with
tap-to-click.
Clicking channels — each click opens its channel, and no drag preview appears:
Deliberately pressing and dragging — the preview appears, follows the cursor,
and the drop lands:
Deferred to follow-up PRs
desktop/src/features/home/useResizableInboxListWidth.tsanddesktop/src/shared/hooks/useThreadPanelWidth.tshave the same defect class —they listen for
pointerupwith{ once: true }and handle nopointercancel, so a lost release leavesdocument.body.style.cursoranduserSelect: nonestuck app-wide.DndContextwiresonDragCancel, so cancelling a live drag withEsc leaves the dimmed source row dimmed. Pre-existing, unrelated to
the press that starts it.
kind:KIND_CHANNEL_SECTIONSevent (useChannelSections.ts). Not caused bythis bug, but it made each spurious drag reach the relay.
SidebarDndContextadvertisesaria-roledescription="draggable"withoutregistering a
KeyboardSensor(the rail has one), so keyboard users are toldabout an interaction they cannot perform.