feat(dnd): add DragHandle component for explicit drag initiation#11
feat(dnd): add DragHandle component for explicit drag initiation#11
Conversation
Adds a DragHandle component that restricts drag operations to only trigger when dragging from a specific element, rather than the entire sortable item. - Web: Uses @atlaskit/pragmatic-drag-and-drop dragHandle option - Native: Uses context to share pan gesture with DragHandle component - Updates columns-form to use DragHandle on grip icon
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📝 WalkthroughWalkthroughThis PR adds a DragHandle component and useDragHandle hook, exposes them from dnd entry points, and implements context-based drag-handle registration and handling in both web and native SortableItem implementations. Changes
Sequence Diagram(s)sequenceDiagram
participant Consumer
participant DragHandle
participant Context as DragHandleContext
participant SortableItem
participant Gesture as GestureDetector
Consumer->>+DragHandle: render (attach ref / mount)
DragHandle->>+Context: call registerDragHandle(element)
Context-->>-DragHandle: store handle ref
Note over SortableItem,Context: SortableItem provides context and tracks hasDragHandle
Consumer->>+SortableItem: user initiates pan on handle
alt hasDragHandle (native)
SortableItem->>+Gesture: GestureDetector attached handles pan
Gesture-->>-SortableItem: emit drag events
else (web or no handle)
SortableItem->>SortableItem: full-item draggable logic starts
end
SortableItem-->>-Consumer: item reorder processed
sequenceDiagram
participant Consumer
participant DragHandle
participant Hook as useDragHandle
participant Context as DragHandleContext
participant Draggable
Consumer->>+Hook: call useDragHandle()
Hook->>+Context: consume registerDragHandle
Hook-->>-Consumer: return dragHandleRef and dragHandleProps
Consumer->>+DragHandle: mount, attach ref callback
DragHandle->>+Hook: invoke dragHandleRef(element)
Hook->>+Context: register handle element
Context-->>-Hook: stored
Consumer->>+Draggable: user drags handle element
Draggable->>+SortableItem: drag start with handle
SortableItem-->>-Draggable: handle drag and reorder
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 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 |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/components/src/dnd/web/sortable-item.tsx (1)
90-104: Potential timing issue with drag handle registration.The
draggableeffect runs on mount and readsdragHandleRef.currentat that time (line 99). IfDragHandlemounts after this effect runs (which is the typical React render order - parent effects run before children mount),dragHandleRef.currentwill benull, and the entire element becomes draggable instead of just the handle.Consider adding
dragHandleRef.currentto a state or using a different synchronization mechanism to re-run the effect when the drag handle is registered.🐛 Suggested approach
One fix is to track registration in state to trigger effect re-run:
export function SortableItem({ ... }: SortableItemProps) { const ref = useRef<HTMLDivElement | null>(null); - const dragHandleRef = useRef<HTMLElement | null>(null); + const [dragHandle, setDragHandle] = React.useState<HTMLElement | null>(null); const [state, setState] = useState<DragState>(idle); ... // Callback for drag handle registration - const registerDragHandle = useCallback((element: HTMLElement | null) => { - dragHandleRef.current = element; - }, []); + const registerDragHandle = React.useCallback((element: HTMLElement | null) => { + setDragHandle(element); + }, []); ... - // Use drag handle if registered, otherwise the entire element is draggable - const dragHandle = dragHandleRef.current ?? undefined; + // Use drag handle if registered, otherwise the entire element is draggable + const handle = dragHandle ?? undefined; return combine( draggable({ element, - dragHandle, + dragHandle: handle, ... }), ... ); - }, [id, listId, axis, disabled]); + }, [id, listId, axis, disabled, dragHandle]);
🤖 Fix all issues with AI agents
In `@packages/components/src/dnd/web/sortable-item.tsx`:
- Around line 1-10: The file currently destructures React exports in the import
line (createContext, useCallback, useContext, useEffect, useRef, useState, plus
types HTMLAttributes/ReactNode); replace that with a single namespace import
(import * as React from 'react' or import React from 'react' per project style)
and update all occurrences: createContext → React.createContext, useCallback →
React.useCallback, useContext → React.useContext, useEffect → React.useEffect,
useRef → React.useRef, useState → React.useState while keeping the type imports
(HTMLAttributes, ReactNode) referenced via React (e.g., React.HTMLAttributes,
React.ReactNode) if required by your type style.
🧹 Nitpick comments (1)
packages/components/src/dnd/native/sortable-item.tsx (1)
352-356: Consider includingsetHasDragHandlein the dependency array for completeness.While
setHasDragHandleis a stable React setState function and won't cause issues, including it in the dependency array aligns with exhaustive-deps best practices and makes the intent clearer.♻️ Suggested fix
const dragHandleContextValue = React.useMemo( () => ({ panGesture, setHasDragHandle }), - [panGesture] + [panGesture, setHasDragHandle] );
Use React.useState, React.useRef, etc. instead of destructured imports.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@packages/components/src/dnd/web/sortable-item.tsx`:
- Around line 83-98: The effect in SortableItem captures dragHandleRef.current
once (inside the React.useEffect that calls draggable and combine) so changes to
the handle after mount won’t rebind; update the effect to re-run when the drag
handle changes by including a handle-dependent value in the dependency array or
by subscribing to register/unregister events from registerDragHandle so the
draggable binding is recreated when dragHandleRef changes; specifically, modify
the React.useEffect that calls draggable({ element, dragHandle, ... }) (and
returns combine(...)) to depend on the current drag handle (e.g., a stable
handleId or dragHandleRef.current) or trigger a rebind when registerDragHandle
updates, ensuring draggable is re-initialized with the new dragHandle.
🧹 Nitpick comments (1)
packages/components/src/dnd/web/sortable-item.tsx (1)
226-250: PreferclassNameover inline style for the cursor.
Inline styles violate the styling guideline and make theme handling harder.♻️ Suggested refactor
return { dragHandleRef, dragHandleProps: { - style: { cursor: 'grab' }, + className: 'cursor-grab', }, }; } @@ return ( - <div ref={dragHandleRef} className={className} style={dragHandleProps.style}> + <div + ref={dragHandleRef} + className={`${dragHandleProps.className ?? ''} ${className}`.trim()} + > {children} </div> ); }As per coding guidelines, prefer
classNameover inline styles.
Replace deprecated runOnUI with scheduleOnUI from react-native-worklets. Also includes React namespace import convention fixes.
There was a problem hiding this comment.
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/components/src/dnd/web/context.tsx (1)
14-20: Error message refers to wrong function name.The error message says
useSortableContext must be used within a SortableListbut the function is nameduseDndContext. This inconsistency could confuse developers debugging context issues.🐛 Proposed fix
export function useDndContext(): SortableContextValue { const context = React.useContext(SortableContext); if (!context) { - throw new Error('useSortableContext must be used within a SortableList'); + throw new Error('useDndContext must be used within a SortableList'); } return context; }
♻️ Duplicate comments (1)
packages/components/src/dnd/web/sortable-item.tsx (1)
73-75: **Drag handle changes after mount won't rebind the draggable.**ThedragHandleoption indraggable()is captured once when the effect runs.draggableis called during effect setup and returns a cleanup function. SinceregisterDragHandleonly setsdragHandleRef.currentwithout triggering the effect to re-run, dynamic handle registration (e.g., aDragHandlemounted afterSortableItem) won't take effect.Currently safe in the codebase (handles are statically rendered in
columns-form.tsx), but this is fragile for dynamic scenarios.🔧 Suggested fix: Trigger rebind when handle changes
+ // Track handle registration version to trigger rebind + const [handleVersion, setHandleVersion] = React.useState(0); + // Callback for drag handle registration const registerDragHandle = React.useCallback((element: HTMLElement | null) => { dragHandleRef.current = element; + // Trigger effect re-run when handle is registered/unregistered + setHandleVersion((v) => v + 1); }, []); // Setup drag and drop behavior - required to attach pragmatic-drag-and-drop to DOM React.useEffect(() => { // ... existing setup code ... - }, [id, listId, axis, disabled]); + }, [id, listId, axis, disabled, handleVersion]);Also applies to: 84-98
🧹 Nitpick comments (4)
packages/components/src/dnd/web/drop-indicator.tsx (1)
42-47: Consider semantic color for theme compatibility (optional).The drop indicator uses hardcoded
bg-blue-700andborder-blue-700. While this is likely intentional to ensure visibility (matching Atlaskit's original styling), consider using a semantic color class (e.g.,bg-primaryor a dedicatedbg-drop-indicator) for better theme compatibility if the design system supports it.This is a minor consideration since drop indicators often intentionally use a distinct, fixed color for consistent visibility.
packages/components/src/dnd/native/context.tsx (1)
85-98: Consider enhancing the useEffect comment to explain WHY it's necessary.Per coding guidelines, useEffect comments should explain WHY it's necessary, not just what it does. The current comment describes the action but could clarify why an effect is required here (e.g., "useEffect required because shared values must be updated on UI thread in response to external prop changes, which cannot be done during render").
The implementation itself is sound—passing
newPositionsas an argument toscheduleOnUIrather than capturing it in the worklet closure is the correct approach.packages/components/src/dnd/web/sortable-list.tsx (1)
41-116: Add a comment explaining WHYuseEffectis necessary.Per coding guidelines, when using
useEffect, add a comment explaining why it is necessary. The effect synchronizes with an external system (monitorForElements), which is a valid use case, but the reasoning should be documented.♻️ Suggested fix
- React.useEffect(() => { + // Effect required: Subscribe to pragmatic-drag-and-drop monitor for cross-item drag events. + // This external subscription cannot be handled during render or via event handlers. + React.useEffect(() => {packages/components/src/dnd/web/sortable-item.tsx (1)
239-253: PreferclassNameover inlinestylefor the cursor.Per coding guidelines, prefer
classNameover inline styles to ensure proper theme switching and avoid CSS variable lag. Thecursor: grabcould be moved to a utility class.♻️ Suggested fix
export function DragHandle({ children, className = '', }: { children: React.ReactNode; className?: string; }) { const { dragHandleRef, dragHandleProps } = useDragHandle(); return ( - <div ref={dragHandleRef} className={className} style={dragHandleProps.style}> + <div ref={dragHandleRef} className={`cursor-grab ${className}`}> {children} </div> ); }And update
useDragHandleto remove the style fromdragHandleProps:return { dragHandleRef, - dragHandleProps: { - style: { cursor: 'grab' }, - }, + dragHandleProps: {}, };
- Add containment check to prevent pragmatic-drag-and-drop warning - Fall back to whole-element dragging if handle is not properly contained - Add JSDoc documentation explaining the architecture and usage - Document that DragHandle must be a descendant of SortableItem children
Summary
DragHandlecomponent that restricts drag operations to only trigger when dragging from a specific element, rather than the entire sortable item@atlaskit/pragmatic-drag-and-dropdragHandle optionUsage
Test plan
Summary by CodeRabbit
New Features
Bug Fixes / UX
Improvements
✏️ Tip: You can customize this high-level summary in your review settings.