clauderic / dnd-kit

The modern, lightweight, performant, accessible and extensible drag & drop toolkit for React.
http://dndkit.com
MIT License
13.15k stars 653 forks source link

Cancel drag when the the draggable item loses focus #857

Open marcospassos opened 2 years ago

marcospassos commented 2 years ago

Currently, when dragging using the keyboard, the item remains active even when focusing out of it. It leads to several issues that can't be solved at the userland.

I tried implementing a custom sensor, but the current design doesn't allow for extension (note the ts-ignore):

/**
 * A custom keyboard sensor cancels the drag when moving the focus out of the draggable element.
 */
class CustomKeyboardSensor extends KeyboardSensor {
    private readonly sensorProps: SensorProps<SensorOptions>;

    constructor(props: KeyboardSensorProps) {
        super(props);

        this.sensorProps = props;

        this.handleDragCancel = this.handleDragCancel.bind(this);

        this.attachSensor();
    }

    private attachSensor(): void {
        this.sensorProps.event.target?.addEventListener('blur', this.handleDragCancel);
    }

    private detachSensor(): void {
        this.sensorProps.event.target?.removeEventListener('blur', this.handleDragCancel);
    }

    private handleDragCancel(): void {
        // There's no other way to notify the parent class to clean up the state.
        // @ts-ignore
        this.detach();
        this.detachSensor();
        this.sensorProps.onCancel();
    }
}

Even ignoring the error, other issues arise. For example, using the CustomKeyboardSensor, when pressing enter to finish dragging, DND kit announces the event as a cancel operation. Even worse, after calling the onCancel callback, in response to a click outside, the item steals the focus back, so you need to click twice to move the focus away from the item.

codewitch commented 2 years ago

Would love assistance on this as well. If a non keyboard user accidentally gets into a keyboard drag state, the app will feel like it is broken since they cant cancel the drag by clicking elsewhere.

One thought would be to build a combination sensor that taps into both keyboard and click events?

pwang347 commented 1 year ago

For what it's worth, here's something hacky I did that seems to be working fine and not running into the issues mentioned above. Note also that in my application I am using handleEnd instead of handleCancel.

export class CustomKeyboardSensor extends KeyboardSensor {
    constructor(props: KeyboardSensorProps) {
        super(props);

        const handleEnd = (this as any).handleEnd.bind(this);
        const detach = (this as any).detach.bind(this);

        props.activeNode.activatorNode.current?.addEventListener("blur", handleEnd);

        // hack: override the detach method to also remove the blur event
        (this as any).detach = () => {
            detach();
            props.activeNode.activatorNode.current?.removeEventListener("blur", handleEnd);
        };
    }
}
theenoahmason commented 3 months ago

See my answer here for how I implemented a pretty solid fix to this (and a couple other issues), including typescript support.