Reputation: 75
I'm using dnd-kit/sortable to allow sorting a list of items using drag-and-drop. This works fine except when users drag a textarea or another child element the entire component is dragged. This becomes a problem when highlight text for example. What I'd like to do is prevent drag action when a user drags a child component of a draggable parent.
I tried using e.preventDefault() and e.stopPropagation(), but neither of those stop the dragging. I've added my code to this sandbox - https://codesandbox.io/p/sandbox/xenodochial-villani-783hjz
In case there is an issue with the sandbox this is the code:
SortableItem.js
import React from "react";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
const SortableItem = ({ id, image, text }) => {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
const stopPropagation = (e) => {
e.preventDefault();
e.stopPropagation();
};
const update = (e) => {
alert(1);
};
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className="sortable-item"
>
<img
width="140px"
src="https://img.freepik.com/free-psd/3d-illustration-human-avatar-profile_23-2150671142.jpg?w=826&t=st=1719155268~exp=1719155868~hmac=af598917d4ff9f549cf54d8d4f1f6e2305b114f067bac23cc6f5d63e2f57b7e8"
alt=""
/>
<textarea
onMouseDown={stopPropagation}
onTouchStart={stopPropagation}
onDragStart={stopPropagation}
onDrop={stopPropagation}
>
{text}
</textarea>
<button onClick={update}>Update</button>
</div>
);
};
export default SortableItem;
SortableList.js
import React, { useState } from "react";
import {
DndContext,
useSensor,
PointerSensor,
MouseSensor,
TouchSensor,
KeyboardSensor,
useSensors,
} from "@dnd-kit/core";
import {
SortableContext,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { arrayMove } from "@dnd-kit/sortable";
import SortableItem from "./SortableItem";
const SortableList = () => {
const pointerSensor = useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
});
const mouseSensor = useSensor(MouseSensor);
const touchSensor = useSensor(TouchSensor);
const keyboardSensor = useSensor(KeyboardSensor);
const sensors = useSensors(
mouseSensor,
touchSensor,
keyboardSensor,
pointerSensor
);
const initialItems = [
{ id: 1, image: "image1.jpg", text: "Item 1" },
{ id: 2, image: "image2.jpg", text: "Item 2" },
{ id: 3, image: "image3.jpg", text: "Item 3" },
{ id: 4, image: "image4.jpg", text: "Item 4" },
];
const [items, setItems] = useState(initialItems);
const handleDragEnd = (event) => {
const { active, over } = event;
if (active.id !== over.id) {
setItems((items) => {
const oldIndex = items.findIndex((item) => item.id === active.id);
const newIndex = items.findIndex((item) => item.id === over.id);
return arrayMove(items, oldIndex, newIndex);
});
}
};
return (
<DndContext onDragEnd={handleDragEnd} sensors={sensors}>
<SortableContext
items={items.map((item) => item.id)}
strategy={verticalListSortingStrategy}
>
<div className="sortable-list">
{items.map((item) => (
<SortableItem
key={item.id}
id={item.id}
image={item.image}
text={item.text}
/>
))}
</div>
</SortableContext>
</DndContext>
);
};
export default SortableList;
App.js
import "./styles.css";
import SortableList from "./SortableList";
export default function App() {
return (
<div className="App">
<h1>Sortable List</h1>
<SortableList />
</div>
);
}
Upvotes: 1
Views: 322
Reputation: 3303
The best solution I've found for this is here.
Step 1: Create a custom mouse & keyboard sensor.
// customSensors.ts
import type { MouseEvent, KeyboardEvent } from 'react';
import {
MouseSensor as LibMouseSensor,
KeyboardSensor as LibKeyboardSensor,
} from '@dnd-kit/core';
function shouldHandleEvent(element: HTMLElement | null): boolean {
let cur = element;
while (cur) {
if (cur.dataset && cur.dataset.noDnd) {
return false;
}
cur = cur.parentElement;
}
return true;
}
export class CustomMouseSensor extends LibMouseSensor {
static activators = [
{
eventName: 'onMouseDown' as const,
handler: ({ nativeEvent: event }: MouseEvent) => {
return shouldHandleEvent(event.target as HTMLElement);
},
},
];
}
export class CustomKeyboardSensor extends LibKeyboardSensor {
static activators = [
{
eventName: 'onKeyDown' as const,
handler: ({ nativeEvent: event }: KeyboardEvent) => {
return shouldHandleEvent(event.target as HTMLElement);
},
},
];
}
Step 2: Then on your parent component where you don't want anything within it to trigger the dnd events, you can add data-no-dnd
or data-no-dnd="true"
(they should both render the same HTML).
<div
data-no-dnd
>
Step 3: Register your custom sensors on the DndContext:
const sensors = useSensors(
useSensor(CustomMouseSensor),
useSensor(CustomKeyboardSensor)
);
....
<DndContext
sensors={sensors}
collisionDetection={collisionDetection}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
>
Upvotes: 0