Marek Hrabe a803bde2ff
🐛 Fix plugin modal dragging bugs (#8871)
* 🐛 Fix plugin modal drag and close interactions

Switch plugin modal dragging to pointer-capture semantics from the header so drag state remains stable when crossing iframe boundaries. Prevent drag start from close-button pointerdown and add regression tests for both non-draggable close-button interaction and close-event dispatch.

Signed-off-by: Marek Hrabe <marekhrabe@me.com>

* 📚 Update changelog for plugin modal drag fix

Document plugin modal drag and close-button interaction fixes in the unreleased changelog.

Signed-off-by: Marek Hrabe <marekhrabe@me.com>

* 🐛 Simplify plugin modal drag CSS selection rules

Keep user-select disabled at the modal wrapper level and keep touch-action scoped to the header drag handle to remove redundant declarations while preserving drag behavior.

Signed-off-by: Marek Hrabe <marekhrabe@me.com>

---------

Signed-off-by: Marek Hrabe <marekhrabe@me.com>
2026-04-09 21:13:10 +02:00

110 lines
2.6 KiB
TypeScript

import { parseTranslate } from './parse-translate';
type DragHandlerLifecycle = {
start?: () => void;
end?: () => void;
};
export const dragHandler = (
el: HTMLElement,
target: HTMLElement = el,
move?: () => void,
lifecycle?: DragHandlerLifecycle,
) => {
let initialTranslate = { x: 0, y: 0 };
let initialClientPosition = { x: 0, y: 0 };
let pointerId: number | null = null;
let dragging = false;
const endDrag = () => {
if (!dragging) {
return;
}
dragging = false;
pointerId = null;
lifecycle?.end?.();
};
const handlePointerMove = (moveEvent: PointerEvent) => {
if (!dragging || moveEvent.pointerId !== pointerId) {
return;
}
const { clientX: moveX, clientY: moveY } = moveEvent;
const deltaX = moveX - initialClientPosition.x + initialTranslate.x;
const deltaY = moveY - initialClientPosition.y + initialTranslate.y;
target.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
move?.();
};
const handlePointerUp = (upEvent: PointerEvent) => {
if (upEvent.pointerId !== pointerId) {
return;
}
if (el.hasPointerCapture(upEvent.pointerId)) {
el.releasePointerCapture(upEvent.pointerId);
}
endDrag();
};
const handlePointerCancel = (cancelEvent: PointerEvent) => {
if (cancelEvent.pointerId !== pointerId) {
return;
}
endDrag();
};
const handleLostPointerCapture = (lostCaptureEvent: PointerEvent) => {
if (lostCaptureEvent.pointerId !== pointerId) {
return;
}
endDrag();
};
const handlePointerDown = (e: PointerEvent) => {
if (e.button !== 0) {
return;
}
const fromButton =
e.target instanceof Element && !!e.target.closest('button');
if (fromButton) {
return;
}
e.preventDefault();
initialClientPosition = { x: e.clientX, y: e.clientY };
initialTranslate = parseTranslate(target);
dragging = true;
pointerId = e.pointerId;
lifecycle?.start?.();
el.setPointerCapture(e.pointerId);
};
el.addEventListener('pointerdown', handlePointerDown);
el.addEventListener('pointermove', handlePointerMove);
el.addEventListener('pointerup', handlePointerUp);
el.addEventListener('pointercancel', handlePointerCancel);
el.addEventListener('lostpointercapture', handleLostPointerCapture);
return () => {
el.removeEventListener('pointerdown', handlePointerDown);
el.removeEventListener('pointermove', handlePointerMove);
el.removeEventListener('pointerup', handlePointerUp);
el.removeEventListener('pointercancel', handlePointerCancel);
el.removeEventListener('lostpointercapture', handleLostPointerCapture);
endDrag();
};
};