-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
fix nodeClickDistance #5556
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
fix nodeClickDistance #5556
Conversation
|
WalkthroughUpdated XYDrag to track initial mouse position and a preventClick flag, disable click-distance initiation by setting clickDistance to Infinity, intercept capture-phase clicks to suppress incidental clicks after drag, and remove the handler on destroy. Also adjusted example node drag sensitivity and nodeClickDistance. (38 words) Changes
Sequence DiagramsequenceDiagram
participant User
participant XYDrag
participant DOM
rect rgb(240,248,255)
Note over User,XYDrag: Drag start
User->>XYDrag: mousedown
XYDrag->>XYDrag: record initialMousePosition\npreventClick = false
end
rect rgb(255,250,235)
Note over User,XYDrag: Dragging
User->>XYDrag: mousemove
XYDrag->>XYDrag: compute distance from initialMousePosition\nif nodes moved or distance > threshold → preventClick = true
end
rect rgb(240,255,240)
Note over User,DOM: Drag end & click interception
User->>XYDrag: mouseup
XYDrag->>DOM: native click may be emitted
DOM->>DOM: capture-click handler checks preventClick / distance
alt preventClick == true or distance exceeded
DOM->>DOM: stopImmediatePropagation / preventDefault (suppress click)
else
DOM->>User: allow click
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Suggestion about how to continue with this:
I am very sure no one is rocking a config right now where |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Nitpick comments (2)
packages/system/src/xydrag/XYDrag.ts (2)
341-351: Use initialMousePosition for threshold calc (clearer, avoids accidental drift).You compute distance from mousePosition, which later mutates. Using initialMousePosition is simpler and intention-revealing. Also prefer Math.hypot.
- const currentMousePosition = getEventPosition(event.sourceEvent, containerBounds!); - const x = currentMousePosition.x - mousePosition.x; - const y = currentMousePosition.y - mousePosition.y; - const distance = Math.sqrt(x * x + y * y); + const currentMousePosition = getEventPosition(event.sourceEvent, containerBounds!); + const dx = currentMousePosition.x - initialMousePosition.x; + const dy = currentMousePosition.y - initialMousePosition.y; + const distance = Math.hypot(dx, dy);
102-104: State additions are fine; just ensure they’re updated for non-draggable interactions.initialMousePosition and preventClick are appropriate. The issues arise from when they’re not updated (non-draggable clicks). Addressed in the click handler refactor comment.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
packages/system/src/xydrag/XYDrag.ts(5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/system/src/xydrag/XYDrag.ts (2)
packages/system/src/types/general.ts (1)
UseDragEvent(267-267)packages/system/src/utils/dom.ts (1)
getEventPosition(52-61)
🔇 Additional comments (2)
packages/system/src/xydrag/XYDrag.ts (2)
299-301: clickDistance(Infinity) rationale looks good; confirm intent vs d3 semantics.d3’s clickDistance controls suppression of the click after mouseup, not drag initiation. Setting it to Infinity disables d3’s built-in click suppression so your custom capture-phase logic governs clicks. Please confirm with tests across mouse/touch.
According to d3-drag docs, drag.clickDistance sets the maximum pointer movement that will still allow the subsequent click; measured in client coordinates; default 0. Setting Infinity prevents d3 from suppressing any click, leaving suppression to your code. (d3js.org)
303-308: Start-state resets and initial position capture — LGTM.Resetting flags and capturing client-coord initialMousePosition at gesture start is correct and consistent with the new thresholding.
Also applies to: 317-318
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
packages/system/src/xydrag/XYDrag.ts (1)
402-418: Critical: stale initialMousePosition on non‑draggable clicks can suppress unrelated clicks; add mousedown/touchstart capture and bounds fallback.If filter=false (e.g., clicking an input inside a node), “start” won’t run, leaving initialMousePosition stale. The capture-phase click handler then computes a large distance and wrongly blocks the click. Capture initial position on mousedown/touchstart regardless of drag filter, and fall back to fresh bounds in the click handler.
- d3Selection.on( - 'click.xydrag', - (event: MouseEvent) => { - const currentMousePosition = getEventPosition(event, containerBounds!); - const x = currentMousePosition.x - initialMousePosition.x; - const y = currentMousePosition.y - initialMousePosition.y; - const distance = Math.sqrt(x * x + y * y); - - if (preventClick || distance > nodeClickDistance) { - preventClick = false; - event.preventDefault(); - event.stopPropagation(); - } - }, - { capture: true } - ); + d3Selection + .on('mousedown.xydrag', (downEvent: MouseEvent) => { + const bounds = (downEvent.currentTarget as Element).getBoundingClientRect(); + containerBounds = bounds; + initialMousePosition = getEventPosition(downEvent, bounds); + }, { capture: true }) + .on('touchstart.xydrag', (downEvent: TouchEvent) => { + const bounds = (downEvent.currentTarget as Element).getBoundingClientRect(); + containerBounds = bounds; + initialMousePosition = getEventPosition(downEvent, bounds); + }, { capture: true }) + .on('click.xydrag', (event: MouseEvent) => { + const bounds = containerBounds ?? (event.currentTarget as Element).getBoundingClientRect(); + const { x, y } = getEventPosition(event, bounds); + const dx = x - initialMousePosition.x; + const dy = y - initialMousePosition.y; + const distance = Math.hypot(dx, dy); + if (preventClick || distance > nodeClickDistance) { + event.preventDefault(); + event.stopPropagation(); + // single-shot suppression + if (preventClick) preventClick = false; + } + }, { capture: true });Also, when rebinding to a new domNode inside update(), unbind previous handlers to avoid leaks/duplicates:
}: DragUpdateParams) { - d3Selection = select(domNode); + if (d3Selection) { + d3Selection.on('.drag', null).on('.xydrag', null); + } + d3Selection = select(domNode);
🧹 Nitpick comments (2)
packages/system/src/xydrag/XYDrag.ts (2)
341-347: Minor: use Math.hypot for clarity.Replace manual sqrt with Math.hypot.
- const x = currentMousePosition.x - initialMousePosition.x; - const y = currentMousePosition.y - initialMousePosition.y; - const distance = Math.sqrt(x * x + y * y); + const dx = currentMousePosition.x - initialMousePosition.x; + const dy = currentMousePosition.y - initialMousePosition.y; + const distance = Math.hypot(dx, dy);
421-424: Namespace cleanup for reliability.Remove all our namespaced handlers in destroy, not only click.
function destroy() { d3Selection?.on('.drag', null); - d3Selection?.on('click.xydrag', null); + d3Selection?.on('.xydrag', null); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
packages/system/src/xydrag/XYDrag.ts(6 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/system/src/xydrag/XYDrag.ts (2)
packages/system/src/types/general.ts (1)
UseDragEvent(267-267)packages/system/src/utils/dom.ts (1)
getEventPosition(52-61)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Playwright Tests
🔇 Additional comments (4)
packages/system/src/xydrag/XYDrag.ts (4)
102-104: State for click suppression looks good.Initialization of initialMousePosition and preventClick is clear and appropriate.
298-300: Relying on custom click suppression via clickDistance(Infinity).This is reasonable; please sanity-check on iOS Safari and Windows Chrome to ensure no regressions in native click behavior after drags.
303-308: Correct resets on drag start.Resetting dragStarted/autoPanStarted/preventClick and capturing the initial position is correct for consistent thresholds.
Also applies to: 317-318
369-373: Single‑shot click suppression on drag end: LGTM.Setting preventClick when positions changed pairs well with resetting it in the click handler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/system/src/xydrag/XYDrag.ts (1)
114-121: Avoid handler leaks on update(): clean up previous selection before rebinding.Without detaching old listeners, repeated update() calls with a new domNode will leave orphaned handlers on the previous element.
Apply:
function update({ @@ - }: DragUpdateParams) { - d3Selection = select(domNode); + }: DragUpdateParams) { + // detach any prior handlers before rebinding to a potentially new domNode + if (d3Selection) { + d3Selection.on('.drag', null).on('.xydrag', null); + } + d3Selection = select(domNode);
♻️ Duplicate comments (1)
packages/system/src/xydrag/XYDrag.ts (1)
403-418: Critical: stale initialMousePosition on non‑draggable clicks; add mousedown/touchstart capture + bounds fallback.click.xydrag relies on initialMousePosition set only in drag “start”. If a click occurs on a target that fails the drag filter, initialMousePosition is stale and legitimate clicks get suppressed; also no bounds fallback. Capture the initial position on every pointer down and compute a fresh bounds fallback in the click handler.
Apply:
- d3Selection.on( - 'click.xydrag', - (event: MouseEvent) => { - const currentMousePosition = getEventPosition(event, containerBounds!); - const x = currentMousePosition.x - initialMousePosition.x; - const y = currentMousePosition.y - initialMousePosition.y; - const distance = Math.sqrt(x * x + y * y); - - if (preventClick || distance > nodeClickDistance) { - preventClick = false; - event.preventDefault(); - event.stopPropagation(); - } - }, - { capture: true } - ); + d3Selection + .on('mousedown.xydrag', (downEvent: MouseEvent) => { + const bounds = (downEvent.currentTarget as Element).getBoundingClientRect(); + containerBounds = bounds; + initialMousePosition = getEventPosition(downEvent, bounds); + }, { capture: true }) + .on('touchstart.xydrag', (downEvent: TouchEvent) => { + const bounds = (downEvent.currentTarget as Element).getBoundingClientRect(); + containerBounds = bounds; + initialMousePosition = getEventPosition(downEvent, bounds); + }, { capture: true }) + .on('click.xydrag', (event: MouseEvent) => { + const bounds = containerBounds ?? (event.currentTarget as Element).getBoundingClientRect(); + const { x, y } = getEventPosition(event, bounds); + const dx = x - initialMousePosition.x; + const dy = y - initialMousePosition.y; + const distance = Math.hypot(dx, dy); + + if (preventClick || distance > nodeClickDistance) { + preventClick = false; // single-shot suppression after drag + event.preventDefault(); + event.stopPropagation(); + } + }, { capture: true });
🧹 Nitpick comments (1)
packages/system/src/xydrag/XYDrag.ts (1)
341-347: Tiny tidy: use Math.hypot and clearer names.No behavior change; improves readability.
- const currentMousePosition = getEventPosition(event.sourceEvent, containerBounds!); - const x = currentMousePosition.x - initialMousePosition.x; - const y = currentMousePosition.y - initialMousePosition.y; - const distance = Math.sqrt(x * x + y * y); + const pos = getEventPosition(event.sourceEvent, containerBounds!); + const dx = pos.x - initialMousePosition.x; + const dy = pos.y - initialMousePosition.y; + const distance = Math.hypot(dx, dy);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
packages/system/src/xydrag/XYDrag.ts(6 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/system/src/xydrag/XYDrag.ts (2)
packages/system/src/types/general.ts (1)
UseDragEvent(267-267)packages/system/src/utils/dom.ts (1)
getEventPosition(52-61)
🔇 Additional comments (2)
packages/system/src/xydrag/XYDrag.ts (2)
298-300: Setting d3 clickDistance to Infinity — good call.Disables d3’s internal click initiation so your custom suppression governs behavior.
369-373: Single‑shot click suppression after move — looks correct.preventClick only set when positions changed; click handler resets it after suppression.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
examples/react/src/examples/Basic/index.tsx (1)
165-166: Consider demonstrating snapToGrid to showcase the fix.The configuration values are correct and align with the PR objectives. However, since this PR primarily fixes issue #5204 (snapToGrid selection glitch), consider adding
snapToGrid={true}to this example to demonstrate the fix in action. Additionally, brief comments explaining whynodeClickDistance={Infinity}is recommended would help users understand the new behavior.Example addition:
nodeDragThreshold={5} + snapToGrid={true} + snapGrid={[15, 15]} nodeClickDistance={Infinity}Or add a comment explaining the configuration:
nodeDragThreshold={5} // Infinity prevents click events after any position change, recommended for snapGrid nodeClickDistance={Infinity}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
examples/react/src/examples/Basic/index.tsx(1 hunks)packages/system/src/xydrag/XYDrag.ts(6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/system/src/xydrag/XYDrag.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Playwright Tests
closes #5204 and to an extent #4996
2 things changed:
This leads to a much better default especially in connection with
snapGrid. You can simply set thenodeClickDistancetoInfinitywhich leads to nodeClickEvents always firing when the node has not moved. This way you don't even have to rely on some arbitrary number but can just let yoursnapGridcontrol it implicitly.I would argue you NEVER want to fire click events after dragging has commenced.
We might want to check this assumption based on the how things like. This is the same behavior asdraggablehandle thisdraggablehtml elements have.The question remains if we shouldn't just remove nodeClickDistance, because nodeDragThreshold should be the sole value controlling this behavior.
Summary by CodeRabbit
Bug Fixes
New Features / Behavior Changes