Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How do I prevent draggable on input and btns #477

Closed
AsasinCree opened this issue Oct 19, 2021 · 20 comments
Closed

How do I prevent draggable on input and btns #477

AsasinCree opened this issue Oct 19, 2021 · 20 comments

Comments

@AsasinCree
Copy link

AsasinCree commented Oct 19, 2021

I have some operations btns and input inside the draggable area. I can't select text by moving mouse in input but triggering the drag event.

Although I can set "DelayConstraint" by sensors, but it's not a perfect solution while I need to balance the drag delay and operations in input.

I want to know is there a config for disable the part of "white" content from dragging but keep dragging on "red" area

image

@clauderic
Copy link
Owner

clauderic commented Oct 19, 2021

You can override the activator function of the sensors you're using to achieve this. If you return false in the activator handler, the sensor will not be activated.

Something like:

class MyPointerSensor extends PointerSensor {
  static activators = [
    {
      eventName: 'onPointerDown',
      handler: ({nativeEvent: event}) => {
        if (
          !event.isPrimary ||
          event.button !== 0 ||
          isInteractiveElement(event.target)
        ) {
          return false;
        }

        return true;
      },
    },
  ];
}

function isInteractiveElement(element) {
  const interactiveElements = [
    'button',
    'input',
    'textarea',
    'select',
    'option',
  ];

  if (interactiveElements.includes(element.tagName.toLowerCase())) {
    return true;
  }

  return false;
}

@AsasinCree
Copy link
Author

AsasinCree commented Oct 22, 2021

Thanks a lot!!!! Best solution. It would be better if we can achieve this by a prop config like "disableDragInteractiveElements" array

By the way, I post the Typescript version if anyone need it

import type { PointerEvent } from "react";
import { PointerSensor } from "@dnd-kit/core";

/**
 * An extended "PointerSensor" that prevent some
 * interactive html element(button, input, textarea, select, option...) from dragging
 */
export class SmartPointerSensor extends PointerSensor {
    static activators = [
        {
            eventName: "onPointerDown" as any,
            handler: ({ nativeEvent: event }: PointerEvent) => {
                if (
                    !event.isPrimary ||
                    event.button !== 0 ||
                    isInteractiveElement(event.target as Element)
                ) {
                    return false;
                }

                return true;
            },
        },
    ];
}

function isInteractiveElement(element: Element | null) {
    const interactiveElements = [
        "button",
        "input",
        "textarea",
        "select",
        "option",
    ];
    if (
        element?.tagName &&
        interactiveElements.includes(element.tagName.toLowerCase())
    ) {
        return true;
    }

    return false;
}

@HelKyle
Copy link

HelKyle commented Dec 3, 2021

For someone who may have the same issue. Just add data-no-dnd="true" to the parent node and all the descendant nodes will not handle the dnd events.

import type { MouseEvent, KeyboardEvent } from 'react'
import {
  MouseSensor as LibMouseSensor,
  KeyboardSensor as LibKeyboardSensor
} from '@dnd-kit/core'

export class MouseSensor extends LibMouseSensor {
  static activators = [
    {
      eventName: 'onMouseDown' as const,
      handler: ({ nativeEvent: event }: MouseEvent) => {
        return shouldHandleEvent(event.target as HTMLElement)
      }
    }
  ]
}

export class KeyboardSensor extends LibKeyboardSensor {
  static activators = [
    {
      eventName: 'onKeyDown' as const,
      handler: ({ nativeEvent: event }: KeyboardEvent<Element>) => {
        return shouldHandleEvent(event.target as HTMLElement)
      }
    }
  ]
}

function shouldHandleEvent(element: HTMLElement | null) {
  let cur = element

  while (cur) {
    if (cur.dataset && cur.dataset.noDnd) {
      return false
    }
    cur = cur.parentElement
  }

  return true
}

oshi97 added a commit to yext/studio that referenced this issue Jul 14, 2022
This PR adds drag and drop functionality to the PropEditor-s rendered by the Studio, letting users reorder their components

CustomMouseSensor was added so that drag and drop would not be activated inside input elements, otherwise it would not be possible to type inside them. see clauderic/dnd-kit#477

TEST=manual

test when I add components a new component appears on the page, and also a new prop editor, and I can save the new component
test that reordering works and can be saved to file, and also doesn't jitter or cause weird flashes as the PropEditor is dragged around (was an issue)
test that I can type inside the PropEditor
@JANEKKO
Copy link

JANEKKO commented Aug 16, 2023

when i use CustomMouseSensor, i got this error, how can i use CustomMouseSensor?
TypeError: Class constructor MouseSensor cannot be invoked without 'new'

here's my code:
const sensors = useSensors( useSensor(CustomMouseSensor, { activationConstraint: { distance: 5, }, }))

@DjVreditel
Copy link

DjVreditel commented Sep 11, 2023

For someone who may have the same issue. Just add data-no-dnd="true" to the parent node and all the descendant nodes will not handle the dnd events.

import type { MouseEvent, KeyboardEvent } from 'react'
import {
  MouseSensor as LibMouseSensor,
  KeyboardSensor as LibKeyboardSensor
} from '@dnd-kit/core'

export class MouseSensor extends LibMouseSensor {
  static activators = [
    {
      eventName: 'onMouseDown' as const,
      handler: ({ nativeEvent: event }: MouseEvent) => {
        return shouldHandleEvent(event.target as HTMLElement)
      }
    }
  ]
}

export class KeyboardSensor extends LibKeyboardSensor {
  static activators = [
    {
      eventName: 'onKeyDown' as const,
      handler: ({ nativeEvent: event }: KeyboardEvent<Element>) => {
        return shouldHandleEvent(event.target as HTMLElement)
      }
    }
  ]
}

function shouldHandleEvent(element: HTMLElement | null) {
  let cur = element

  while (cur) {
    if (cur.dataset && cur.dataset.noDnd) {
      return false
    }
    cur = cur.parentElement
  }

  return true
}

Thanks!

This is improved TypeScript example:

import { MouseSensor as LibMouseSensor, TouchSensor as LibTouchSensor } from '@dnd-kit/core';
import { MouseEvent, TouchEvent } from 'react';

// Block DnD event propagation if element have "data-no-dnd" attribute
const handler = ({ nativeEvent: event }: MouseEvent | TouchEvent) => {
    let cur = event.target as HTMLElement;

    while (cur) {
        if (cur.dataset && cur.dataset.noDnd) {
            return false;
        }
        cur = cur.parentElement as HTMLElement;
    }

    return true;
};

export class MouseSensor extends LibMouseSensor {
    static activators = [{ eventName: 'onMouseDown', handler }] as typeof LibMouseSensor['activators'];
}

export class TouchSensor extends LibTouchSensor {
    static activators = [{ eventName: 'onTouchStart', handler }] as typeof LibTouchSensor['activators'];
}

@JANEKKO
Copy link

JANEKKO commented Sep 11, 2023 via email

@RafaelCENG
Copy link

Hello everyone, I tried this approach but for some reason my didnt work.
I have a very similar approach with this sandbox.
https://codesandbox.io/p/sandbox/dnd-kit-form-builder-fii0zh?file=%2Fsrc%2Fstyles.css
image
I want to make it draggable only from the dots and being able to expand my component,click on the fields etc.
I added the sensors the data-no-dnd="true" to my parent but nothing..

@JANEKKO
Copy link

JANEKKO commented Jan 24, 2024 via email

@JANEKKO
Copy link

JANEKKO commented Feb 27, 2024 via email

@HyungwonJang0327
Copy link

Hello everyone, I tried this approach but for some reason my didnt work. I have a very similar approach with this sandbox. https://codesandbox.io/p/sandbox/dnd-kit-form-builder-fii0zh?file=%2Fsrc%2Fstyles.css image I want to make it draggable only from the dots and being able to expand my component,click on the fields etc. I added the sensors the data-no-dnd="true" to my parent but nothing..

I'm experiencing the same problem.
I think it'll work if I don't spread the listeners of useSortable to children, but I don't know how to stop it.

@JANEKKO
Copy link

JANEKKO commented May 13, 2024 via email

@tom-leamon
Copy link

This seems very fundamental, I think the proposed solution should either be added to the docs or built into the library behind a prop like "allowEventPropagation" that does it behind the scenes.

@paddu1246
Copy link

paddu1246 commented Jun 13, 2024

Hi All,

in Dnd kit library input tag onchage event not fired, I was tried above approach , I didn't get any luck. could please help me out this problem.

Thank you in advance

@JANEKKO
Copy link

JANEKKO commented Jun 13, 2024 via email

@ChTiSh
Copy link

ChTiSh commented Jul 10, 2024

@DjVreditel your solution worked beautifully, thank you. Post this as when I saw the recent comments regarding solutions not working I lost some faith wondering if some recent updates caused changes, but I shouldn't.

@JANEKKO
Copy link

JANEKKO commented Jul 10, 2024 via email

@mikalai-sauchanka
Copy link

mikalai-sauchanka commented Oct 9, 2024

If you get "TypeError: Class constructor MouseSensor cannot be invoked without 'new'", patching the activators (since it's a static prop, can be done once) should work. Also canStartDrag can be anything you want.

const FORBID_DRAG_ON = ["INPUT", "TEXTAREA"];
const canStartDrag = () => {
  if (document.activeElement) {
    const tagName = (document.activeElement as HTMLElement).tagName;
    if (
      FORBID_DRAG_ON.includes(tagName) ||
      (tagName === "DIV" && (document.activeElement as HTMLDivElement).contentEditable === "true")
    ) {
      return false;
    }
  }

  return true;
};

const patchActivator = <T extends SensorOptions>(activators: Activators<T>, eventName: string): Activators<T> => {
  const originalActivatorIndex = activators.findIndex((x) => x.eventName === eventName);
  const originalActivator = activators[originalActivatorIndex];

  activators[originalActivatorIndex] = {
    eventName: originalActivator.eventName,
    handler(...args) {
      return canStartDrag() && originalActivator.handler(...args);
    }
  };
  return activators;
};

patchActivator(KeyboardSensor.activators, "onKeyDown");
patchActivator(PointerSensor.activators, "onPointerDown");

@JANEKKO
Copy link

JANEKKO commented Oct 9, 2024 via email

@1used23
Copy link

1used23 commented Nov 2, 2024

More beautiful solution in my opinion, if you get "TypeError: Class constructor MouseSensor cannot be invoked without 'new'" with previous solutions:

import type { MouseEvent, TouchEvent } from 'react';
import { MouseSensor, TouchSensor } from '@dnd-kit/core';

const IGNORE_TAGS = ['BUTTON'];

// Block Dnd, if IGNORE_TAGS includes elements tag or element has data-no-dnd attribute
const customHandleEvent = (element: HTMLElement | null) => {
  let cur = element;

  while (cur) {
    if (IGNORE_TAGS.includes(cur.tagName) || cur.dataset.noDnd) {
      return false;
    }
    cur = cur.parentElement;
  }

  return true;
};

MouseSensor.activators = [
  {
    eventName: 'onMouseDown',
    handler: ({ nativeEvent: event }: MouseEvent) => customHandleEvent(event.target as HTMLElement),
  },
];

TouchSensor.activators = [
  {
    eventName: 'onTouchStart',
    handler: ({ nativeEvent: event }: TouchEvent) => customHandleEvent(event.target as HTMLElement),
  },
];

@JANEKKO
Copy link

JANEKKO commented Nov 2, 2024 via email

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

No branches or pull requests