mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 11:18:36 +00:00
🐛 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>
This commit is contained in:
parent
e49b7ce14c
commit
a803bde2ff
@ -38,6 +38,7 @@
|
|||||||
- Token tree should be expanded by default [Taiga #13631](https://tree.taiga.io/project/penpot/issue/13631)
|
- Token tree should be expanded by default [Taiga #13631](https://tree.taiga.io/project/penpot/issue/13631)
|
||||||
- Fix opacity incorrectly disabled for visible shapes [Taiga #13906](https://tree.taiga.io/project/penpot/issue/13906)
|
- Fix opacity incorrectly disabled for visible shapes [Taiga #13906](https://tree.taiga.io/project/penpot/issue/13906)
|
||||||
- Update onboarding image [Taiga #13864](https://tree.taiga.io/project/penpot/issue/13864)
|
- Update onboarding image [Taiga #13864](https://tree.taiga.io/project/penpot/issue/13864)
|
||||||
|
- Fix plugin modal drag interactions over iframe and close-button behavior (by @marekhrabe) [Github #8871](https://github.com/penpot/penpot/pull/8871)
|
||||||
- Fix hot update on color-row on texts [Taiga #13923](https://tree.taiga.io/project/penpot/issue/13923)
|
- Fix hot update on color-row on texts [Taiga #13923](https://tree.taiga.io/project/penpot/issue/13923)
|
||||||
- Fix selected color tokens [Taiga #13930](https://tree.taiga.io/project/penpot/issue/13930)
|
- Fix selected color tokens [Taiga #13930](https://tree.taiga.io/project/penpot/issue/13930)
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,45 @@
|
|||||||
import { expect, describe, vi } from 'vitest';
|
import { expect, describe, vi } from 'vitest';
|
||||||
import { dragHandler } from './drag-handler.js';
|
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', () => {
|
describe('dragHandler', () => {
|
||||||
let element: HTMLElement;
|
let element: HTMLElement;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
element = document.createElement('div');
|
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);
|
document.body.appendChild(element);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -14,65 +48,109 @@ describe('dragHandler', () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should attach mousedown event listener to the element', () => {
|
it('should attach pointerdown event listener to the element', () => {
|
||||||
const addEventListenerMock = vi.spyOn(element, 'addEventListener');
|
const addEventListenerMock = vi.spyOn(element, 'addEventListener');
|
||||||
|
|
||||||
dragHandler(element);
|
dragHandler(element);
|
||||||
|
|
||||||
expect(addEventListenerMock).toHaveBeenCalledWith(
|
expect(addEventListenerMock).toHaveBeenCalledWith(
|
||||||
'mousedown',
|
'pointerdown',
|
||||||
expect.any(Function),
|
expect.any(Function),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update element transform on mousemove', () => {
|
it('should update element transform on pointermove', () => {
|
||||||
const mouseDownEvent = new MouseEvent('mousedown', {
|
const pointerDownEvent = createPointerEvent('pointerdown', {
|
||||||
clientX: 100,
|
clientX: 100,
|
||||||
clientY: 100,
|
clientY: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
dragHandler(element);
|
dragHandler(element);
|
||||||
|
|
||||||
element.dispatchEvent(mouseDownEvent);
|
element.dispatchEvent(pointerDownEvent);
|
||||||
|
|
||||||
const mouseMoveEvent = new MouseEvent('mousemove', {
|
const pointerMoveEvent = createPointerEvent('pointermove', {
|
||||||
clientX: 150,
|
clientX: 150,
|
||||||
clientY: 150,
|
clientY: 150,
|
||||||
});
|
});
|
||||||
document.dispatchEvent(mouseMoveEvent);
|
element.dispatchEvent(pointerMoveEvent);
|
||||||
|
|
||||||
expect(element.style.transform).toBe('translate(50px, 50px)');
|
expect(element.style.transform).toBe('translate(50px, 50px)');
|
||||||
|
|
||||||
const mouseMoveEvent2 = new MouseEvent('mousemove', {
|
const pointerMoveEvent2 = createPointerEvent('pointermove', {
|
||||||
clientX: 200,
|
clientX: 200,
|
||||||
clientY: 200,
|
clientY: 200,
|
||||||
});
|
});
|
||||||
document.dispatchEvent(mouseMoveEvent2);
|
element.dispatchEvent(pointerMoveEvent2);
|
||||||
|
|
||||||
expect(element.style.transform).toBe('translate(100px, 100px)');
|
expect(element.style.transform).toBe('translate(100px, 100px)');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove event listeners on mouseup', () => {
|
it('should run lifecycle callbacks on drag start/end', () => {
|
||||||
const removeEventListenerMock = vi.spyOn(document, 'removeEventListener');
|
const start = vi.fn();
|
||||||
|
const end = vi.fn();
|
||||||
const mouseDownEvent = new MouseEvent('mousedown', {
|
const pointerDownEvent = createPointerEvent('pointerdown', {
|
||||||
clientX: 100,
|
clientX: 100,
|
||||||
clientY: 100,
|
clientY: 100,
|
||||||
|
pointerId: 2,
|
||||||
|
});
|
||||||
|
const pointerUpEvent = createPointerEvent('pointerup', {
|
||||||
|
pointerId: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
dragHandler(element);
|
dragHandler(element, element, undefined, { start, end });
|
||||||
|
element.dispatchEvent(pointerDownEvent);
|
||||||
|
element.dispatchEvent(pointerUpEvent);
|
||||||
|
|
||||||
element.dispatchEvent(mouseDownEvent);
|
expect(start).toHaveBeenCalledTimes(1);
|
||||||
|
expect(end).toHaveBeenCalledTimes(1);
|
||||||
|
expect(element.releasePointerCapture).toHaveBeenCalledWith(2);
|
||||||
|
});
|
||||||
|
|
||||||
const mouseUpEvent = new MouseEvent('mouseup');
|
it('should ignore pointerdown events from button targets', () => {
|
||||||
document.dispatchEvent(mouseUpEvent);
|
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(
|
expect(removeEventListenerMock).toHaveBeenCalledWith(
|
||||||
'mousemove',
|
'pointerdown',
|
||||||
expect.any(Function),
|
expect.any(Function),
|
||||||
);
|
);
|
||||||
expect(removeEventListenerMock).toHaveBeenCalledWith(
|
expect(removeEventListenerMock).toHaveBeenCalledWith(
|
||||||
'mouseup',
|
'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),
|
expect.any(Function),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,14 +1,36 @@
|
|||||||
import { parseTranslate } from './parse-translate';
|
import { parseTranslate } from './parse-translate';
|
||||||
|
|
||||||
|
type DragHandlerLifecycle = {
|
||||||
|
start?: () => void;
|
||||||
|
end?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
export const dragHandler = (
|
export const dragHandler = (
|
||||||
el: HTMLElement,
|
el: HTMLElement,
|
||||||
target: HTMLElement = el,
|
target: HTMLElement = el,
|
||||||
move?: () => void,
|
move?: () => void,
|
||||||
|
lifecycle?: DragHandlerLifecycle,
|
||||||
) => {
|
) => {
|
||||||
let initialTranslate = { x: 0, y: 0 };
|
let initialTranslate = { x: 0, y: 0 };
|
||||||
let initialClientPosition = { 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 handleMouseMove = (moveEvent: MouseEvent) => {
|
|
||||||
const { clientX: moveX, clientY: moveY } = moveEvent;
|
const { clientX: moveX, clientY: moveY } = moveEvent;
|
||||||
const deltaX = moveX - initialClientPosition.x + initialTranslate.x;
|
const deltaX = moveX - initialClientPosition.x + initialTranslate.x;
|
||||||
const deltaY = moveY - initialClientPosition.y + initialTranslate.y;
|
const deltaY = moveY - initialClientPosition.y + initialTranslate.y;
|
||||||
@ -18,19 +40,70 @@ export const dragHandler = (
|
|||||||
move?.();
|
move?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
const handlePointerUp = (upEvent: PointerEvent) => {
|
||||||
document.removeEventListener('mousemove', handleMouseMove);
|
if (upEvent.pointerId !== pointerId) {
|
||||||
document.removeEventListener('mouseup', handleMouseUp);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.hasPointerCapture(upEvent.pointerId)) {
|
||||||
|
el.releasePointerCapture(upEvent.pointerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
endDrag();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseDown = (e: MouseEvent) => {
|
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 };
|
initialClientPosition = { x: e.clientX, y: e.clientY };
|
||||||
initialTranslate = parseTranslate(target);
|
initialTranslate = parseTranslate(target);
|
||||||
document.addEventListener('mousemove', handleMouseMove);
|
|
||||||
document.addEventListener('mouseup', handleMouseUp);
|
dragging = true;
|
||||||
|
pointerId = e.pointerId;
|
||||||
|
lifecycle?.start?.();
|
||||||
|
|
||||||
|
el.setPointerCapture(e.pointerId);
|
||||||
};
|
};
|
||||||
|
|
||||||
el.addEventListener('mousedown', handleMouseDown);
|
el.addEventListener('pointerdown', handlePointerDown);
|
||||||
|
el.addEventListener('pointermove', handlePointerMove);
|
||||||
|
el.addEventListener('pointerup', handlePointerUp);
|
||||||
|
el.addEventListener('pointercancel', handlePointerCancel);
|
||||||
|
el.addEventListener('lostpointercapture', handleLostPointerCapture);
|
||||||
|
|
||||||
return handleMouseUp;
|
return () => {
|
||||||
|
el.removeEventListener('pointerdown', handlePointerDown);
|
||||||
|
el.removeEventListener('pointermove', handlePointerMove);
|
||||||
|
el.removeEventListener('pointerup', handlePointerUp);
|
||||||
|
el.removeEventListener('pointercancel', handlePointerCancel);
|
||||||
|
el.removeEventListener('lostpointercapture', handleLostPointerCapture);
|
||||||
|
endDrag();
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
125
plugins/libs/plugins-runtime/src/lib/modal/plugin-modal.spec.ts
Normal file
125
plugins/libs/plugins-runtime/src/lib/modal/plugin-modal.spec.ts
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import './plugin-modal.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('PluginModalElement', () => {
|
||||||
|
let setPointerCaptureSpy: ReturnType<typeof vi.fn>;
|
||||||
|
let releasePointerCaptureSpy: ReturnType<typeof vi.fn>;
|
||||||
|
let hasPointerCaptureSpy: ReturnType<typeof vi.fn>;
|
||||||
|
let originalSetPointerCapture: typeof HTMLElement.prototype.setPointerCapture;
|
||||||
|
let originalReleasePointerCapture: typeof HTMLElement.prototype.releasePointerCapture;
|
||||||
|
let originalHasPointerCapture: typeof HTMLElement.prototype.hasPointerCapture;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
originalSetPointerCapture = HTMLElement.prototype.setPointerCapture;
|
||||||
|
originalReleasePointerCapture = HTMLElement.prototype.releasePointerCapture;
|
||||||
|
originalHasPointerCapture = HTMLElement.prototype.hasPointerCapture;
|
||||||
|
|
||||||
|
setPointerCaptureSpy = vi.fn();
|
||||||
|
releasePointerCaptureSpy = vi.fn();
|
||||||
|
hasPointerCaptureSpy = vi.fn().mockReturnValue(true);
|
||||||
|
|
||||||
|
Object.defineProperty(HTMLElement.prototype, 'setPointerCapture', {
|
||||||
|
configurable: true,
|
||||||
|
value: setPointerCaptureSpy,
|
||||||
|
});
|
||||||
|
Object.defineProperty(HTMLElement.prototype, 'releasePointerCapture', {
|
||||||
|
configurable: true,
|
||||||
|
value: releasePointerCaptureSpy,
|
||||||
|
});
|
||||||
|
Object.defineProperty(HTMLElement.prototype, 'hasPointerCapture', {
|
||||||
|
configurable: true,
|
||||||
|
value: hasPointerCaptureSpy,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
Object.defineProperty(HTMLElement.prototype, 'setPointerCapture', {
|
||||||
|
configurable: true,
|
||||||
|
value: originalSetPointerCapture,
|
||||||
|
});
|
||||||
|
Object.defineProperty(HTMLElement.prototype, 'releasePointerCapture', {
|
||||||
|
configurable: true,
|
||||||
|
value: originalReleasePointerCapture,
|
||||||
|
});
|
||||||
|
Object.defineProperty(HTMLElement.prototype, 'hasPointerCapture', {
|
||||||
|
configurable: true,
|
||||||
|
value: originalHasPointerCapture,
|
||||||
|
});
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not start dragging on close button pointerdown', () => {
|
||||||
|
const modal = document.createElement('plugin-modal');
|
||||||
|
modal.setAttribute('title', 'Test modal');
|
||||||
|
modal.setAttribute('iframe-src', 'about:blank');
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
|
const shadow = modal.shadowRoot;
|
||||||
|
expect(shadow).toBeTruthy();
|
||||||
|
|
||||||
|
const wrapper = shadow?.querySelector<HTMLElement>('.wrapper');
|
||||||
|
const closeButton = shadow?.querySelector<HTMLElement>('button');
|
||||||
|
|
||||||
|
expect(wrapper).toBeTruthy();
|
||||||
|
expect(closeButton).toBeTruthy();
|
||||||
|
|
||||||
|
closeButton?.dispatchEvent(
|
||||||
|
createPointerEvent('pointerdown', {
|
||||||
|
pointerId: 11,
|
||||||
|
button: 0,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(wrapper?.classList.contains('is-dragging')).toBe(false);
|
||||||
|
expect(setPointerCaptureSpy).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
modal.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should dispatch close event when close button is clicked', () => {
|
||||||
|
const modal = document.createElement('plugin-modal');
|
||||||
|
modal.setAttribute('title', 'Test modal');
|
||||||
|
modal.setAttribute('iframe-src', 'about:blank');
|
||||||
|
|
||||||
|
const onClose = vi.fn();
|
||||||
|
modal.addEventListener('close', onClose);
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
|
const closeButton = modal.shadowRoot?.querySelector<HTMLElement>('button');
|
||||||
|
expect(closeButton).toBeTruthy();
|
||||||
|
|
||||||
|
closeButton?.dispatchEvent(
|
||||||
|
new MouseEvent('click', {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(onClose).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
modal.remove();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -67,11 +67,6 @@ export class PluginModalElement extends HTMLElement {
|
|||||||
this.wrapper.style.maxInlineSize = '90vw';
|
this.wrapper.style.maxInlineSize = '90vw';
|
||||||
this.wrapper.style.maxBlockSize = '90vh';
|
this.wrapper.style.maxBlockSize = '90vh';
|
||||||
|
|
||||||
// move modal to the top
|
|
||||||
this.#dragEvents = dragHandler(this.#inner, this.wrapper, () => {
|
|
||||||
this.calculateZIndex();
|
|
||||||
});
|
|
||||||
|
|
||||||
const header = document.createElement('div');
|
const header = document.createElement('div');
|
||||||
header.classList.add('header');
|
header.classList.add('header');
|
||||||
|
|
||||||
@ -124,6 +119,23 @@ export class PluginModalElement extends HTMLElement {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// move modal to the top
|
||||||
|
this.#dragEvents = dragHandler(
|
||||||
|
header,
|
||||||
|
this.wrapper,
|
||||||
|
() => {
|
||||||
|
this.calculateZIndex();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: () => {
|
||||||
|
this.wrapper.classList.add('is-dragging');
|
||||||
|
},
|
||||||
|
end: () => {
|
||||||
|
this.wrapper.classList.remove('is-dragging');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
this.addEventListener('message', (e: Event) => {
|
this.addEventListener('message', (e: Event) => {
|
||||||
if (!iframe.contentWindow) {
|
if (!iframe.contentWindow) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -42,6 +42,8 @@
|
|||||||
min-inline-size: 25px;
|
min-inline-size: 25px;
|
||||||
min-block-size: 200px;
|
min-block-size: 200px;
|
||||||
resize: both;
|
resize: both;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
&:after {
|
&:after {
|
||||||
content: '';
|
content: '';
|
||||||
cursor: se-resize;
|
cursor: se-resize;
|
||||||
@ -58,7 +60,6 @@
|
|||||||
|
|
||||||
.inner {
|
.inner {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
cursor: grab;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -78,6 +79,12 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
border-block-end: 2px solid var(--color-background-quaternary);
|
border-block-end: 2px solid var(--color-background-quaternary);
|
||||||
padding-block-end: var(--spacing-4);
|
padding-block-end: var(--spacing-4);
|
||||||
|
cursor: grab;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper.is-dragging .header {
|
||||||
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@ -92,7 +99,6 @@ h1 {
|
|||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-bold);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-inline-end: var(--spacing-4);
|
margin-inline-end: var(--spacing-4);
|
||||||
user-select: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
iframe {
|
iframe {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user