窗口失焦时丢弃未提交的树拖拽

PreviewWorkspace.tsx 的 onWindowBlur 一并清掉 treeDragRef 与树拖拽预览偏移:失焦时浏览器不一定补 pointercancel,陈旧手势会被后续 pointerup 复用并把意外偏移写进文档
tests/previewRightPanDrag.test.tsx 补失焦后抬起不提交偏移的用例
This commit is contained in:
2026-09-24 16:07:56 +08:00
parent 134046f806
commit 76dfd74dea
2 changed files with 280 additions and 240 deletions
@@ -404,6 +404,17 @@ export function PreviewWorkspace({
rightPressSequenceRef.current = false; rightPressSequenceRef.current = false;
rightPanRef.current = null; rightPanRef.current = null;
setIsPanning(false); setIsPanning(false);
// 失焦时浏览器不一定补 pointercancel:丢掉未提交的树拖拽,避免后续指针事件
// 复用陈旧手势把意外偏移写进文档。
const drag = treeDragRef.current;
if (drag) {
treeDragRef.current = null;
setPreviewTreeOffsets((current) => {
const next = new Map(current);
next.delete(drag.treeId);
return next;
});
}
}; };
window.addEventListener('keydown', onKeyDown); window.addEventListener('keydown', onKeyDown);
window.addEventListener('keyup', onKeyUp); window.addEventListener('keyup', onKeyUp);
@@ -263,9 +263,35 @@ describe('PreviewWorkspace tree drag', () => {
}); });
expect(cancelled.canvas.setTreeOffset).not.toHaveBeenCalled(); expect(cancelled.canvas.setTreeOffset).not.toHaveBeenCalled();
}); });
});
describe('PreviewWorkspace right-button pan', () => { it('drops an in-flight tree drag when the window loses focus', () => {
const { canvas, rootNode, treeLayer } = renderTreeDrag();
dispatchPointer(rootNode, 'pointerdown', {
button: 0,
clientX: 10,
clientY: 10,
pointerId: 13,
});
dispatchPointer(treeLayer, 'pointermove', {
clientX: 40,
clientY: 50,
pointerId: 13,
});
act(() => {
window.dispatchEvent(new Event('blur'));
});
dispatchPointer(treeLayer, 'pointerup', {
button: 0,
clientX: 40,
clientY: 50,
pointerId: 13,
});
expect(canvas.setTreeOffset).not.toHaveBeenCalled();
});
describe('PreviewWorkspace right-button pan', () => {
it('pans the viewport on a right drag and never opens the node menu', () => { it('pans the viewport on a right drag and never opens the node menu', () => {
const { canvas, child, container, preview } = renderPreview(); const { canvas, child, container, preview } = renderPreview();
const before = worldTranslate(container); const before = worldTranslate(container);
@@ -348,7 +374,9 @@ describe('PreviewWorkspace right-button pan', () => {
expect(canvas.selectNode).toHaveBeenCalledWith('child'); expect(canvas.selectNode).toHaveBeenCalledWith('child');
expect(screen.getByRole('menu').style.left).toBe('101px'); expect(screen.getByRole('menu').style.left).toBe('101px');
expect(screen.getByRole('menuitem', { name: '新增同级节点' })).toBeTruthy(); expect(
screen.getByRole('menuitem', { name: '新增同级节点' }),
).toBeTruthy();
}); });
it('opens the node menu when pointerup is retargeted to the capture element', () => { it('opens the node menu when pointerup is retargeted to the capture element', () => {
@@ -515,4 +543,5 @@ describe('PreviewWorkspace right-button pan', () => {
expect(canvas.updateNodeTransform).toHaveBeenCalledTimes(1); expect(canvas.updateNodeTransform).toHaveBeenCalledTimes(1);
}); });
});
}); });