PhoenixDown
PhoenixDown

Reputation: 75

Prevent drag when user drags a child component of a draggable component using dnd-kit sortable

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

Answers (1)

Kyle H
Kyle H

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

Related Questions