Skip to content

Commit 0569456

Browse files
committed
fix(tables): prevent stuck resize drag when pointer leaves window
1 parent 609acf6 commit 0569456

2 files changed

Lines changed: 124 additions & 10 deletions

File tree

packages/super-editor/src/components/TableResizeOverlay.test.js

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1308,5 +1308,91 @@ describe('TableResizeOverlay', () => {
13081308

13091309
wrapper.unmount();
13101310
});
1311+
1312+
it('should cancel an active column drag on window blur', async () => {
1313+
const editor = createMockEditor();
1314+
const tableElement = createMockTableElement();
1315+
const wrapper = mount(TableResizeOverlay, {
1316+
props: {
1317+
editor,
1318+
visible: true,
1319+
tableElement,
1320+
},
1321+
});
1322+
1323+
await nextTick();
1324+
1325+
const downEvent = new MouseEvent('mousedown', { clientX: 100 });
1326+
Object.defineProperty(downEvent, 'preventDefault', { value: vi.fn() });
1327+
Object.defineProperty(downEvent, 'stopPropagation', { value: vi.fn() });
1328+
wrapper.vm.onHandleMouseDown(downEvent, 0);
1329+
1330+
expect(wrapper.vm.dragState).not.toBeNull();
1331+
expect(editor.view.dom.style.pointerEvents).toBe('none');
1332+
1333+
window.dispatchEvent(new Event('blur'));
1334+
await nextTick();
1335+
1336+
expect(wrapper.vm.dragState).toBeNull();
1337+
expect(editor.view.dom.style.pointerEvents).toBe('auto');
1338+
expect(wrapper.emitted('resize-end')).toBeDefined();
1339+
1340+
wrapper.unmount();
1341+
});
1342+
1343+
it('should cancel an active row drag when the document becomes hidden', async () => {
1344+
const originalVisibilityState = Object.getOwnPropertyDescriptor(document, 'visibilityState');
1345+
Object.defineProperty(document, 'visibilityState', {
1346+
configurable: true,
1347+
value: 'hidden',
1348+
});
1349+
1350+
try {
1351+
const editor = createMockEditor();
1352+
const metadata = {
1353+
columns: [
1354+
{ i: 0, x: 0, w: 100, min: 50, r: 1 },
1355+
{ i: 1, x: 100, w: 150, min: 50, r: 1 },
1356+
],
1357+
rows: [
1358+
{ i: 0, y: 0, h: 50, min: 30, r: 1 },
1359+
{ i: 1, y: 50, h: 50, min: 30, r: 1 },
1360+
],
1361+
};
1362+
const tableElement = createMockTableElement(metadata);
1363+
const wrapper = mount(TableResizeOverlay, {
1364+
props: {
1365+
editor,
1366+
visible: true,
1367+
tableElement,
1368+
},
1369+
});
1370+
1371+
await nextTick();
1372+
1373+
const downEvent = new MouseEvent('mousedown', { clientY: 50 });
1374+
Object.defineProperty(downEvent, 'preventDefault', { value: vi.fn() });
1375+
Object.defineProperty(downEvent, 'stopPropagation', { value: vi.fn() });
1376+
wrapper.vm.onRowHandleMouseDown(downEvent, 0);
1377+
1378+
expect(wrapper.vm.rowDragState).not.toBeNull();
1379+
expect(editor.view.dom.style.pointerEvents).toBe('none');
1380+
1381+
document.dispatchEvent(new Event('visibilitychange'));
1382+
await nextTick();
1383+
1384+
expect(wrapper.vm.rowDragState).toBeNull();
1385+
expect(editor.view.dom.style.pointerEvents).toBe('auto');
1386+
expect(wrapper.emitted('resize-end')).toBeDefined();
1387+
1388+
wrapper.unmount();
1389+
} finally {
1390+
if (originalVisibilityState) {
1391+
Object.defineProperty(document, 'visibilityState', originalVisibilityState);
1392+
} else {
1393+
delete document.visibilityState;
1394+
}
1395+
}
1396+
});
13111397
});
13121398
});

packages/super-editor/src/components/TableResizeOverlay.vue

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,36 @@ const MIN_RESIZE_DELTA_PX = 1;
242242
let rafId = null;
243243
let isUnmounted = false;
244244
245+
function removeInteractionCancelListeners() {
246+
window.removeEventListener('blur', onInteractionCancel);
247+
document.removeEventListener('visibilitychange', onInteractionCancel);
248+
}
249+
250+
function cancelActiveResizeDrag() {
251+
if (!dragState.value && !rowDragState.value) return;
252+
253+
forcedCleanup.value = true;
254+
if (dragState.value) {
255+
onDocumentMouseUp(new MouseEvent('mouseup'));
256+
}
257+
if (rowDragState.value) {
258+
onRowDocumentMouseUp();
259+
}
260+
forcedCleanup.value = false;
261+
}
262+
263+
function onInteractionCancel(event) {
264+
if (!dragState.value && !rowDragState.value) return;
265+
if (event?.type === 'visibilitychange' && document.visibilityState === 'visible') {
266+
return;
267+
}
268+
if (document.visibilityState && document.visibilityState !== 'visible') {
269+
cancelActiveResizeDrag();
270+
return;
271+
}
272+
cancelActiveResizeDrag();
273+
}
274+
245275
/**
246276
* Starts continuous RAF-based tracking of the overlay position.
247277
*
@@ -763,6 +793,8 @@ function onHandleMouseDown(event, resizableBoundaryIndex) {
763793
try {
764794
document.addEventListener('mousemove', onDocumentMouseMove);
765795
document.addEventListener('mouseup', onDocumentMouseUp);
796+
window.addEventListener('blur', onInteractionCancel);
797+
document.addEventListener('visibilitychange', onInteractionCancel);
766798
767799
emit('resize-start', {
768800
columnIndex: boundary.index,
@@ -890,6 +922,7 @@ function onDocumentMouseUp(event) {
890922
// Clean up event listeners and restore pointer events
891923
document.removeEventListener('mousemove', onDocumentMouseMove);
892924
document.removeEventListener('mouseup', onDocumentMouseUp);
925+
removeInteractionCancelListeners();
893926
894927
if (props.editor?.view?.dom) {
895928
const pmView = props.editor.view.dom;
@@ -1130,6 +1163,8 @@ function onRowHandleMouseDown(event, rowBoundaryIndex) {
11301163
11311164
document.addEventListener('mousemove', onRowDocumentMouseMove);
11321165
document.addEventListener('mouseup', onRowDocumentMouseUp);
1166+
window.addEventListener('blur', onInteractionCancel);
1167+
document.addEventListener('visibilitychange', onInteractionCancel);
11331168
11341169
emit('resize-start', { rowIndex: rowBoundary.i });
11351170
}
@@ -1165,6 +1200,7 @@ function onRowDocumentMouseUp() {
11651200
11661201
document.removeEventListener('mousemove', onRowDocumentMouseMove);
11671202
document.removeEventListener('mouseup', onRowDocumentMouseUp);
1203+
removeInteractionCancelListeners();
11681204
11691205
if (props.editor?.view?.dom) {
11701206
props.editor.view.dom.style.pointerEvents = 'auto';
@@ -1286,16 +1322,7 @@ watch(
12861322
} else {
12871323
stopOverlayTracking();
12881324
// Clean up drag state if overlay is hidden
1289-
if (dragState.value) {
1290-
forcedCleanup.value = true;
1291-
onDocumentMouseUp(new MouseEvent('mouseup'));
1292-
forcedCleanup.value = false;
1293-
}
1294-
if (rowDragState.value) {
1295-
forcedCleanup.value = true;
1296-
onRowDocumentMouseUp();
1297-
forcedCleanup.value = false;
1298-
}
1325+
cancelActiveResizeDrag();
12991326
}
13001327
},
13011328
);
@@ -1333,6 +1360,7 @@ onBeforeUnmount(() => {
13331360
props.editor.view.dom.style.pointerEvents = 'auto';
13341361
}
13351362
1363+
removeInteractionCancelListeners();
13361364
window.removeEventListener('scroll', updateOverlayRect, true);
13371365
window.removeEventListener('resize', updateOverlayRect);
13381366
});

0 commit comments

Comments
 (0)