feat(datagrid): 支持列宽随拖动实时调整

- 在动画帧内同步提交列宽,使表头与表体实时重排
- 合并高频鼠标事件并跳过重复像素宽度更新
- 增加拖动期间实时更新的交互回归测试
This commit is contained in:
AutumnNazi
2026-07-31 14:26:03 +08:00
parent 6ed3597262
commit 24f7813f58
2 changed files with 61 additions and 21 deletions

View File

@@ -88,6 +88,12 @@ describe('useDataGridColumnResize interaction cleanup', () => {
expect(update({ name: 120 })).toEqual({ name: width });
};
const flushAnimationFrames = () => {
const callbacks = [...scheduledFrames.values()];
scheduledFrames.clear();
callbacks.forEach((callback) => callback(0));
};
beforeEach(() => {
vi.useFakeTimers();
scheduledFrames = new Map();
@@ -164,6 +170,17 @@ describe('useDataGridColumnResize interaction cleanup', () => {
expect(resize?.isResizingRef.current).toBe(false);
});
it('updates the complete table width on the animation frame while dragging', () => {
beginResize();
act(() => fakeDocument.dispatch('mousemove', { buttons: 1, clientX: 230 }));
expect(setColumnWidths).not.toHaveBeenCalled();
act(() => flushAnimationFrames());
expectLastWidthUpdate(150);
expect(ghost.style.transform).toBe('translateX(190px)');
});
it('self-heals when movement reports no pressed button', () => {
beginResize();

View File

@@ -11,12 +11,26 @@ const ROW_NUMBER_MIN_WIDTH = 28;
const ROW_NUMBER_MAX_WIDTH = 120;
type UseDataGridColumnResizeContext = Record<string, any>;
type ColumnResizeDragState = {
startX: number;
startWidth: number;
key: string;
containerLeft: number;
};
type ColumnResizeListeners = {
blur: () => void;
move: (event: MouseEvent) => void;
up: (event: MouseEvent) => void;
};
const resolveColumnResizeWidth = (dragState: ColumnResizeDragState, clientX: number): number => {
const deltaX = clientX - dragState.startX;
const isRowNumberColumn = dragState.key === GONAVI_ROW_NUMBER_COLUMN_KEY;
const minWidth = isRowNumberColumn ? ROW_NUMBER_MIN_WIDTH : MIN_DATA_TABLE_COLUMN_WIDTH;
const maxWidth = isRowNumberColumn ? ROW_NUMBER_MAX_WIDTH : Number.POSITIVE_INFINITY;
return Math.min(maxWidth, Math.max(minWidth, dragState.startWidth + deltaX));
};
export const useDataGridColumnResize = (ctx: UseDataGridColumnResizeContext) => {
const {
columnMetaMap,
@@ -33,12 +47,7 @@ export const useDataGridColumnResize = (ctx: UseDataGridColumnResizeContext) =>
showColumnType,
} = ctx;
const draggingRef = useRef<{
startX: number;
startWidth: number;
key: string;
containerLeft: number;
} | null>(null);
const draggingRef = useRef<ColumnResizeDragState | null>(null);
const ghostRef = useRef<HTMLDivElement>(null);
const resizeRafRef = useRef<number | null>(null);
const latestClientXRef = useRef<number | null>(null);
@@ -47,17 +56,34 @@ export const useDataGridColumnResize = (ctx: UseDataGridColumnResizeContext) =>
const resizeBodyStyleRef = useRef<{ cursor: string; userSelect: string } | null>(null);
const resizeListenersRef = useRef<ColumnResizeListeners | null>(null);
const setColumnWidthsRef = useRef(setColumnWidths);
const lastAppliedResizeWidthRef = useRef<number | null>(null);
const autoFitCanvasRef = useRef<HTMLCanvasElement | null>(null);
setColumnWidthsRef.current = setColumnWidths;
const flushGhostPosition = useCallback(() => {
resizeRafRef.current = null;
if (!draggingRef.current || !ghostRef.current) return;
if (latestClientXRef.current === null) return;
const relativeLeft = latestClientXRef.current - draggingRef.current.containerLeft;
ghostRef.current.style.transform = `translateX(${relativeLeft}px)`;
const applyResizeWidth = useCallback((dragState: ColumnResizeDragState, clientX: number, force = false) => {
const newWidth = resolveColumnResizeWidth(dragState, clientX);
if (!force && lastAppliedResizeWidthRef.current === newWidth) return;
lastAppliedResizeWidthRef.current = newWidth;
setColumnWidthsRef.current((prev: Record<string, number>) => (
prev[dragState.key] === newWidth
? prev
: { ...prev, [dragState.key]: newWidth }
));
}, []);
const flushResizeFrame = useCallback(() => {
resizeRafRef.current = null;
if (!draggingRef.current) return;
if (latestClientXRef.current === null) return;
const dragState = draggingRef.current;
const clientX = latestClientXRef.current;
if (ghostRef.current) {
const relativeLeft = clientX - dragState.containerLeft;
ghostRef.current.style.transform = `translateX(${relativeLeft}px)`;
}
applyResizeWidth(dragState, clientX);
}, [applyResizeWidth]);
const detachResizeListeners = useCallback(() => {
const listeners = resizeListenersRef.current;
if (!listeners) return;
@@ -110,14 +136,10 @@ export const useDataGridColumnResize = (ctx: UseDataGridColumnResizeContext) =>
if (commit && dragState) {
const finalClientX = Number.isFinite(clientX) ? clientX as number : latestClientX ?? dragState.startX;
const deltaX = finalClientX - dragState.startX;
const isRowNumberColumn = dragState.key === GONAVI_ROW_NUMBER_COLUMN_KEY;
const minWidth = isRowNumberColumn ? ROW_NUMBER_MIN_WIDTH : MIN_DATA_TABLE_COLUMN_WIDTH;
const maxWidth = isRowNumberColumn ? ROW_NUMBER_MAX_WIDTH : Number.POSITIVE_INFINITY;
const newWidth = Math.min(maxWidth, Math.max(minWidth, dragState.startWidth + deltaX));
setColumnWidthsRef.current((prev: Record<string, number>) => ({ ...prev, [dragState.key]: newWidth }));
applyResizeWidth(dragState, finalClientX, true);
}
}, [detachResizeListeners, restoreResizeBodyStyles]);
lastAppliedResizeWidthRef.current = null;
}, [applyResizeWidth, detachResizeListeners, restoreResizeBodyStyles]);
const handleResizeStart = useCallback((key: string) => (e: React.MouseEvent) => {
e.preventDefault();
@@ -137,6 +159,7 @@ export const useDataGridColumnResize = (ctx: UseDataGridColumnResizeContext) =>
});
const containerLeft = containerRef.current?.getBoundingClientRect().left ?? 0;
draggingRef.current = { startX, startWidth: currentWidth, key, containerLeft };
lastAppliedResizeWidthRef.current = currentWidth;
latestClientXRef.current = startX;
if (ghostRef.current && containerRef.current) {
@@ -153,7 +176,7 @@ export const useDataGridColumnResize = (ctx: UseDataGridColumnResizeContext) =>
return;
}
if (resizeRafRef.current !== null) return;
resizeRafRef.current = requestAnimationFrame(flushGhostPosition);
resizeRafRef.current = requestAnimationFrame(flushResizeFrame);
};
const handleUp = (event: MouseEvent) => finishResize(event.clientX);
const handleBlur = () => finishResize();
@@ -172,7 +195,7 @@ export const useDataGridColumnResize = (ctx: UseDataGridColumnResizeContext) =>
};
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
}, [columnWidths, containerRef, dataTableDensity, finishResize, flushGhostPosition]);
}, [columnWidths, containerRef, dataTableDensity, finishResize, flushResizeFrame]);
useEffect(() => () => {
finishResize(undefined, false, false);