Cancel drag when the the draggable item loses focus
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.
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?
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);
};
}
}