按右键按下时的命中目标决定是否弹节点菜单

PreviewWorkspace.tsx 在右键按下时记住命中元素,抬起时改成按它解析节点与树:指针被视口捕获后 pointerup 的 target 是捕获元素,原先按抬起目标找不到 [data-node-id],节点菜单永远打不开
指针取消时同步清掉记录的目标
tests/previewRightPanDrag.test.tsx 补「抬起被重定向到捕获元素仍弹菜单」用例
This commit is contained in:
2026-09-24 16:05:16 +08:00
parent ef4601c555
commit 064c26d7c9
2 changed files with 38 additions and 4 deletions
@@ -118,6 +118,9 @@ export function PreviewWorkspace({
const didInitialFitRef = useRef(false);
const panRef = useRef<Extract<DragState, { kind: 'pan' }> | null>(null);
const rightPanRef = useRef<PreviewRightPanGesture | null>(null);
// 右键按下时的命中元素:指针被视口捕获后,pointerup 的 target 会变成捕获元素,
// 必须在按下时把真正的节点/树目标记下来。
const rightPanTargetRef = useRef<EventTarget | null>(null);
const rightPressSequenceRef = useRef(false);
const treeDragRef = useRef<{
treeId: string;
@@ -422,6 +425,7 @@ export function PreviewWorkspace({
event.preventDefault();
event.currentTarget.focus({ preventScroll: true });
event.currentTarget.setPointerCapture(event.pointerId);
rightPanTargetRef.current = event.target;
rightPanRef.current = createPreviewRightPanGesture({
pointerId: event.pointerId,
pointer: { x: event.clientX, y: event.clientY },
@@ -468,8 +472,11 @@ export function PreviewWorkspace({
);
};
const openNodeContextMenuAt = (event: ReactPointerEvent<HTMLDivElement>) => {
const target = resolveRightClickMenuTarget(event.target);
const openNodeContextMenuAt = (
event: ReactPointerEvent<HTMLDivElement>,
pressedTarget: EventTarget | null,
) => {
const target = resolveRightClickMenuTarget(pressedTarget);
if (!target) return;
const layout = treeLayouts.find(
(item) => item.tree.src_ui_design === target.treeId,
@@ -501,7 +508,9 @@ export function PreviewWorkspace({
),
});
if (release.viewport) setViewport(release.viewport);
if (release.openNodeMenu) openNodeContextMenuAt(event);
const pressedTarget = rightPanTargetRef.current;
rightPanTargetRef.current = null;
if (release.openNodeMenu) openNodeContextMenuAt(event, pressedTarget);
return;
}
if (panRef.current?.pointerId !== event.pointerId) return;
@@ -516,7 +525,10 @@ export function PreviewWorkspace({
const cancelsRightPan =
rightPan !== null && rightPan.pointerId === event.pointerId;
const cancelsPan = pan !== null && pan.pointerId === event.pointerId;
if (cancelsRightPan) rightPanRef.current = null;
if (cancelsRightPan) {
rightPanRef.current = null;
rightPanTargetRef.current = null;
}
if (cancelsPan) panRef.current = null;
if (cancelsRightPan || cancelsPan) {
setIsPanning(false);
@@ -292,6 +292,28 @@ describe('PreviewWorkspace right-button pan', () => {
expect(screen.getByRole('menuitem', { name: '新增同级节点' })).toBeTruthy();
});
it('opens the node menu when pointerup is retargeted to the capture element', () => {
const { canvas, child, preview } = renderPreview();
dispatchPointer(child, 'pointerdown', {
button: 2,
clientX: 100,
clientY: 100,
pointerId: 7,
});
// 指针被视口捕获后,浏览器把后续指针事件派发给捕获元素:抬起目标不再是节点,
// 菜单必须按按下时的命中目标裁决。
dispatchPointer(preview, 'pointerup', {
button: 2,
clientX: 101,
clientY: 100,
pointerId: 7,
});
expect(canvas.selectNode).toHaveBeenCalledWith('child');
expect(screen.getByRole('menu')).toBeTruthy();
});
it('keeps an empty-grid right click silent, unlike the left button', () => {
const { canvas, preview } = renderPreview();