按右键按下时的命中目标决定是否弹节点菜单
PreviewWorkspace.tsx 在右键按下时记住命中元素,抬起时改成按它解析节点与树:指针被视口捕获后 pointerup 的 target 是捕获元素,原先按抬起目标找不到 [data-node-id],节点菜单永远打不开 指针取消时同步清掉记录的目标 tests/previewRightPanDrag.test.tsx 补「抬起被重定向到捕获元素仍弹菜单」用例
This commit is contained in:
+16
-4
@@ -118,6 +118,9 @@ export function PreviewWorkspace({
|
|||||||
const didInitialFitRef = useRef(false);
|
const didInitialFitRef = useRef(false);
|
||||||
const panRef = useRef<Extract<DragState, { kind: 'pan' }> | null>(null);
|
const panRef = useRef<Extract<DragState, { kind: 'pan' }> | null>(null);
|
||||||
const rightPanRef = useRef<PreviewRightPanGesture | null>(null);
|
const rightPanRef = useRef<PreviewRightPanGesture | null>(null);
|
||||||
|
// 右键按下时的命中元素:指针被视口捕获后,pointerup 的 target 会变成捕获元素,
|
||||||
|
// 必须在按下时把真正的节点/树目标记下来。
|
||||||
|
const rightPanTargetRef = useRef<EventTarget | null>(null);
|
||||||
const rightPressSequenceRef = useRef(false);
|
const rightPressSequenceRef = useRef(false);
|
||||||
const treeDragRef = useRef<{
|
const treeDragRef = useRef<{
|
||||||
treeId: string;
|
treeId: string;
|
||||||
@@ -422,6 +425,7 @@ export function PreviewWorkspace({
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.currentTarget.focus({ preventScroll: true });
|
event.currentTarget.focus({ preventScroll: true });
|
||||||
event.currentTarget.setPointerCapture(event.pointerId);
|
event.currentTarget.setPointerCapture(event.pointerId);
|
||||||
|
rightPanTargetRef.current = event.target;
|
||||||
rightPanRef.current = createPreviewRightPanGesture({
|
rightPanRef.current = createPreviewRightPanGesture({
|
||||||
pointerId: event.pointerId,
|
pointerId: event.pointerId,
|
||||||
pointer: { x: event.clientX, y: event.clientY },
|
pointer: { x: event.clientX, y: event.clientY },
|
||||||
@@ -468,8 +472,11 @@ export function PreviewWorkspace({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openNodeContextMenuAt = (event: ReactPointerEvent<HTMLDivElement>) => {
|
const openNodeContextMenuAt = (
|
||||||
const target = resolveRightClickMenuTarget(event.target);
|
event: ReactPointerEvent<HTMLDivElement>,
|
||||||
|
pressedTarget: EventTarget | null,
|
||||||
|
) => {
|
||||||
|
const target = resolveRightClickMenuTarget(pressedTarget);
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
const layout = treeLayouts.find(
|
const layout = treeLayouts.find(
|
||||||
(item) => item.tree.src_ui_design === target.treeId,
|
(item) => item.tree.src_ui_design === target.treeId,
|
||||||
@@ -501,7 +508,9 @@ export function PreviewWorkspace({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
if (release.viewport) setViewport(release.viewport);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (panRef.current?.pointerId !== event.pointerId) return;
|
if (panRef.current?.pointerId !== event.pointerId) return;
|
||||||
@@ -516,7 +525,10 @@ export function PreviewWorkspace({
|
|||||||
const cancelsRightPan =
|
const cancelsRightPan =
|
||||||
rightPan !== null && rightPan.pointerId === event.pointerId;
|
rightPan !== null && rightPan.pointerId === event.pointerId;
|
||||||
const cancelsPan = pan !== null && pan.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 (cancelsPan) panRef.current = null;
|
||||||
if (cancelsRightPan || cancelsPan) {
|
if (cancelsRightPan || cancelsPan) {
|
||||||
setIsPanning(false);
|
setIsPanning(false);
|
||||||
|
|||||||
@@ -292,6 +292,28 @@ describe('PreviewWorkspace right-button pan', () => {
|
|||||||
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', () => {
|
||||||
|
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', () => {
|
it('keeps an empty-grid right click silent, unlike the left button', () => {
|
||||||
const { canvas, preview } = renderPreview();
|
const { canvas, preview } = renderPreview();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user