修复素材框选捕获丢失后的状态清理
新增 lostpointercapture 指针匹配清理且不重复释放捕获 贯通素材选择 Hook 到素材列表的丢失捕获事件 补充生命周期回归测试和前端拆分文档
This commit is contained in:
@@ -101,7 +101,7 @@
|
||||
- 主视图继续保留上传文件读取、上传占位卡片进度、拖到画布坐标、创建画布图层、工程资源持久化和画布图层清理;素材删除通过 `onDeleteAssets` 回调通知主视图清理关联图层。
|
||||
- 该 hook 有独立单测覆盖素材库加载归一化、401 登录、新建文件夹临时 id 替换、素材移动、删除回调和多选删除,避免后续整理侧栏 JSX 时丢失素材库能力。
|
||||
- `useImageCanvasAssetSelection.ts`
|
||||
- 作为素材选择的唯一状态边界,统一持有选择模式、完整选中集合、范围锚点、可选素材有效性 reconcile、单项 / Shift / 当前可见全选增量、框选几何与框选生命周期,并向批量下载 / 删除只暴露已经按素材顺序解析的 `selectedAssets`;删除入口根据当前 `visibleAssetIds` 识别未显示选择并在执行完整集合删除前弹出危险确认。
|
||||
- 作为素材选择的唯一状态边界,统一持有选择模式、完整选中集合、范围锚点、可选素材有效性 reconcile、单项 / Shift / 当前可见全选增量、框选几何与框选生命周期;框选除 `pointerup / pointercancel` 外必须在匹配的 `lostpointercapture` 到达时只清理框选状态,不得再次释放已经丢失的 capture。该 hook 向批量下载 / 删除只暴露已经按素材顺序解析的 `selectedAssets`;删除入口根据当前 `visibleAssetIds` 识别未显示选择并在执行完整集合删除前弹出危险确认。
|
||||
- 搜索和文件夹折叠只在侧栏产生 `visibleAssetIds` 并传入增量动作,不得直接修改或 reconcile 选择集合;选择行为测试集中在该 hook,素材库 model 不再维护第二套选择状态机。
|
||||
|
||||
## 第十一阶段模块
|
||||
|
||||
@@ -80,6 +80,9 @@ export type ImageCanvasAssetLibraryPanelViewProps = {
|
||||
onAssetMarqueePointerDown: (event: ReactPointerEvent<HTMLDivElement>) => void;
|
||||
onAssetMarqueePointerMove: (event: ReactPointerEvent<HTMLDivElement>) => void;
|
||||
onAssetMarqueePointerUp: (event: ReactPointerEvent<HTMLDivElement>) => void;
|
||||
onAssetMarqueeLostPointerCapture: (
|
||||
event: ReactPointerEvent<HTMLDivElement>,
|
||||
) => void;
|
||||
updateAssetMoveDropFolder: (folderId: string | null) => void;
|
||||
addUploadedFiles: (
|
||||
files: FileList | File[],
|
||||
@@ -136,6 +139,7 @@ export function ImageCanvasAssetLibraryPanelView({
|
||||
onAssetMarqueePointerDown,
|
||||
onAssetMarqueePointerMove,
|
||||
onAssetMarqueePointerUp,
|
||||
onAssetMarqueeLostPointerCapture,
|
||||
updateAssetMoveDropFolder,
|
||||
addUploadedFiles,
|
||||
requestUpload,
|
||||
@@ -191,6 +195,7 @@ export function ImageCanvasAssetLibraryPanelView({
|
||||
onPointerMove={onAssetMarqueePointerMove}
|
||||
onPointerUp={onAssetMarqueePointerUp}
|
||||
onPointerCancel={onAssetMarqueePointerUp}
|
||||
onLostPointerCapture={onAssetMarqueeLostPointerCapture}
|
||||
>
|
||||
{pinnedAssetMoveFolderId ? (
|
||||
<div
|
||||
|
||||
@@ -63,6 +63,7 @@ function createSidebarProps(): ImageCanvasSidebarViewProps {
|
||||
onAssetMarqueePointerDown: vi.fn(),
|
||||
onAssetMarqueePointerMove: vi.fn(),
|
||||
onAssetMarqueePointerUp: vi.fn(),
|
||||
onAssetMarqueeLostPointerCapture: vi.fn(),
|
||||
updateAssetMoveDropFolder: vi.fn(),
|
||||
addUploadedFiles: vi.fn(),
|
||||
requestUpload: vi.fn(),
|
||||
|
||||
@@ -714,6 +714,7 @@ export function ImageCanvasEditorView({
|
||||
handleAssetMarqueePointerDown,
|
||||
handleAssetMarqueePointerMove,
|
||||
handleAssetMarqueePointerUp,
|
||||
handleAssetMarqueeLostPointerCapture,
|
||||
refreshAssetLibrary,
|
||||
} = useImageCanvasAssetLibrary({
|
||||
assetListRef,
|
||||
@@ -2273,6 +2274,7 @@ export function ImageCanvasEditorView({
|
||||
onAssetMarqueePointerDown: handleAssetMarqueePointerDown,
|
||||
onAssetMarqueePointerMove: handleAssetMarqueePointerMove,
|
||||
onAssetMarqueePointerUp: handleAssetMarqueePointerUp,
|
||||
onAssetMarqueeLostPointerCapture: handleAssetMarqueeLostPointerCapture,
|
||||
updateAssetMoveDropFolder,
|
||||
addUploadedFiles,
|
||||
requestUpload,
|
||||
|
||||
@@ -101,6 +101,7 @@ function createSidebarProps(
|
||||
onAssetMarqueePointerDown: vi.fn(),
|
||||
onAssetMarqueePointerMove: vi.fn(),
|
||||
onAssetMarqueePointerUp: vi.fn(),
|
||||
onAssetMarqueeLostPointerCapture: vi.fn(),
|
||||
updateAssetMoveDropFolder: vi.fn(),
|
||||
addUploadedFiles: vi.fn(),
|
||||
requestUpload: vi.fn(),
|
||||
|
||||
@@ -76,6 +76,9 @@ export type ImageCanvasSidebarViewProps = {
|
||||
onAssetMarqueePointerDown: (event: ReactPointerEvent<HTMLDivElement>) => void;
|
||||
onAssetMarqueePointerMove: (event: ReactPointerEvent<HTMLDivElement>) => void;
|
||||
onAssetMarqueePointerUp: (event: ReactPointerEvent<HTMLDivElement>) => void;
|
||||
onAssetMarqueeLostPointerCapture: (
|
||||
event: ReactPointerEvent<HTMLDivElement>,
|
||||
) => void;
|
||||
updateAssetMoveDropFolder: (folderId: string | null) => void;
|
||||
addUploadedFiles: (
|
||||
files: FileList | File[],
|
||||
@@ -148,6 +151,7 @@ export function ImageCanvasSidebarView({
|
||||
onAssetMarqueePointerDown,
|
||||
onAssetMarqueePointerMove,
|
||||
onAssetMarqueePointerUp,
|
||||
onAssetMarqueeLostPointerCapture,
|
||||
updateAssetMoveDropFolder,
|
||||
addUploadedFiles,
|
||||
requestUpload,
|
||||
@@ -335,6 +339,7 @@ export function ImageCanvasSidebarView({
|
||||
onAssetMarqueePointerDown={onAssetMarqueePointerDown}
|
||||
onAssetMarqueePointerMove={onAssetMarqueePointerMove}
|
||||
onAssetMarqueePointerUp={onAssetMarqueePointerUp}
|
||||
onAssetMarqueeLostPointerCapture={onAssetMarqueeLostPointerCapture}
|
||||
updateAssetMoveDropFolder={updateAssetMoveDropFolder}
|
||||
addUploadedFiles={addUploadedFiles}
|
||||
requestUpload={requestUpload}
|
||||
|
||||
@@ -139,6 +139,7 @@ export function useImageCanvasAssetLibrary({
|
||||
handleAssetMarqueePointerDown,
|
||||
handleAssetMarqueePointerMove,
|
||||
handleAssetMarqueePointerUp,
|
||||
handleAssetMarqueeLostPointerCapture,
|
||||
} = useImageCanvasAssetSelection({
|
||||
assetListRef,
|
||||
assets,
|
||||
@@ -539,6 +540,7 @@ export function useImageCanvasAssetLibrary({
|
||||
handleAssetMarqueePointerDown,
|
||||
handleAssetMarqueePointerMove,
|
||||
handleAssetMarqueePointerUp,
|
||||
handleAssetMarqueeLostPointerCapture,
|
||||
refreshAssetLibrary,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { createRef } from 'react';
|
||||
import { createRef, type PointerEvent as ReactPointerEvent } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { EditorAsset } from './ImageCanvasEditorTypes';
|
||||
@@ -229,4 +229,50 @@ describe('useImageCanvasAssetSelection', () => {
|
||||
expect(result.current.isAssetSelectionMode).toBe(false);
|
||||
expect(result.current.selectedAssetIds.size).toBe(0);
|
||||
});
|
||||
|
||||
it('clears only the matching marquee when pointer capture is lost', () => {
|
||||
const assetList = document.createElement('div');
|
||||
const releasePointerCapture = vi.fn();
|
||||
Object.assign(assetList, {
|
||||
setPointerCapture: vi.fn(),
|
||||
releasePointerCapture,
|
||||
});
|
||||
const assetListRef = { current: assetList };
|
||||
const { result } = renderHook(() =>
|
||||
useImageCanvasAssetSelection({
|
||||
assetListRef,
|
||||
assets: [createAsset()],
|
||||
}),
|
||||
);
|
||||
const target = document.createElement('div');
|
||||
|
||||
act(() => result.current.openAssetSelectionMode());
|
||||
act(() =>
|
||||
result.current.handleAssetMarqueePointerDown({
|
||||
target,
|
||||
button: 0,
|
||||
pointerId: 7,
|
||||
clientX: 10,
|
||||
clientY: 20,
|
||||
ctrlKey: false,
|
||||
metaKey: false,
|
||||
preventDefault: vi.fn(),
|
||||
} as unknown as ReactPointerEvent<HTMLDivElement>),
|
||||
);
|
||||
expect(result.current.assetMarquee?.pointerId).toBe(7);
|
||||
|
||||
act(() =>
|
||||
result.current.handleAssetMarqueeLostPointerCapture({
|
||||
pointerId: 8,
|
||||
} as ReactPointerEvent<HTMLDivElement>),
|
||||
);
|
||||
expect(result.current.assetMarquee?.pointerId).toBe(7);
|
||||
act(() =>
|
||||
result.current.handleAssetMarqueeLostPointerCapture({
|
||||
pointerId: 7,
|
||||
} as ReactPointerEvent<HTMLDivElement>),
|
||||
);
|
||||
expect(result.current.assetMarquee).toBeNull();
|
||||
expect(releasePointerCapture).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -433,6 +433,15 @@ export function useImageCanvasAssetSelection({
|
||||
[assetListRef, assetMarquee],
|
||||
);
|
||||
|
||||
const handleAssetMarqueeLostPointerCapture = useCallback(
|
||||
(event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
setAssetMarquee((currentMarquee) =>
|
||||
currentMarquee?.pointerId === event.pointerId ? null : currentMarquee,
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
selectableAssets,
|
||||
selectedAssets,
|
||||
@@ -447,5 +456,6 @@ export function useImageCanvasAssetSelection({
|
||||
handleAssetMarqueePointerDown,
|
||||
handleAssetMarqueePointerMove,
|
||||
handleAssetMarqueePointerUp,
|
||||
handleAssetMarqueeLostPointerCapture,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user