Skip to content

Comments

feat: introduce Sortable component with DnD capabilities and Storybook examples#54

Merged
Aunshon merged 5 commits intomainfrom
add/sortable-component
Feb 19, 2026
Merged

feat: introduce Sortable component with DnD capabilities and Storybook examples#54
Aunshon merged 5 commits intomainfrom
add/sortable-component

Conversation

@Aunshon
Copy link
Collaborator

@Aunshon Aunshon commented Feb 17, 2026

Summary by CodeRabbit

  • New Features
    • Added sortable list component with drag-and-drop support for multiple layouts (vertical, horizontal, grid)
    • Includes customizable drag handles, visual drag overlays, and cross-list reordering
    • Supports inline editing and per-item actions

…k examples

- Added `Sortable`, `SortableItem`, `SortableDragHandle`, `SortableProvider`, and `DragOverlay` components using `@dnd-kit` for drag-and-drop functionality.
- Integrated examples for vertical, horizontal, grid, drag handle, multiple lists, and custom UI in `Sortable.stories.tsx` within Storybook.
- Exported the new components in `ui/index.ts` for external use.
- Updated `package.json` to include `@dnd-kit` dependencies.
@Aunshon Aunshon self-assigned this Feb 17, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a drag-and-drop sortable component system powered by dnd-kit. It includes core components (Sortable, SortableItem, SortableDragHandle, SortableProvider), comprehensive Storybook documentation with eight usage examples, and updates to package dependencies and public API exports.

Changes

Cohort / File(s) Summary
Drag-and-Drop Dependencies
package.json
Added @dnd-kit family packages (@dnd-kit/core, @dnd-kit/modifiers, @dnd-kit/react, @dnd-kit/sortable, @dnd-kit/utilities) and autoprefixer to dependencies.
Sortable Implementation
src/components/ui/sortable.tsx
Implemented complete drag-and-drop sortable system with Sortable container component, SortableItem wrapper, SortableDragHandle activator, SortableProvider context wrapper, and internal SortableItemContext for drag state management. Includes sensor configuration, collision detection, array reordering on drop, and optional overlay rendering.
Component Exports
src/components/ui/index.ts, src/index.ts
Added public exports for Sortable, SortableItem, SortableDragHandle, SortableProvider, DragOverlay, and their prop types; removed PopoverClose and PopoverArrow; added PopoverHeader to Popover exports.
Storybook Documentation
src/components/ui/Sortable.stories.tsx
Added eight comprehensive story examples (AdvancedCustomUI, Vertical, WithDragHandle, Horizontal, Grid, CustomGapsAndPadding, DragOverlayExample, MultipleLists) demonstrating different sorting strategies, drag handles, cross-list behavior, and overlay customization.

Sequence Diagram

sequenceDiagram
    actor User
    participant SortableItem
    participant DndContext as DnD Context<br/>(Sensors & Collision)
    participant Sortable
    participant DragOverlay
    
    User->>SortableItem: Activate drag via<br/>SortableDragHandle
    SortableItem->>DndContext: Register drag start<br/>(activeId set)
    DndContext->>Sortable: Notify drag active
    Sortable->>DragOverlay: Render overlay for<br/>active item
    User->>SortableItem: Drag over items
    DndContext->>Sortable: Track drag position<br/>(collision detection)
    User->>DndContext: Drop item
    DndContext->>Sortable: Trigger drag end
    Sortable->>Sortable: Calculate new order<br/>(arrayMove)
    Sortable->>Sortable: Invoke onValueChange<br/>& onMove callbacks
    Sortable->>DragOverlay: Clear overlay
    SortableItem->>User: Render reordered items
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 With dnd-kit's magic, items now dance,
Dragging and sorting in harmonious trance,
Storybook shows each configuration with flair,
From grids to lists floating through air,
A sortable symphony, beyond compare!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title directly and comprehensively describes the main change: introducing a Sortable component with drag-and-drop capabilities and Storybook examples, which aligns perfectly with all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch add/sortable-component

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

@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: 3

🧹 Nitpick comments (6)
src/components/ui/sortable.tsx (3)

1-1: "use strict" is unnecessary in TypeScript/ESM files.

TypeScript modules and ES modules are always in strict mode. This directive is a no-op here.

🧹 Remove unnecessary directive
-"use strict";
-
 import * as React from "react";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/sortable.tsx` at line 1, Remove the unnecessary "use
strict" directive at the top of this module: delete the standalone string
literal "use strict" from src/components/ui/sortable.tsx since TypeScript/ESM
modules are already strict; no other changes required.

232-250: SortableProvider doesn't expose onDragEnd/onDragStart handling — document the intended usage.

SortableProvider is a bare DndContext wrapper with no built-in state management for active items or reordering. This is presumably intended for advanced use cases (e.g., cross-list dragging), but there are no stories or documentation showing how a consumer would wire it up. A doc comment or a corresponding Storybook story using SortableProvider with <Sortable container={false}> would help clarify the intended pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/sortable.tsx` around lines 232 - 250, SortableProvider
currently wraps DndContext but doesn't document or demonstrate how consumers
should handle drag lifecycle (onDragEnd/onDragStart) or manage active
item/reordering state; add a short doc comment above the SortableProvider
function explaining that it intentionally forwards DndContext props and that
consumers must supply handlers like onDragEnd/onDragStart (and maintain item
order/active id) for reordering or cross-list behavior, and add a Storybook
story demonstrating usage (e.g., using SortableProvider with sensors prop and a
consumer-managed onDragEnd that updates an array, and an example using <Sortable
container={false}> for advanced patterns) so users can see the expected wiring.

36-49: Consider documenting the onMove vs onValueChange distinction.

The props expose both onMove (called with the raw DragEndEvent) and onValueChange (called with the reordered array). Their relationship isn't immediately obvious — onMove fires on every drag end regardless of whether a reorder occurred (line 95), while onValueChange only fires when items actually swap. A JSDoc comment on onMove would help consumers understand the contract.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/sortable.tsx` around lines 36 - 49, Add a JSDoc comment to
the onMove prop in the SortableProps interface clarifying its contract: state
that onMove receives the raw DragEndEvent (type DragEndEvent) and is invoked on
every drag end regardless of whether items changed, while onValueChange is only
called when the items array actually reorders; mention that consumers can use
onMove for low-level event handling and onValueChange for finalized reordered
data. Reference the SortableProps interface, onMove, onValueChange, and
DragEndEvent so readers can find the relevant types and behavior.
src/components/ui/Sortable.stories.tsx (1)

4-8: Consider re-exporting sorting strategies from the sortable module.

Stories (and consumers) currently import verticalListSortingStrategy, horizontalListSortingStrategy, and rectSortingStrategy directly from @dnd-kit/sortable. For a cleaner DX, re-export these from your sortable.tsx module so consumers don't need to add @dnd-kit/sortable as a direct dependency or know the underlying library.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/Sortable.stories.tsx` around lines 4 - 8, Update your
sortable.tsx module to re-export verticalListSortingStrategy,
horizontalListSortingStrategy, and rectSortingStrategy from "@dnd-kit/sortable"
(e.g., export { verticalListSortingStrategy, horizontalListSortingStrategy,
rectSortingStrategy } from "@dnd-kit/sortable"), and then change imports in
Sortable.stories.tsx to import these strategies from your sortable module
instead of directly from "@dnd-kit/sortable" so consumers can import strategies
via your component module.
src/components/ui/popover.tsx (1)

15-17: Inconsistent export style for PopoverPortal.

PopoverPortal uses an inline export function declaration while all other components in this file are declared without export and then exported together in the named export block at the bottom (Lines 87–94). Consider moving PopoverPortal into the bottom export block for consistency.

♻️ Suggested fix
-export function PopoverPortal({ ...props }: PopoverPrimitive.Portal.Props) {
+function PopoverPortal({ ...props }: PopoverPrimitive.Portal.Props) {
   return <PopoverPrimitive.Portal data-slot="popover-portal" {...props} />;
 }

Then add it to the bottom export (which it already is in index.ts, but keep the source consistent):

 export {
   Popover,
   PopoverContent,
   PopoverDescription,
   PopoverHeader,
+  PopoverPortal,
   PopoverTitle,
   PopoverTrigger,
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/popover.tsx` around lines 15 - 17, PopoverPortal is
declared with an inline export while the rest of the components are declared
without export and then exported together; change PopoverPortal to a
non-exported function declaration (function PopoverPortal(props:
PopoverPrimitive.Portal.Props) { ... }) so it matches the other components, and
ensure its identifier PopoverPortal is included in the named export block at the
bottom alongside the other components; keep the existing JSX
(<PopoverPrimitive.Portal data-slot="popover-portal" {...props} />) and type
(PopoverPrimitive.Portal.Props) intact.
src/index.ts (1)

146-151: Re-exporting DragOverlay directly from dnd-kit couples the public API to the library.

DragOverlay is a raw dnd-kit component re-exported as part of this package's public surface. If dnd-kit's API changes in a future version, it becomes a breaking change for your consumers. Consider wrapping it in a thin component (similar to how Sortable wraps DndContext) so you own the API contract.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.ts` around lines 146 - 151, The public export currently re-exports
DragOverlay from dnd-kit (symbol DragOverlay in src/index.ts) which couples our
API to the external library; create a thin wrapper component (e.g.,
OurDragOverlay or SortableDragOverlay) that imports DragOverlay from
'@dnd-kit/core', forwards props/children and any refs, and export that wrapper
from the module instead of re-exporting DragOverlay directly; update the export
list (replace DragOverlay with the new wrapper name), add matching TypeScript
types/exports for the wrapper, and adjust any internal imports/tests to use the
new wrapper so consumers rely on our stable API surface.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@package.json`:
- Around line 60-64: Remove the unused and incompatible dependency entry
"@dnd-kit/react": "^0.3.0" from package.json (it's not imported anywhere and
conflicts with the legacy dnd-kit packages like "@dnd-kit/core" and
"@dnd-kit/sortable"); delete that line from the dependencies block, then update
your lockfile by reinstalling (run npm install or yarn install) so the lockfile
and node_modules are consistent.

In `@src/components/ui/Sortable.stories.tsx`:
- Around line 319-362: The MultipleLists story uses two independent Sortable
components (each with its own DndContext) so items cannot be dragged between
lists; either change the implementation to share a single
SortableProvider/DndContext around both lists to enable cross-list dragging
(wrap both lists with a common SortableProvider or move a single DndContext to
encompass both Sortable instances and coordinate on a shared onValueChange
handler for itemsA/itemsB and SortableItem ids), or if cross-list dragging is
not desired, rename MultipleLists to IndependentLists or SideBySideLists to make
behavior clear; update the story accordingly (referencing MultipleLists,
Sortable, SortableProvider/DndContext, SortableItem, itemsA/itemsB,
setItemsA/setItemsB).

In `@src/components/ui/sortable.tsx`:
- Around line 80-109: The DndContext handlers are being overridden by the
trailing {...props}; to fix, destructure onDragStart, onDragEnd, and
onDragCancel from the component props (keep onValueChange, onMove, etc.), then
compose them inside the DndContext: call your internal logic (setActiveId,
arrayMove + onValueChange, setActiveId(null), onMove) and after that call the
user-supplied handlers (the destructured onDragStart/onDragEnd/onDragCancel)
with the same event; finally spread the remaining props (the rest) into
DndContext so user props cannot silently override the internal handlers. Ensure
references to DndContext, onDragEnd, onDragStart, onDragCancel, setActiveId,
onMove, and onValueChange are updated accordingly.

---

Nitpick comments:
In `@src/components/ui/popover.tsx`:
- Around line 15-17: PopoverPortal is declared with an inline export while the
rest of the components are declared without export and then exported together;
change PopoverPortal to a non-exported function declaration (function
PopoverPortal(props: PopoverPrimitive.Portal.Props) { ... }) so it matches the
other components, and ensure its identifier PopoverPortal is included in the
named export block at the bottom alongside the other components; keep the
existing JSX (<PopoverPrimitive.Portal data-slot="popover-portal" {...props} />)
and type (PopoverPrimitive.Portal.Props) intact.

In `@src/components/ui/Sortable.stories.tsx`:
- Around line 4-8: Update your sortable.tsx module to re-export
verticalListSortingStrategy, horizontalListSortingStrategy, and
rectSortingStrategy from "@dnd-kit/sortable" (e.g., export {
verticalListSortingStrategy, horizontalListSortingStrategy, rectSortingStrategy
} from "@dnd-kit/sortable"), and then change imports in Sortable.stories.tsx to
import these strategies from your sortable module instead of directly from
"@dnd-kit/sortable" so consumers can import strategies via your component
module.

In `@src/components/ui/sortable.tsx`:
- Line 1: Remove the unnecessary "use strict" directive at the top of this
module: delete the standalone string literal "use strict" from
src/components/ui/sortable.tsx since TypeScript/ESM modules are already strict;
no other changes required.
- Around line 232-250: SortableProvider currently wraps DndContext but doesn't
document or demonstrate how consumers should handle drag lifecycle
(onDragEnd/onDragStart) or manage active item/reordering state; add a short doc
comment above the SortableProvider function explaining that it intentionally
forwards DndContext props and that consumers must supply handlers like
onDragEnd/onDragStart (and maintain item order/active id) for reordering or
cross-list behavior, and add a Storybook story demonstrating usage (e.g., using
SortableProvider with sensors prop and a consumer-managed onDragEnd that updates
an array, and an example using <Sortable container={false}> for advanced
patterns) so users can see the expected wiring.
- Around line 36-49: Add a JSDoc comment to the onMove prop in the SortableProps
interface clarifying its contract: state that onMove receives the raw
DragEndEvent (type DragEndEvent) and is invoked on every drag end regardless of
whether items changed, while onValueChange is only called when the items array
actually reorders; mention that consumers can use onMove for low-level event
handling and onValueChange for finalized reordered data. Reference the
SortableProps interface, onMove, onValueChange, and DragEndEvent so readers can
find the relevant types and behavior.

In `@src/index.ts`:
- Around line 146-151: The public export currently re-exports DragOverlay from
dnd-kit (symbol DragOverlay in src/index.ts) which couples our API to the
external library; create a thin wrapper component (e.g., OurDragOverlay or
SortableDragOverlay) that imports DragOverlay from '@dnd-kit/core', forwards
props/children and any refs, and export that wrapper from the module instead of
re-exporting DragOverlay directly; update the export list (replace DragOverlay
with the new wrapper name), add matching TypeScript types/exports for the
wrapper, and adjust any internal imports/tests to use the new wrapper so
consumers rely on our stable API surface.

package.json Outdated
Comment on lines 60 to 64
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/react": "^0.3.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

dnd-kit npm packages @dnd-kit/core @dnd-kit/sortable @dnd-kit/react compatibility 2025 2026

💡 Result:

Compatibility (2025–2026)

1) “Legacy” dnd-kit (stable)

  • Use @dnd-kit/core + @dnd-kit/sortable together (Sortable is built on top of Core). [1][2]
  • Latest visible package versions (as of the sources crawled): @dnd-kit/core 6.3.1 and @dnd-kit/sortable 10.0.0. [3][2]

2) “New” dnd-kit (rewrite)

  • @dnd-kit/react is the new React API layer (published as 0.x, e.g. 0.1.21 in the npm listing snippet). [4]
  • The official migration guide says to migrate from @dnd-kit/core/@dnd-kit/sortable/@dnd-kit/utilities to @dnd-kit/react (and @dnd-kit/helpers), i.e. it’s a replacement track, not an add-on. [5]
  • The legacy docs explicitly note there’s a new version and recommend using the latest instead of the legacy install path. [6]

Practical takeaway

  • Don’t mix @dnd-kit/react (new 0.x line) with the legacy @dnd-kit/core / @dnd-kit/sortable stack in the same implementation—pick one track:
    • Legacy: @dnd-kit/core + @dnd-kit/sortable [1][2]
    • New: @dnd-kit/react (+ @dnd-kit/helpers, plus other new-scope packages as needed) [5][4]

Sources: [1] @dnd-kit Sortable docs [2] Snyk package info for @dnd-kit/sortable [3] npm listing snippet for @dnd-kit/core [4] npm listing snippet for @dnd-kit/react [5] official migration guide (core → react) [6] legacy installation page noting a new version is available.


🏁 Script executed:

# Find and examine sortable.tsx to see which APIs are actually imported
fd -e tsx -e ts | xargs rg -l "sortable.tsx|useSortable|SortableContext" | head -5

Repository: getdokan/plugin-ui

Length of output: 92


🏁 Script executed:

# Also check the actual package.json to see the full context
cat -n package.json | sed -n '50,75p'

Repository: getdokan/plugin-ui

Length of output: 1084


🏁 Script executed:

cat -n src/components/ui/sortable.tsx | head -50

Repository: getdokan/plugin-ui

Length of output: 1696


🏁 Script executed:

# Search for imports from dnd-kit packages
rg "from ['\"]@dnd-kit" src/components/ui/sortable.tsx -A 1 -B 1

Repository: getdokan/plugin-ui

Length of output: 270


🏁 Script executed:

rg "@dnd-kit/react" --type ts --type tsx

Repository: getdokan/plugin-ui

Length of output: 89


🏁 Script executed:

rg "@dnd-kit/react" --type typescript

Repository: getdokan/plugin-ui

Length of output: 96


🏁 Script executed:

# Also try searching without type restriction
rg "@dnd-kit/react" src/

Repository: getdokan/plugin-ui

Length of output: 44


Remove unused @dnd-kit/react dependency — incompatible with the legacy dnd-kit stack in use.

The codebase uses the legacy dnd-kit track (@dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities), which is explicitly incompatible with @dnd-kit/react. The latter is a replacement API from a complete dnd-kit rewrite (available as 0.x pre-release) and should not be mixed with the legacy packages. A search of the entire codebase confirms @dnd-kit/react is never imported or used anywhere—only the legacy packages are referenced in sortable.tsx. Remove @dnd-kit/react@^0.3.0 from dependencies.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 60 - 64, Remove the unused and incompatible
dependency entry "@dnd-kit/react": "^0.3.0" from package.json (it's not imported
anywhere and conflicts with the legacy dnd-kit packages like "@dnd-kit/core" and
"@dnd-kit/sortable"); delete that line from the dependencies block, then update
your lockfile by reinstalling (run npm install or yarn install) so the lockfile
and node_modules are consistent.

Comment on lines +319 to +362
export const MultipleLists: Story = {
render: () => {
function Demo() {
const [itemsA, setItemsA] = useState([
{ id: "A1", content: "A1" },
{ id: "A2", content: "A2" },
]);
const [itemsB, setItemsB] = useState([
{ id: "B1", content: "B1" },
{ id: "B2", content: "B2" },
]);

return (
<div className="flex gap-8 p-4">
<div className="w-48 space-y-4">
<h3 className="font-bold">List A</h3>
<Sortable items={itemsA} onValueChange={setItemsA}>
<div className="min-h-[100px] rounded-lg border bg-muted/50 p-2 space-y-2">
{itemsA.map(item => (
<SortableItem key={item.id} id={item.id}>
<div className="rounded border bg-card p-2 shadow-sm text-center">{item.content}</div>
</SortableItem>
))}
</div>
</Sortable>
</div>
<div className="w-48 space-y-4">
<h3 className="font-bold">List B</h3>
<Sortable items={itemsB} onValueChange={setItemsB}>
<div className="min-h-[100px] rounded-lg border bg-muted/50 p-2 space-y-2">
{itemsB.map(item => (
<SortableItem key={item.id} id={item.id}>
<div className="rounded border bg-card p-2 shadow-sm text-center">{item.content}</div>
</SortableItem>
))}
</div>
</Sortable>
</div>
</div>
);
}
return <Demo />;
},
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

MultipleLists story doesn't demonstrate cross-list dragging.

Each list has its own independent <Sortable> (and thus its own DndContext), so items can only be reordered within each list, not dragged between them. If cross-list DnD is intended, both lists would need to share a single SortableProvider. If the intent is just "two independent sortable lists on one page," consider renaming to something like IndependentLists or SideBySideLists to set correct expectations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/Sortable.stories.tsx` around lines 319 - 362, The
MultipleLists story uses two independent Sortable components (each with its own
DndContext) so items cannot be dragged between lists; either change the
implementation to share a single SortableProvider/DndContext around both lists
to enable cross-list dragging (wrap both lists with a common SortableProvider or
move a single DndContext to encompass both Sortable instances and coordinate on
a shared onValueChange handler for itemsA/itemsB and SortableItem ids), or if
cross-list dragging is not desired, rename MultipleLists to IndependentLists or
SideBySideLists to make behavior clear; update the story accordingly
(referencing MultipleLists, Sortable, SortableProvider/DndContext, SortableItem,
itemsA/itemsB, setItemsA/setItemsB).

Comment on lines +80 to +109
return (
<DndContext
sensors={sensors}
collisionDetection={collisionDetection}
onDragStart={(event) => setActiveId(event.active.id)}
onDragEnd={(event) => {
const { active, over } = event;
if (over && active.id !== over.id) {
const oldIndex = items.findIndex((item) => item.id === active.id);
const newIndex = items.findIndex((item) => item.id === over.id);

if (oldIndex !== -1 && newIndex !== -1) {
onValueChange?.(arrayMove(items, oldIndex, newIndex));
}
}
onMove?.(event);
setActiveId(null);
}}
onDragCancel={() => setActiveId(null)}
{...props}
>
{content}
{overlay && (
<DragOverlay>
{activeId ? overlay : null}
</DragOverlay>
)}
</DndContext>
);
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

{...props} spread after inline handlers will silently override internal drag logic.

SortableProps extends DndContextProps, so onDragStart, onDragEnd, and onDragCancel are not destructured out and remain in ...props. Since {...props} is spread after the explicit handlers (line 99), React uses the last value — meaning a consumer passing onDragEnd will completely bypass the internal arrayMove reordering logic.

Move {...props} before the inline handlers, or destructure onDragStart/onDragEnd/onDragCancel from props and compose them with the internal logic.

🐛 Proposed fix — destructure and compose handlers
 const Sortable = <TData extends { id: UniqueIdentifier }>({
   items,
   onValueChange,
   onMove,
   strategy = verticalListSortingStrategy,
   children,
   overlay,
   sensors: propSensors,
   collisionDetection = closestCenter,
   container = true,
+  onDragStart: onDragStartProp,
+  onDragEnd: onDragEndProp,
+  onDragCancel: onDragCancelProp,
   ...props
 }: SortableProps<TData>) => {
   const [activeId, setActiveId] = React.useState<UniqueIdentifier | null>(null);
   ...

   return (
     <DndContext
       sensors={sensors}
       collisionDetection={collisionDetection}
-      onDragStart={(event) => setActiveId(event.active.id)}
-      onDragEnd={(event) => {
+      onDragStart={(event) => {
+        setActiveId(event.active.id);
+        onDragStartProp?.(event);
+      }}
+      onDragEnd={(event) => {
         const { active, over } = event;
         if (over && active.id !== over.id) {
           const oldIndex = items.findIndex((item) => item.id === active.id);
           const newIndex = items.findIndex((item) => item.id === over.id);

           if (oldIndex !== -1 && newIndex !== -1) {
             onValueChange?.(arrayMove(items, oldIndex, newIndex));
           }
         }
         onMove?.(event);
         setActiveId(null);
+        onDragEndProp?.(event);
+      }}
+      onDragCancel={(event) => {
+        setActiveId(null);
+        onDragCancelProp?.(event);
       }}
-      onDragCancel={() => setActiveId(null)}
       {...props}
     >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/sortable.tsx` around lines 80 - 109, The DndContext
handlers are being overridden by the trailing {...props}; to fix, destructure
onDragStart, onDragEnd, and onDragCancel from the component props (keep
onValueChange, onMove, etc.), then compose them inside the DndContext: call your
internal logic (setActiveId, arrayMove + onValueChange, setActiveId(null),
onMove) and after that call the user-supplied handlers (the destructured
onDragStart/onDragEnd/onDragCancel) with the same event; finally spread the
remaining props (the rest) into DndContext so user props cannot silently
override the internal handlers. Ensure references to DndContext, onDragEnd,
onDragStart, onDragCancel, setActiveId, onMove, and onValueChange are updated
accordingly.

Copy link

@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: 2

🧹 Nitpick comments (1)
src/index.ts (1)

146-151: Export DragOverlayProps type alongside DragOverlay for API consistency.

DragOverlay (line 151) is exported without a matching props type export, while the other Sortable-related components have type exports (lines 221–224): SortableProps, SortableItemProps, SortableDragHandleProps, and SortableProviderProps. Consumers needing to type-check or extend DragOverlay props would have to import directly from @dnd-kit/react. Add DragOverlayProps to maintain consistent API design.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.ts` around lines 146 - 151, Export the DragOverlayProps type
alongside the existing DragOverlay export to maintain API consistency: add an
export of DragOverlayProps (re-exporting the type from the same module you
import DragOverlay from, e.g., `@dnd-kit/react`) so consumers can import {
DragOverlay, DragOverlayProps } from this package; update the export list where
Sortable, SortableItem, SortableDragHandle, SortableProvider and DragOverlay are
exported to include DragOverlayProps.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/index.ts`:
- Around line 137-143: The barrel export in src/index.ts has dropped
PopoverClose and PopoverArrow which still exist in
src/components/ui/popover.tsx; restore the public API by re-adding PopoverClose
and PopoverArrow to the export list in src/index.ts (or, if the removal is
intentional, add a changelog/migration note explaining the breaking change and
update any consumer docs). Locate the export block containing Popover,
PopoverContent, PopoverDescription, PopoverHeader, PopoverPortal, PopoverTitle,
PopoverTrigger and include PopoverClose and PopoverArrow there so consumers
importing from the main barrel continue to work.
- Around line 146-151: There are mixed dnd-kit stacks being exported (Sortable,
SortableItem, SortableDragHandle, SortableProvider, DragOverlay) causing runtime
conflicts; pick one approach and update the exports and imports accordingly:
either remove all usages and exports of `@dnd-kit/react` (DragOverlay and any
react-stack imports) and keep the legacy stack (ensure
src/components/ui/sortable.tsx uses `@dnd-kit/core`@^6.3.x +
`@dnd-kit/sortable`@^10.0.0 and export only Sortable, SortableItem,
SortableDragHandle, SortableProvider), or migrate fully to the new stack by
removing `@dnd-kit/core` and `@dnd-kit/sortable`, update
src/components/ui/sortable.tsx to use `@dnd-kit/react`@0.3.0 (import sortable
utilities from `@dnd-kit/react/sortable`) and export the new equivalents
(including DragOverlay) consistently; make sure imports/exports across the
codebase match the chosen stack.

---

Nitpick comments:
In `@src/index.ts`:
- Around line 146-151: Export the DragOverlayProps type alongside the existing
DragOverlay export to maintain API consistency: add an export of
DragOverlayProps (re-exporting the type from the same module you import
DragOverlay from, e.g., `@dnd-kit/react`) so consumers can import { DragOverlay,
DragOverlayProps } from this package; update the export list where Sortable,
SortableItem, SortableDragHandle, SortableProvider and DragOverlay are exported
to include DragOverlayProps.

Comment on lines +146 to +151
// Sortable
Sortable,
SortableItem,
SortableDragHandle,
SortableProvider,
DragOverlay,
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

@dnd-kit/react 0.3.0 @dnd-kit/sortable 10.0.0 compatibility

💡 Result:

They’re not compatible.

  • @dnd-kit/sortable@10.0.0 is part of the legacy stack and has a peer dependency on @dnd-kit/core ^6.3.0 (not @dnd-kit/react). [1]
  • @dnd-kit/react@0.3.0 is the new React package in the “migrate from @dnd-kit/core to @dnd-kit/react” track, where you replace @dnd-kit/core + @dnd-kit/sortable with @dnd-kit/react (+ related new packages). [2]
  • Sortable support in the new stack is exposed via @dnd-kit/react/sortable (not @dnd-kit/sortable). [3]

So you should either:

  1. Stay legacy: use @dnd-kit/core@^6.3.x with @dnd-kit/sortable@10.0.0, or
  2. Go new: use @dnd-kit/react@0.3.0 with @dnd-kit/react/sortable (matching the same new-version line). [1][2][3]

Sources: [1] [2] [3]


🏁 Script executed:

# Find and check package.json for actual dependencies and versions
fd -e package.json -type f | head -5

Repository: getdokan/plugin-ui

Length of output: 232


🏁 Script executed:

# Find package.json correctly
find . -name "package.json" -type f | head -5

Repository: getdokan/plugin-ui

Length of output: 76


🏁 Script executed:

# Check imports in src/index.ts
cat src/index.ts | head -20

Repository: getdokan/plugin-ui

Length of output: 631


🏁 Script executed:

# Check package.json for `@dnd-kit` dependencies
cat package.json | grep -A 20 "@dnd-kit"

Repository: getdokan/plugin-ui

Length of output: 914


🏁 Script executed:

# Look for dnd-kit imports in src/index.ts
rg "@dnd-kit|Sortable" src/index.ts -A 2 -B 2

Repository: getdokan/plugin-ui

Length of output: 434


🏁 Script executed:

# Find where Sortable components are imported from
rg "from.*Sortable|from.*@dnd-kit" src/ -A 1 -B 1 | head -40

Repository: getdokan/plugin-ui

Length of output: 659


🏁 Script executed:

# Check if there's a Sortable component file
find src -name "*ortable*" -o -name "*sortable*" | head -20

Repository: getdokan/plugin-ui

Length of output: 131


Remove @dnd-kit/react or migrate to the new dnd-kit stack — do not mix them.

The codebase has incompatible @dnd-kit versions. @dnd-kit/sortable@10.0.0 is the legacy stack and requires @dnd-kit/core@^6.3.0, while @dnd-kit/react@0.3.0 is the new stack that replaces both. These are mutually exclusive—mixing them will cause runtime conflicts.

Choose one:

  • Stay legacy: Remove @dnd-kit/react, keep @dnd-kit/core@^6.3.1 + @dnd-kit/sortable@^10.0.0 (current setup in src/components/ui/sortable.tsx).
  • Migrate to new: Remove @dnd-kit/core and @dnd-kit/sortable, use @dnd-kit/react@0.3.0 and import sortable utilities from @dnd-kit/react/sortable.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.ts` around lines 146 - 151, There are mixed dnd-kit stacks being
exported (Sortable, SortableItem, SortableDragHandle, SortableProvider,
DragOverlay) causing runtime conflicts; pick one approach and update the exports
and imports accordingly: either remove all usages and exports of `@dnd-kit/react`
(DragOverlay and any react-stack imports) and keep the legacy stack (ensure
src/components/ui/sortable.tsx uses `@dnd-kit/core`@^6.3.x +
`@dnd-kit/sortable`@^10.0.0 and export only Sortable, SortableItem,
SortableDragHandle, SortableProvider), or migrate fully to the new stack by
removing `@dnd-kit/core` and `@dnd-kit/sortable`, update
src/components/ui/sortable.tsx to use `@dnd-kit/react`@0.3.0 (import sortable
utilities from `@dnd-kit/react/sortable`) and export the new equivalents
(including DragOverlay) consistently; make sure imports/exports across the
codebase match the chosen stack.

…and `sortable`

- Replaced `any` types with `DraggableSyntheticListeners` for better type safety.
- Simplified `PopoverPortal` by removing unnecessary export keyword.
- Removed unused imports (e.g., `GripVertical`, `defaultDropAnimationSideEffects`, `DropdownProvider`, and strategies).
- Adjusted `Sortable.stories.tsx` to remove unused props and imports for cleaner implementation.
- Added `@dnd-kit` packages (`core`, `modifiers`, `react`, `sortable`, `utilities`) and related dependencies.
- Introduced new peer dependencies and updates for better compatibility and functionality.
@Aunshon Aunshon merged commit d7a63cc into main Feb 19, 2026
1 check passed
@Aunshon Aunshon deleted the add/sortable-component branch February 19, 2026 04:43
Copy link

@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: 3

🧹 Nitpick comments (1)
src/components/ui/Sortable.stories.tsx (1)

38-38: Hoist Demo components out of the render callbacks to avoid unintended state resets.

Every story defines function Demo() inside render: () => { ... }. Each invocation of render produces a new function reference, so React treats it as a different component type and performs a full unmount/remount — discarding all interactive state (sorted order, editingId, switch states). This is triggered by HMR, any args/controls change in the Storybook panel, or any parent re-render.

The fix is to hoist each Demo component to module scope (or above the story object):

♻️ Example fix (applies to all 8 stories)
+function VerticalDemo() {
+  const [items, setItems] = useState(initialItems);
+  return (
+    <div className="w-80">
+      <Sortable items={items} onValueChange={setItems}>
+        <div className="flex flex-col gap-2">
+          {items.map((item) => (
+            <SortableItem key={item.id} id={item.id}>
+              <div className="flex items-center justify-between rounded-md border bg-card p-4 shadow-sm">
+                <span>{item.content}</span>
+              </div>
+            </SortableItem>
+          ))}
+        </div>
+      </Sortable>
+    </div>
+  );
+}

 export const Vertical: Story = {
-  render: () => {
-    function Demo() {
-      const [items, setItems] = useState(initialItems);
-      return (
-        <div className="w-80">
-          <Sortable items={items} onValueChange={setItems}>
-            <div className="flex flex-col gap-2">
-              {items.map((item) => (
-                <SortableItem key={item.id} id={item.id}>
-                  <div className="flex items-center justify-between rounded-md border bg-card p-4 shadow-sm">
-                    <span>{item.content}</span>
-                  </div>
-                </SortableItem>
-              ))}
-            </div>
-          </Sortable>
-        </div>
-      );
-    }
-    return <Demo />;
-  },
+  render: () => <VerticalDemo />,
 };

Also applies to: 135-135, 159-159, 189-189, 217-217, 251-251, 281-281, 317-317

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/Sortable.stories.tsx` at line 38, Each story currently
defines a local function Demo inside its render: () => { ... } callback which
creates a new component type on every render (e.g., Demo in the render of each
story), causing React to unmount/remount and lose interactive state; hoist each
Demo component to module scope (move the Demo function out of the render
callback and place it above the story definitions) and update the story render
to return the hoisted Demo component (keep any props passed via story args),
ensuring the same component reference is used across renders and preserving
state for stories that use Demo, editingId, sorted order, and switches.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/ui/Sortable.stories.tsx`:
- Around line 85-90: The icon-only <button> that calls setEditingId(null) and
renders the X component lacks an accessible name; update that button to include
an appropriate aria-label (e.g., aria-label="Cancel editing" or "Close") so
screen readers get meaningful context for the action triggered by the X icon,
and ensure the X icon itself remains decorative (aria-hidden) if you add the
label.
- Around line 173-175: In the WithDragHandle story in Sortable.stories.tsx the
remove/action Button (the X icon) is missing an onClick handler; either wire
that Button up to call the story's remove function (e.g., call an existing
handleRemove or onRemove item callback used by the Sortable list) or add a local
handler that updates the story's items state to remove the corresponding item id
when clicked; if removal isn't supported in this story, replace the interactive
Button with a non-interactive element (e.g., a span) to avoid implying
interactivity. Ensure you modify the Button inside the WithDragHandle render so
it receives onClick={() => removeItem(id)} (or similar) and that removeItem is
defined and wired to the same state/props managing the list.
- Around line 78-106: The edit UI is currently uncontrolled and never persists
changes: the Input uses defaultValue and the save/cancel handlers (setEditingId
in the X and Check button handlers) only close edit mode without updating the
items state. Change to a controlled edit flow by adding a temporary edit value
(e.g., editingValue) when entering edit mode for the specific item (set
editingValue = item.label in the handler that sets editingId), bind that
editingValue to the Input (onChange updates editingValue), and on the Check
button click update the items state (use setItems to map the item with matching
item.id and replace its label with editingValue) then setEditingId(null); keep
the X button to discard changes by just clearing editingId (and optionally
clearing editingValue).

---

Duplicate comments:
In `@src/components/ui/Sortable.stories.tsx`:
- Around line 316-359: The story MultipleLists currently renders two separate
Sortable components (inside Demo), creating two independent DndContexts so
cross-list dragging is impossible; either (A) make one shared
Sortable/DndContext by moving the <Sortable> wrapper to encompass both list
containers and update Sortable/SortableItem usage and onValueChange to handle
item moves between itemsA and itemsB (use item ids and a single state handler in
Demo), or (B) if independent lists are intended, rename the story and UI text
from MultipleLists to IndependentLists (and update the story description) so it
doesn't promise cross-list drag behavior. Target symbols: MultipleLists, Demo,
Sortable, SortableItem, itemsA, itemsB, onValueChange.

---

Nitpick comments:
In `@src/components/ui/Sortable.stories.tsx`:
- Line 38: Each story currently defines a local function Demo inside its render:
() => { ... } callback which creates a new component type on every render (e.g.,
Demo in the render of each story), causing React to unmount/remount and lose
interactive state; hoist each Demo component to module scope (move the Demo
function out of the render callback and place it above the story definitions)
and update the story render to return the hoisted Demo component (keep any props
passed via story args), ensuring the same component reference is used across
renders and preserving state for stories that use Demo, editingId, sorted order,
and switches.

Comment on lines +78 to +106
{editingId === item.id ? (
<div className="relative flex-1">
<Input
defaultValue={item.label}
className="h-9 border-primary/50 ring-primary/20 focus-visible:border-primary focus-visible:ring-primary/30"
autoFocus
/>
<button
onClick={() => setEditingId(null)}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
<X className="size-4" />
</button>
</div>
) : (
<span className="text-sm font-medium">{item.label}</span>
)}
</div>

<div className="flex items-center gap-3">
{editingId === item.id ? (
<Button
variant="outline"
size="icon-sm"
className="h-8 w-8 border-muted"
onClick={() => setEditingId(null)}
>
<Check className="size-4 text-muted-foreground" />
</Button>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Edits in AdvancedCustomUI are silently discarded — demo implies saving but never updates state.

The Input uses defaultValue (uncontrolled), so its value is never read back. The "save" action (Check button, line 99 and the X button, line 86) only calls setEditingId(null) without committing the new label to items state. Users interacting with the demo will type a label change, click save, and see it revert — which misrepresents the editing capability.

Either make it a read-only view-mode toggle (remove the Input entirely), or wire up a controlled flow:

🐛 Proposed fix to persist edits
+      const [editValues, setEditValues] = useState<Record<string, string>>(
+        Object.fromEntries(items.map((i) => [i.id, i.label]))
+      );
+
+      const saveEdit = (id: string) => {
+        setItems((prev) =>
+          prev.map((item) =>
+            item.id === id ? { ...item, label: editValues[id] } : item
+          )
+        );
+        setEditingId(null);
+      };

         {editingId === item.id ? (
           <div className="relative flex-1">
             <Input
-              defaultValue={item.label}
+              value={editValues[item.id]}
+              onChange={(e) =>
+                setEditValues((prev) => ({ ...prev, [item.id]: e.target.value }))
+              }
               className="..."
               autoFocus
             />
             <button
-              onClick={() => setEditingId(null)}
+              onClick={() => saveEdit(item.id)}
               ...
             >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/Sortable.stories.tsx` around lines 78 - 106, The edit UI is
currently uncontrolled and never persists changes: the Input uses defaultValue
and the save/cancel handlers (setEditingId in the X and Check button handlers)
only close edit mode without updating the items state. Change to a controlled
edit flow by adding a temporary edit value (e.g., editingValue) when entering
edit mode for the specific item (set editingValue = item.label in the handler
that sets editingId), bind that editingValue to the Input (onChange updates
editingValue), and on the Check button click update the items state (use
setItems to map the item with matching item.id and replace its label with
editingValue) then setEditingId(null); keep the X button to discard changes by
just clearing editingId (and optionally clearing editingValue).

Comment on lines +85 to +90
<button
onClick={() => setEditingId(null)}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
<X className="size-4" />
</button>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Icon-only <button> is missing an aria-label.

The native <button> wrapping the X icon (cancel editing) has no accessible name. Screen readers will announce it without meaningful context.

♿ Proposed fix
-            <button
-              onClick={() => setEditingId(null)}
-              className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
-            >
+            <button
+              onClick={() => setEditingId(null)}
+              aria-label="Cancel editing"
+              className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
+            >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
onClick={() => setEditingId(null)}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
<X className="size-4" />
</button>
<button
onClick={() => setEditingId(null)}
aria-label="Cancel editing"
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
<X className="size-4" />
</button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/Sortable.stories.tsx` around lines 85 - 90, The icon-only
<button> that calls setEditingId(null) and renders the X component lacks an
accessible name; update that button to include an appropriate aria-label (e.g.,
aria-label="Cancel editing" or "Close") so screen readers get meaningful context
for the action triggered by the X icon, and ensure the X icon itself remains
decorative (aria-hidden) if you add the label.

Comment on lines +173 to +175
<Button variant="ghost" size="icon-sm">
<X className="size-4" />
</Button>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove/action Button in WithDragHandle has no onClick handler.

The X icon button appears interactive but does nothing when clicked. In a demo, this creates confusion about whether removal is supported. Either wire up a remove handler or replace it with a non-interactive element.

🐛 Proposed fix to add a remove handler
-                    <Button variant="ghost" size="icon-sm">
+                    <Button
+                      variant="ghost"
+                      size="icon-sm"
+                      aria-label="Remove item"
+                      onClick={() =>
+                        setItems((prev) => prev.filter((i) => i.id !== item.id))
+                      }
+                    >
                       <X className="size-4" />
                     </Button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Button variant="ghost" size="icon-sm">
<X className="size-4" />
</Button>
<Button
variant="ghost"
size="icon-sm"
aria-label="Remove item"
onClick={() =>
setItems((prev) => prev.filter((i) => i.id !== item.id))
}
>
<X className="size-4" />
</Button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/Sortable.stories.tsx` around lines 173 - 175, In the
WithDragHandle story in Sortable.stories.tsx the remove/action Button (the X
icon) is missing an onClick handler; either wire that Button up to call the
story's remove function (e.g., call an existing handleRemove or onRemove item
callback used by the Sortable list) or add a local handler that updates the
story's items state to remove the corresponding item id when clicked; if removal
isn't supported in this story, replace the interactive Button with a
non-interactive element (e.g., a span) to avoid implying interactivity. Ensure
you modify the Button inside the WithDragHandle render so it receives
onClick={() => removeItem(id)} (or similar) and that removeItem is defined and
wired to the same state/props managing the list.

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