Skip to content

feat(dnd): add DragHandle component for explicit drag initiation#11

Merged
kilbot merged 4 commits intomainfrom
feature/dnd-drag-handle
Jan 22, 2026
Merged

feat(dnd): add DragHandle component for explicit drag initiation#11
kilbot merged 4 commits intomainfrom
feature/dnd-drag-handle

Conversation

@kilbot
Copy link
Copy Markdown
Contributor

@kilbot kilbot commented Jan 22, 2026

Summary

  • Adds a DragHandle component that restricts drag operations to only trigger when dragging from a specific element, rather than the entire sortable item
  • Web implementation uses @atlaskit/pragmatic-drag-and-drop dragHandle option
  • Native implementation uses context to share pan gesture with DragHandle component
  • Updates columns-form to use DragHandle on grip icon

Usage

import { SortableList, DragHandle } from '@wcpos/components/dnd';

<SortableList
  listId="my-list"
  items={items}
  getItemId={(item) => item.id}
  onOrderChange={handleOrderChange}
  renderItem={(item) => (
    <div>
      <DragHandle className="cursor-grab">
        <Icon name="gripLinesVertical" />
      </DragHandle>
      <span>{item.name}</span>
    </div>
  )}
/>

Test plan

  • Verify dragging only works from the grip icon on web
  • Verify dragging only works from the grip icon on native
  • Verify existing SortableList usage (without DragHandle) still works

Summary by CodeRabbit

  • New Features

    • Added a dedicated drag handle so items can be dragged from a specific handle rather than the whole item.
    • Exposed a hook to attach drag-handle behavior (web + native) for custom handle components.
  • Bug Fixes / UX

    • Wrapped existing grip icon with the new drag-handle while preserving appearance and cursor.
  • Improvements

    • Drop indicator now supports the left edge for more accurate placement feedback.

✏️ Tip: You can customize this high-level summary in your review settings.

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
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jan 22, 2026

Warning

Rate limit exceeded

@kilbot has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 7 minutes and 53 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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.

📝 Walkthrough

Walkthrough

This 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

Cohort / File(s) Summary
Public API / Entry points
packages/components/src/dnd/index.ts, packages/components/src/dnd/index.web.ts, packages/components/src/dnd/native/index.ts
Export DragHandle and useDragHandle from platform-specific modules; remove local useDragHandle implementation from main index in favor of re-exports.
Web SortableItem & related
packages/components/src/dnd/web/sortable-item.tsx, packages/components/src/dnd/web/context.tsx, packages/components/src/dnd/web/drop-indicator.tsx, packages/components/src/dnd/web/sortable-list.tsx
Add DragHandleContext, registration flow, DragHandle component and update useDragHandle (returns dragHandleRef + props); switch internal imports to React.*; add left edge support in drop indicator.
Native SortableItem & related
packages/components/src/dnd/native/sortable-item.tsx, packages/components/src/dnd/native/index.ts, packages/components/src/dnd/native/context.tsx, packages/components/src/dnd/native/types.ts
Add native DragHandle component, useDragHandle hook, and DragHandleContext; SortableItem tracks hasDragHandle and conditionally wraps content with GestureDetector; replace runOnUI with scheduleOnUI worklet wrappers.
Types
packages/components/src/dnd/types/sortable.ts
Change ReactNode imports to namespace (React.ReactNode) in public type signatures for SortableListProps and SortableItemProps.
Consumers / Usage
packages/core/src/screens/main/components/ui-settings/columns-form.tsx
Wrap existing grip icon with new DragHandle component and adjust related styling class placement.

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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I nibble at code, a tiny handle appears,
Context threads softly between web and native ears,
A tap, a pull, the list rearranged with care,
Grip now small and precise, not the whole item to bear,
Hop, drag, reorder — I dance through the air!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(dnd): add DragHandle component for explicit drag initiation' directly and accurately describes the main change in the pull request—the introduction of a DragHandle component that restricts drag initiation to specific elements.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 draggable effect runs on mount and reads dragHandleRef.current at that time (line 99). If DragHandle mounts after this effect runs (which is the typical React render order - parent effects run before children mount), dragHandleRef.current will be null, and the entire element becomes draggable instead of just the handle.

Consider adding dragHandleRef.current to 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 including setHasDragHandle in the dependency array for completeness.

While setHasDragHandle is 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.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: Prefer className over 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 className over inline styles.

Replace deprecated runOnUI with scheduleOnUI from react-native-worklets.
Also includes React namespace import convention fixes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a 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/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 SortableList but the function is named useDndContext. 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.**The dragHandle option in draggable() is captured once when the effect runs. draggable is called during effect setup and returns a cleanup function. Since registerDragHandle only sets dragHandleRef.current without triggering the effect to re-run, dynamic handle registration (e.g., a DragHandle mounted after SortableItem) 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-700 and border-blue-700. While this is likely intentional to ensure visibility (matching Atlaskit's original styling), consider using a semantic color class (e.g., bg-primary or a dedicated bg-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 newPositions as an argument to scheduleOnUI rather 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 WHY useEffect is 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: Prefer className over inline style for the cursor.

Per coding guidelines, prefer className over inline styles to ensure proper theme switching and avoid CSS variable lag. The cursor: grab could 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 useDragHandle to remove the style from dragHandleProps:

 	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
@kilbot kilbot merged commit 8665f98 into main Jan 22, 2026
3 checks passed
@kilbot kilbot deleted the feature/dnd-drag-handle branch January 22, 2026 19:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant