修复资源画布浮层里的滚轮被画布消费:@ 选择器列表滚动时画布跟着平移 / 缩放
- 共享浮层判据抽出 isEventInsideFloatingOverlay,并新增 isFloatingOverlayWheelEvent:关闭判定与滚轮归属共用同一份口径(DOM 不在边界里的 portal 浮层 + 已登记浮层) - isEventInsideFloatingMenu 返回值收紧为布尔,避免判据把命中的元素当真值往下传 - 资源画布唯一滚轮入口 handleResourceBookWheel 加统一守卫:浮层里的滚轮一律不消费(不 preventDefault、不动视口),不再逐浮层加 stopPropagation - resourceCanvasFocusModel 增加 RESOURCE_CANVAS_WHEEL_OVERLAY_SELECTOR 与 isResourceCanvasWheelOverlayTarget,登记留在画布 DOM 里自带滚动区的浮层(快速编辑 / 信息 / 筛选) - projectResourceLiveIntegration 新增用例:在选择器列表上派发真实 wheel → 浮层自己收到该事件、画布 data-resource-viewport 不变;对照组场景根派发 wheel → 视口照旧变化 - resourceCanvasFloatingDismiss 新增判据单测:portal 浮层 / 共享弹出层 / 已登记浮层三种来源归浮层,边界里的卡片与空白仍归画布,判不出归属不抢 - pitfalls.md 记录「portal 事件沿 React 树冒泡导致画布误吃滚轮」的根因与排查口径
This commit is contained in:
@@ -38,6 +38,37 @@ export function isResourceCanvasInteractionTarget(
|
||||
return Boolean(target?.closest(RESOURCE_CANVAS_INTERACTION_SELECTOR));
|
||||
}
|
||||
|
||||
/**
|
||||
* 画布浮层里有自己滚动区的那几个:落在它们里面的滚轮归浮层,画布不得消费。
|
||||
*
|
||||
* 词表与 `RESOURCE_CANVAS_INTERACTION_SELECTOR` 的浮层成员同一套类名,只去掉按钮 / 输入 /
|
||||
* 资源卡这类本身不该吞滚轮的交互控件(在卡片上滚轮照旧平移 / 缩放画布):
|
||||
* - `.image-canvas-editor__generation-composer`:快速编辑 / 角色动画浮层(自带提示词滚动区);
|
||||
* - `.game-resource-info-panel`:资源信息浮层(`overflow: auto`);
|
||||
* - `.game-resource-filter-panel`:筛选面板(卡片区 `overflow-y: auto`)。
|
||||
*/
|
||||
export const RESOURCE_CANVAS_WHEEL_OVERLAY_SELECTOR = [
|
||||
'.image-canvas-editor__generation-composer',
|
||||
'.game-resource-info-panel',
|
||||
'.game-resource-filter-panel',
|
||||
].join(', ');
|
||||
|
||||
/**
|
||||
* 这一次滚轮是不是落在「自己吃滚轮的画布浮层」里。
|
||||
*
|
||||
* portal 到 `document.body` 的浮层(`@` 资源选择器与候选菜单、共享模型 / 比例弹出层…)由
|
||||
* `isResourceReferenceOverlayTarget` 与共享 hook 的 portal 弹层判据负责,这里只管留在画布
|
||||
* DOM 里的浮层。
|
||||
*/
|
||||
export function isResourceCanvasWheelOverlayTarget(
|
||||
target: EventTarget | null,
|
||||
): boolean {
|
||||
return (
|
||||
target instanceof Element &&
|
||||
target.closest(RESOURCE_CANVAS_WHEEL_OVERLAY_SELECTOR) !== null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 画布浮层是否可以被清焦点顺手关掉。
|
||||
*
|
||||
|
||||
@@ -88,7 +88,10 @@ import {
|
||||
import { ImageCanvasProjectAssetPickerDialog } from '../../../../../src/components/image-editor/ImageCanvasProjectAssetPickerDialog';
|
||||
import { ImageCanvasQuickEditPanelView } from '../../../../../src/components/image-editor/ImageCanvasQuickEditPanelView';
|
||||
import { ImageCanvasSelectedLayerToolbarView } from '../../../../../src/components/image-editor/ImageCanvasSelectedLayerToolbarView';
|
||||
import { useImageCanvasFloatingOptionDismiss } from '../../../../../src/components/image-editor/useImageCanvasFloatingOptionDismiss';
|
||||
import {
|
||||
isFloatingOverlayWheelEvent,
|
||||
useImageCanvasFloatingOptionDismiss,
|
||||
} from '../../../../../src/components/image-editor/useImageCanvasFloatingOptionDismiss';
|
||||
import { DesignWorkspacePanel } from '../../features/project-workspace/DesignWorkspacePanel';
|
||||
import {
|
||||
LocalGamePreviewFrame,
|
||||
@@ -115,6 +118,7 @@ import {
|
||||
import {
|
||||
canDismissResourceCanvasQuickEdit,
|
||||
isResourceCanvasInteractionTarget,
|
||||
isResourceCanvasWheelOverlayTarget,
|
||||
resolveResourceCanvasFloatingPanelDismissOpen,
|
||||
resolveResourceCanvasFocusEscapeActive,
|
||||
} from '../../features/resource-canvas/resourceCanvasFocusModel';
|
||||
@@ -1120,6 +1124,23 @@ function ResourceBookTitleBar({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 画布滚轮归属的「已登记浮层」总表,与「点外部关闭」用的是同一套浮层口径:
|
||||
* - `@` 资源选择器与候选菜单 portal 到 `document.body`;
|
||||
* - 留在画布 DOM 里、自带滚动区的画布浮层(快速编辑 / 信息 / 筛选,见
|
||||
* `isResourceCanvasWheelOverlayTarget`)。
|
||||
*
|
||||
* 落在它们里面的滚轮归浮层自己,画布不消费(不 preventDefault、不动视口)。
|
||||
*/
|
||||
function isResourceCanvasFloatingOverlayWheelTarget(
|
||||
target: EventTarget | null,
|
||||
) {
|
||||
return (
|
||||
isResourceReferenceOverlayTarget(target) ||
|
||||
isResourceCanvasWheelOverlayTarget(target)
|
||||
);
|
||||
}
|
||||
|
||||
function ResourceBookScene({
|
||||
plan,
|
||||
resourceBookState,
|
||||
@@ -4406,11 +4427,28 @@ export default function ProjectDevelopmentView({
|
||||
|
||||
const handleResourceBookWheel = useCallback(
|
||||
(event: ReactWheelEvent<HTMLDivElement> | WheelEvent) => {
|
||||
resourceBookTransitionControllerRef.current.settle();
|
||||
const sceneElement = resourceBookManagerRef.current;
|
||||
if (!sceneElement) {
|
||||
return;
|
||||
}
|
||||
/*
|
||||
浮层优先:滚轮只在「这一下确实落在资源画布管理区里」时才归画布。
|
||||
|
||||
React 的 portal 事件沿 **React 树** 冒泡(React 把委托监听挂在 portal 容器
|
||||
`document.body` 上),所以 `@` 选择素材浮层里的滚轮也会走到场景根的 `onWheel`
|
||||
这里;而它在 DOM 上根本不在管理区里。判据与上面「点外部关闭」同一份
|
||||
(`isFloatingOverlayWheelEvent` = 浮层边界 + 已登记浮层):用户在选择器列表上
|
||||
滚轮时,列表自己滚,背后的画布不再跟着平移 / 缩放。
|
||||
*/
|
||||
if (
|
||||
isFloatingOverlayWheelEvent(event.target, {
|
||||
boundaryRefs: [resourceBookManagerRef],
|
||||
isInsideExtraOverlay: isResourceCanvasFloatingOverlayWheelTarget,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
resourceBookTransitionControllerRef.current.settle();
|
||||
event.preventDefault();
|
||||
const rect = sceneElement.getBoundingClientRect();
|
||||
const screenPoint = {
|
||||
|
||||
@@ -893,6 +893,93 @@ describe('project resource live canvas integration', () => {
|
||||
expect(deriveCalls[0]?.prompt).toBe(expectedText);
|
||||
});
|
||||
|
||||
/**
|
||||
* 用户报的原始现象:在「快速编辑 → 插入素材引用」开出的选择器列表上滚鼠标滚轮,
|
||||
* 滚的不是列表,而是背后的资源画布(画布跟着缩放 / 平移)。
|
||||
*
|
||||
* 链路:选择器 portal 到 `document.body`,而 React 的 portal 事件沿 **React 树** 冒泡
|
||||
* (React 把委托监听挂在 portal 容器上),所以它的 wheel 照样走到画布场景根的
|
||||
* `onWheel`;修复前那一下会被画布消费掉。这里用真实事件序列钉住「浮层里的滚轮归浮层、
|
||||
* 画布视口一格不动」,同时用对照用例钉住「画布本体的滚轮照旧」。
|
||||
*/
|
||||
it('在选择素材浮层里滚轮:浮层自己收到、画布视口不动,画布本体滚轮照旧', async () => {
|
||||
installTauri();
|
||||
render(<DerivedWorkbench includeArt />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开待归类' }));
|
||||
fireEvent.click(await findResourceSelectButton('source-art.png'));
|
||||
const toolbar = await screen.findByRole('toolbar', {
|
||||
name: '图片工具栏',
|
||||
});
|
||||
fireEvent.click(within(toolbar).getByRole('button', { name: '快速编辑' }));
|
||||
const panel = await screen.findByRole('dialog', {
|
||||
name: '快速编辑图片',
|
||||
});
|
||||
fireEvent.click(
|
||||
within(panel).getByRole('button', { name: '插入素材引用' }),
|
||||
);
|
||||
const picker = await screen.findByRole('dialog', { name: '选择素材' });
|
||||
|
||||
// 前提自检:选择器 DOM 上确实不在资源画本里(这正是 React 事件仍会冒泡到画布的原因)。
|
||||
const manager = document.querySelector('.game-resource-book-manager');
|
||||
expect(manager?.contains(picker)).toBe(false);
|
||||
expect(picker.parentElement).toBe(document.body);
|
||||
|
||||
const list = picker.querySelector('.resource-reference-picker-list');
|
||||
if (!list) throw new Error('missing picker list');
|
||||
const overlayWheelCalls = vi.fn();
|
||||
list.addEventListener('wheel', overlayWheelCalls);
|
||||
|
||||
const readViewport = () =>
|
||||
document
|
||||
.querySelector('[data-resource-viewport]')
|
||||
?.getAttribute('data-resource-viewport');
|
||||
const readSceneWorldTransform = () =>
|
||||
document
|
||||
.querySelector('.game-resource-book-scene-world')
|
||||
?.getAttribute('style');
|
||||
const viewportBefore = readViewport();
|
||||
const sceneWorldBefore = readSceneWorldTransform();
|
||||
expect(viewportBefore).toBeTruthy();
|
||||
|
||||
// 真实事件序列:从浮层内部元素派发滚轮(等同用户在选择器列表上滚)。
|
||||
const overlayWheel = new WheelEvent('wheel', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
deltaY: 240,
|
||||
clientX: 80,
|
||||
clientY: 60,
|
||||
});
|
||||
act(() => {
|
||||
list.dispatchEvent(overlayWheel);
|
||||
});
|
||||
|
||||
// 浮层自己收到该事件、且没被画布消费(列表按原生行为滚动)。
|
||||
expect(overlayWheelCalls).toHaveBeenCalledTimes(1);
|
||||
expect(overlayWheel.defaultPrevented).toBe(false);
|
||||
// 画布视口一格不动。
|
||||
expect(readViewport()).toBe(viewportBefore);
|
||||
expect(readSceneWorldTransform()).toBe(sceneWorldBefore);
|
||||
|
||||
list.removeEventListener('wheel', overlayWheelCalls);
|
||||
|
||||
// 对照用例:画布本体(场景根)上的滚轮必须照旧平移视口——修复没把画布交互一起关掉。
|
||||
const sceneRoot = document.querySelector('.game-resource-book-scene');
|
||||
if (!sceneRoot) throw new Error('missing scene root');
|
||||
const canvasWheel = new WheelEvent('wheel', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
deltaY: 120,
|
||||
clientX: 90,
|
||||
clientY: 70,
|
||||
});
|
||||
act(() => {
|
||||
sceneRoot.dispatchEvent(canvasWheel);
|
||||
});
|
||||
expect(canvasWheel.defaultPrevented).toBe(true);
|
||||
expect(readViewport()).not.toBe(viewportBefore);
|
||||
});
|
||||
|
||||
it('creates a brand new media asset from the canvas generation entry with a create-mode derive request', async () => {
|
||||
const { deriveCalls } = installTauri({ failFirstDerive: true });
|
||||
render(<DerivedWorkbench />);
|
||||
|
||||
@@ -6,11 +6,15 @@ import { afterEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import type { QuickEditPanelState } from '../../../src/components/image-editor/ImageCanvasEditorTypes';
|
||||
import { ImageCanvasQuickEditPanelView } from '../../../src/components/image-editor/ImageCanvasQuickEditPanelView';
|
||||
import { useImageCanvasFloatingOptionDismiss } from '../../../src/components/image-editor/useImageCanvasFloatingOptionDismiss';
|
||||
import {
|
||||
isFloatingOverlayWheelEvent,
|
||||
useImageCanvasFloatingOptionDismiss,
|
||||
} from '../../../src/components/image-editor/useImageCanvasFloatingOptionDismiss';
|
||||
import {
|
||||
canDismissResourceCanvasQuickEdit,
|
||||
isResourceCanvasHostOverlayOpen,
|
||||
isResourceCanvasInteractionTarget,
|
||||
isResourceCanvasWheelOverlayTarget,
|
||||
resolveResourceCanvasFloatingPanelDismissOpen,
|
||||
resolveResourceCanvasFocusEscapeActive,
|
||||
} from '../src/features/resource-canvas/resourceCanvasFocusModel';
|
||||
@@ -315,6 +319,69 @@ describe('资源画布浮层关闭时机', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('画布滚轮归属判据', () => {
|
||||
/**
|
||||
* 与「点外部关闭」同一份口径:portal 出去的东西 DOM 上不在边界里,但 React 的事件会沿
|
||||
* React 树冒泡到画布宿主的 `onWheel`。滚轮这类事件必须按 DOM 判归属,否则用户在
|
||||
* `@` 选择器列表上滚动时,背后的画布会跟着平移 / 缩放。
|
||||
*/
|
||||
test('portal 出去的浮层与已登记浮层归浮层,边界里的画布元素归画布', () => {
|
||||
document.body.innerHTML = `
|
||||
<div id="owner">
|
||||
<div id="card-blank">画布空白</div>
|
||||
<div class="image-canvas-editor__portal-menu"><span id="menu-item">共享弹出层选项</span></div>
|
||||
<div class="resource-reference-picker"><span id="picker-item">选择器条目</span></div>
|
||||
</div>
|
||||
<div id="portal"><span id="portal-item">portal 到 body 的候选项</span></div>
|
||||
`;
|
||||
const ownerRef = { current: document.querySelector<HTMLElement>('#owner') };
|
||||
const options = {
|
||||
boundaryRefs: [ownerRef],
|
||||
isInsideExtraOverlay: (target: EventTarget | null) =>
|
||||
target instanceof Element &&
|
||||
target.closest('.resource-reference-picker') !== null,
|
||||
};
|
||||
const at = (selector: string) =>
|
||||
document.querySelector<HTMLElement>(selector);
|
||||
|
||||
// 画布自己的:DOM 在边界里,也不是浮层。
|
||||
expect(isFloatingOverlayWheelEvent(at('#card-blank'), options)).toBe(false);
|
||||
// portal 到 body 的浮层:DOM 根本不在边界里 ⇒ 归浮层。
|
||||
expect(isFloatingOverlayWheelEvent(at('#portal-item'), options)).toBe(true);
|
||||
// 边界里但落在共享弹出层上:与「点外部关闭」同口径 ⇒ 归浮层。
|
||||
expect(isFloatingOverlayWheelEvent(at('#menu-item'), options)).toBe(true);
|
||||
// 边界里但由宿主登记成浮层(`@` 选择器 / 候选菜单)⇒ 归浮层。
|
||||
expect(isFloatingOverlayWheelEvent(at('#picker-item'), options)).toBe(true);
|
||||
// 判不出归属(没有事件目标)时不抢:归浮层。
|
||||
expect(isFloatingOverlayWheelEvent(null, options)).toBe(true);
|
||||
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
test('自带滚动区的画布浮层归浮层,资源卡与画布空白仍归画布', () => {
|
||||
document.body.innerHTML = `
|
||||
<div class="game-resource-book-manager">
|
||||
<div class="game-resource-card"><span id="card">卡片</span></div>
|
||||
<div id="blank">画布空白</div>
|
||||
<div class="image-canvas-editor__generation-composer"><span id="prompt">快速编辑提示词</span></div>
|
||||
<div class="game-resource-info-panel"><span id="info">信息浮层</span></div>
|
||||
<div class="game-resource-filter-panel"><span id="filter">筛选面板</span></div>
|
||||
</div>
|
||||
`;
|
||||
const at = (selector: string) =>
|
||||
document.querySelector<HTMLElement>(selector);
|
||||
|
||||
expect(isResourceCanvasWheelOverlayTarget(at('#card'))).toBe(false);
|
||||
expect(isResourceCanvasWheelOverlayTarget(at('#blank'))).toBe(false);
|
||||
expect(isResourceCanvasWheelOverlayTarget(at('#prompt'))).toBe(true);
|
||||
expect(isResourceCanvasWheelOverlayTarget(at('#info'))).toBe(true);
|
||||
expect(isResourceCanvasWheelOverlayTarget(at('#filter'))).toBe(true);
|
||||
expect(isResourceCanvasWheelOverlayTarget(null)).toBe(false);
|
||||
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
});
|
||||
|
||||
/** 与 AGC 一样只给必要 props:下拉弹层就地渲染,不走 portal。 */
|
||||
function QuickEditPanelHarness({
|
||||
initialPanel,
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
|
||||
> 当前口径:本文件保留可复用的排障经验;历史条目的旧路由、旧版本和已删除文档仅作根因背景,不得据此恢复退役入口。当前命令、路由和 schema 以代码与 `docs/README.md` 为准。
|
||||
|
||||
## 2026-09-12 画布滚轮要按 DOM 归属判定,portal 出去的浮层不能把滚轮让给画布
|
||||
|
||||
- **现象**:资源卡「快速编辑」里用 `@` 开出「选择素材」浮层后,在选择器列表上滚鼠标滚轮,列表自己在滚,背后的资源画布也一起平移 / 缩放(用户口语:「滚轮还是回滚到画布上」)。
|
||||
- **原因**:选择器与输入区候选菜单 `createPortal(..., document.body)`,DOM 上不在画布管理区里;但 React 的 portal 事件沿 **React 树** 冒泡(React 把委托监听挂在 portal 容器上),所以它们的 `wheel` 照样走到画布场景根的 `onWheel`,被当成画布手势消费。React 的 `wheel` 委托监听是 passive 的:`preventDefault()` 是空操作(只报 warning),真正出问题的是视口状态被改写——所以「事件没被 preventDefault」不能作为「画布没吃这一下」的判据。
|
||||
- **处理**:滚轮归属与「点外部关闭」共用同一份浮层口径(`src/components/image-editor/useImageCanvasFloatingOptionDismiss.ts`:`isEventInsideFloatingOverlay` + `isFloatingOverlayWheelEvent`)——DOM 不在宿主边界里的(portal 出去的一律算浮层)与已登记为浮层内部的都归浮层。资源画布只保留 `handleResourceBookWheel` 一处守卫,留在画布 DOM 里自带滚动区的浮层(快速编辑 / 信息 / 筛选,见 `RESOURCE_CANVAS_WHEEL_OVERLAY_SELECTOR`)按同一入口登记,不要再逐浮层加 `stopPropagation`。判据判不出归属时(没有元素目标)不抢滚轮。
|
||||
- **排查顺序**:先确认浮层是不是 portal 出去的;是的话不要先怀疑 CSS `overflow`、`overscroll-behavior` 或事件被 `preventDefault`,直接查画布宿主上的 `onWheel` / `onPointerDown` 有没有做 DOM 归属判断。
|
||||
- **验证**:`npm run test -- apps/ai-game-creator-shell/tests/resourceCanvasFloatingDismiss.test.tsx`(判据单测,含 portal / 共享弹出层 / 已登记浮层三种来源与「判不出归属不抢」)与 `npm run test -- apps/ai-game-creator-shell/tests/projectResourceLiveIntegration.test.tsx`(真实事件序列:选择器列表里派发 `wheel` → 画布 `data-resource-viewport` 不变、浮层自己收到该事件;对照组:场景根上派发 `wheel` → 视口照旧变化)。
|
||||
- **关联**:`src/components/image-editor/useImageCanvasFloatingOptionDismiss.ts`、`apps/ai-game-creator-shell/src/view/project-development/index.tsx`(`handleResourceBookWheel`)、`apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasFocusModel.ts`
|
||||
|
||||
## 2026-09-12 策划项目重开前必须恢复运行模式
|
||||
|
||||
- 工作台不能只依赖创建时的内存 `startMode`:重开时丢失该值会挂载游戏资源画布,而对话恢复后又进入策划状态,造成左右区域不一致。
|
||||
|
||||
@@ -4,6 +4,12 @@ type FloatingOptionBoundaryRef = {
|
||||
readonly current: HTMLElement | null;
|
||||
};
|
||||
|
||||
/** 浮层边界登记:谁算「浮层内部」,关闭判定与滚轮归属共用同一份。 */
|
||||
type FloatingOverlayBoundaryOptions = {
|
||||
boundaryRefs?: Array<FloatingOptionBoundaryRef | null | undefined>;
|
||||
isInsideExtraOverlay?: (target: EventTarget | null) => boolean;
|
||||
};
|
||||
|
||||
type UseImageCanvasFloatingOptionDismissOptions = {
|
||||
isOpen: boolean;
|
||||
boundaryRefs: Array<FloatingOptionBoundaryRef | null | undefined>;
|
||||
@@ -38,10 +44,62 @@ function isEventInsideBoundary(
|
||||
function isEventInsideFloatingMenu(target: EventTarget | null) {
|
||||
return (
|
||||
target instanceof Element &&
|
||||
target.closest('.image-canvas-editor__portal-menu')
|
||||
target.closest('.image-canvas-editor__portal-menu') !== null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 事件是否落在「已登记为浮层」的那几块 DOM 里。
|
||||
*
|
||||
* 这些浮层 portal 到 `document.body`(共享弹出层)或由宿主额外登记(`isInsideExtraOverlay`),
|
||||
* 关闭判定与滚轮归属共用这一份口径,避免两处各写一套白名单。
|
||||
*/
|
||||
function isEventInsideRegisteredOverlay(
|
||||
target: EventTarget | null,
|
||||
isInsideExtraOverlay?: (target: EventTarget | null) => boolean,
|
||||
) {
|
||||
return (
|
||||
isEventInsideFloatingMenu(target) || isInsideExtraOverlay?.(target) === true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 「点外部关闭」的边界判据:DOM 在边界里(含 portal 出去的浮层登记)就算内部。
|
||||
*/
|
||||
export function isEventInsideFloatingOverlay(
|
||||
target: EventTarget | null,
|
||||
{ boundaryRefs = [], isInsideExtraOverlay }: FloatingOverlayBoundaryOptions,
|
||||
) {
|
||||
return (
|
||||
isEventInsideBoundary(target, boundaryRefs) ||
|
||||
isEventInsideRegisteredOverlay(target, isInsideExtraOverlay)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 「这一下滚轮归浮层还是归画布」的判据,与上面的关闭判定同源。
|
||||
*
|
||||
* React 的 portal 事件沿 **React 树** 冒泡(React 把委托监听挂在 portal 容器 `document.body`
|
||||
* 上),所以 portal 出去的浮层里的滚轮照样会走到宿主的 `onWheel`。DOM 上根本不在宿主里的
|
||||
* 那一下,只可能是浮层派出来的:必须原样放过,否则用户在 `@` 选择器列表上滚轮时,列表
|
||||
* 在滚、背后的画布也一起平移 / 缩放。
|
||||
*
|
||||
* 两条判据:
|
||||
* 1. DOM 不在边界里 —— portal 出去的浮层(`@` 选择器、候选菜单、共享弹出层……),一律归浮层;
|
||||
* 2. DOM 仍在边界里、但已登记为浮层内部的(`isInsideExtraOverlay` 与共享弹出层),同样归浮层。
|
||||
*
|
||||
* 返回 `true` 表示这次滚轮归浮层,宿主不得消费(不 preventDefault、不动视口)。
|
||||
*/
|
||||
export function isFloatingOverlayWheelEvent(
|
||||
target: EventTarget | null,
|
||||
{ boundaryRefs = [], isInsideExtraOverlay }: FloatingOverlayBoundaryOptions,
|
||||
) {
|
||||
if (!isEventInsideBoundary(target, boundaryRefs)) {
|
||||
return true;
|
||||
}
|
||||
return isEventInsideRegisteredOverlay(target, isInsideExtraOverlay);
|
||||
}
|
||||
|
||||
export function useImageCanvasFloatingOptionDismiss({
|
||||
isOpen,
|
||||
boundaryRefs,
|
||||
@@ -57,9 +115,10 @@ export function useImageCanvasFloatingOptionDismiss({
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
// 中文注释:选项项点击后要保留浮层;父级面板其它区域点击才收起。
|
||||
if (
|
||||
isEventInsideBoundary(event.target, boundaryRefs) ||
|
||||
isEventInsideFloatingMenu(event.target) ||
|
||||
isInsideExtraOverlay?.(event.target) === true
|
||||
isEventInsideFloatingOverlay(event.target, {
|
||||
boundaryRefs,
|
||||
isInsideExtraOverlay,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user