mirror of
https://github.com/penpot/penpot.git
synced 2026-07-12 09:10:16 +00:00
* 🐛 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>
158 lines
4.1 KiB
TypeScript
158 lines
4.1 KiB
TypeScript
import { expect, describe, vi } from 'vitest';
|
|
import { dragHandler } from './drag-handler.js';
|
|
|
|
type PointerLikeEvent = MouseEvent & { pointerId: number };
|
|
|
|
function createPointerEvent(
|
|
type: string,
|
|
init: Partial<PointerEventInit> = {},
|
|
): PointerLikeEvent {
|
|
const event = new MouseEvent(type, {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
clientX: init.clientX ?? 0,
|
|
clientY: init.clientY ?? 0,
|
|
button: init.button ?? 0,
|
|
}) as PointerLikeEvent;
|
|
|
|
Object.defineProperty(event, 'pointerId', {
|
|
configurable: true,
|
|
value: init.pointerId ?? 1,
|
|
});
|
|
|
|
return event;
|
|
}
|
|
|
|
describe('dragHandler', () => {
|
|
let element: HTMLElement;
|
|
|
|
beforeEach(() => {
|
|
element = document.createElement('div');
|
|
Object.defineProperty(element, 'setPointerCapture', {
|
|
configurable: true,
|
|
value: vi.fn(),
|
|
});
|
|
Object.defineProperty(element, 'releasePointerCapture', {
|
|
configurable: true,
|
|
value: vi.fn(),
|
|
});
|
|
Object.defineProperty(element, 'hasPointerCapture', {
|
|
configurable: true,
|
|
value: vi.fn().mockReturnValue(true),
|
|
});
|
|
document.body.appendChild(element);
|
|
});
|
|
|
|
afterEach(() => {
|
|
document.body.removeChild(element);
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('should attach pointerdown event listener to the element', () => {
|
|
const addEventListenerMock = vi.spyOn(element, 'addEventListener');
|
|
|
|
dragHandler(element);
|
|
|
|
expect(addEventListenerMock).toHaveBeenCalledWith(
|
|
'pointerdown',
|
|
expect.any(Function),
|
|
);
|
|
});
|
|
|
|
it('should update element transform on pointermove', () => {
|
|
const pointerDownEvent = createPointerEvent('pointerdown', {
|
|
clientX: 100,
|
|
clientY: 100,
|
|
});
|
|
|
|
dragHandler(element);
|
|
|
|
element.dispatchEvent(pointerDownEvent);
|
|
|
|
const pointerMoveEvent = createPointerEvent('pointermove', {
|
|
clientX: 150,
|
|
clientY: 150,
|
|
});
|
|
element.dispatchEvent(pointerMoveEvent);
|
|
|
|
expect(element.style.transform).toBe('translate(50px, 50px)');
|
|
|
|
const pointerMoveEvent2 = createPointerEvent('pointermove', {
|
|
clientX: 200,
|
|
clientY: 200,
|
|
});
|
|
element.dispatchEvent(pointerMoveEvent2);
|
|
|
|
expect(element.style.transform).toBe('translate(100px, 100px)');
|
|
});
|
|
|
|
it('should run lifecycle callbacks on drag start/end', () => {
|
|
const start = vi.fn();
|
|
const end = vi.fn();
|
|
const pointerDownEvent = createPointerEvent('pointerdown', {
|
|
clientX: 100,
|
|
clientY: 100,
|
|
pointerId: 2,
|
|
});
|
|
const pointerUpEvent = createPointerEvent('pointerup', {
|
|
pointerId: 2,
|
|
});
|
|
|
|
dragHandler(element, element, undefined, { start, end });
|
|
element.dispatchEvent(pointerDownEvent);
|
|
element.dispatchEvent(pointerUpEvent);
|
|
|
|
expect(start).toHaveBeenCalledTimes(1);
|
|
expect(end).toHaveBeenCalledTimes(1);
|
|
expect(element.releasePointerCapture).toHaveBeenCalledWith(2);
|
|
});
|
|
|
|
it('should ignore pointerdown events from button targets', () => {
|
|
const start = vi.fn();
|
|
const button = document.createElement('button');
|
|
const icon = document.createElement('span');
|
|
button.appendChild(icon);
|
|
element.appendChild(button);
|
|
|
|
dragHandler(element, element, undefined, { start });
|
|
|
|
icon.dispatchEvent(
|
|
createPointerEvent('pointerdown', {
|
|
pointerId: 5,
|
|
button: 0,
|
|
}),
|
|
);
|
|
|
|
expect(start).not.toHaveBeenCalled();
|
|
expect(element.setPointerCapture).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should remove pointer listeners on teardown', () => {
|
|
const removeEventListenerMock = vi.spyOn(element, 'removeEventListener');
|
|
|
|
const cleanup = dragHandler(element);
|
|
cleanup();
|
|
|
|
expect(removeEventListenerMock).toHaveBeenCalledWith(
|
|
'pointerdown',
|
|
expect.any(Function),
|
|
);
|
|
expect(removeEventListenerMock).toHaveBeenCalledWith(
|
|
'pointermove',
|
|
expect.any(Function),
|
|
);
|
|
expect(removeEventListenerMock).toHaveBeenCalledWith(
|
|
'pointerup',
|
|
expect.any(Function),
|
|
);
|
|
expect(removeEventListenerMock).toHaveBeenCalledWith(
|
|
'pointercancel',
|
|
expect.any(Function),
|
|
);
|
|
expect(removeEventListenerMock).toHaveBeenCalledWith(
|
|
'lostpointercapture',
|
|
expect.any(Function),
|
|
);
|
|
});
|
|
});
|