mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-15 11:14:31 +08:00
✨ feat(datagrid): 支持列宽随拖动实时调整 (#799)
## 背景 Issue #789 中,表数据与查询结果的列宽只在拖动结束后生效,拖动过程中表格内容不会随分隔线同步调整,操作反馈存在明显延迟。 ## 变更点 - 在 requestAnimationFrame 内提交最新列宽,使表头与完整表体随拖动实时重排 - 合并高频鼠标移动事件并跳过重复像素宽度,降低无效状态更新 - 保留松手时的最终宽度提交,并增加拖动期间实时更新的交互回归测试 ## 影响范围 - 影响共用 DataGrid 的表数据与查询结果列宽拖动交互 - 不涉及后端、数据库结构、连接配置或数据读写逻辑 ## 验证 - npm run test -- src/components/DataGrid.layout.test.tsx src/components/useDataGridColumnResize.interaction.test.tsx(38 passed) - npx tsc --noEmit --pretty false - npm run build - 手工验证表数据与查询结果均可随拖动实时调整 Closes #789
This commit is contained in:
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user