fix(abstract): prevent drop target oscillation caused by layout shifts#2101
Open
clauderic wants to merge 1 commit into
Open
fix(abstract): prevent drop target oscillation caused by layout shifts#2101clauderic wants to merge 1 commit into
clauderic wants to merge 1 commit into
Conversation
Setting a drop target can shift the layout (e.g. optimistic re-ordering across columns), which can flip the outcome of collision detection at the same pointer position and immediately re-target the previous droppable, oscillating between the two on every micro pointer movement. CollisionNotifier now tracks recently set or cleared drop targets along with the coordinates at which the change happened, and ignores collisions with them until the pointer has traveled a minimum distance (CollisionNotifier.hysteresis, 10px by default). Recording both the abandoned target and the new destination also catches cycles where OptimisticSortingPlugin re-targets the source between switches. Clearing the drop target is never suppressed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 9be9b23 The changes in this PR will be included in the next version bump. This PR includes changesets to release 10 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@dnd-kit/abstract
@dnd-kit/collision
@dnd-kit/dom
@dnd-kit/geometry
@dnd-kit/helpers
@dnd-kit/react
@dnd-kit/solid
@dnd-kit/state
@dnd-kit/svelte
@dnd-kit/vue
commit: |
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.
Problem
The winner of collision detection is a function of the current layout, but setting a drop target mutates the layout:
setDropTarget→dragover→OptimisticSortingPluginre-orders the DOM → droppable shapes shift. When the pointer sits in a region wherewinner(layout A) = Bbutwinner(layout B) = A, the drop target enters a limit cycle.The existing
#previousCoordinatesguard inCollisionObserveronly protects a perfectly still pointer — every pixel of hand jitter recomputes collisions against the shifted layout and flips the target again. In a multi-column board this manifests as an item flickering between two columns non-stop:Fix
CollisionNotifiernow keeps a small history of recent drop targets ({id, coordinates at switch}) and refuses to re-target any of them until the pointer has traveled at leastCollisionNotifier.hysteresispx (default10, now exported from@dnd-kit/abstractand tunable) from where the switch happened. Entries prune themselves once the pointer travels past the threshold, so deliberate movement back is honored after ~10px, while jitter-scale oscillation locks after at most one visible flip.Design notes:
OptimisticSortingPluginre-targets to the source after every cross-container reorder, so the oscillation manifests as the winning collision alternating between two other droppables while the target snaps back to the source each time. Matching only against the abandoned target would never fire.nullis never recorded — otherwise the target could stick to a droppable while the pointer visibly hovers empty space.isEqualearly-return is bypassed so pointer travel keeps being re-evaluated on every collision update; otherwise the collision id-order can stay identical indefinitely and the pending switch would never be honored.SortableKeyboardPluginbypasses the notifier).Verification
Deterministic repro driving 20 half-pixel jitter movements through a scripted
dragover-reacts-to-target feedback loop:col-2 → col-1 → col-2 → …forevercollision-notifier-hysteresis.test.ts(5 tests): plain A↔B cycle, target clearing, optimistic re-target-to-source cycle, per-drag state reset, threshold configurability.Behavior notes
CollisionNotifier.hysteresis.🤖 Generated with Claude Code