perf(table): ColumnResizer сообщает MRT начало и конец перетаскивания ширины

isResizingColumn выставляется на mousedown и снимается по mouseup, blur окна
и размонтированию: пока тянут, MRT замораживает тело таблицы (design.md D3).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-28 21:18:44 +04:00
parent 61cafb44a6
commit e5f4b3b965
2 changed files with 35 additions and 12 deletions

View File

@ -1,14 +1,28 @@
import { useCallback, useEffect, useRef, useState } from 'react';
const ColumnResizer = ({ reportDragDeltaX }) => {
const ColumnResizer = ({ columnId, reportDragDeltaX, onDragStart, onDragEnd }) => {
const [isDragging, setIsDragging] = useState(false);
const isDraggingRef = useRef(false);
const lastXRef = useRef(0);
// HeaderCell создаёт колбэки на каждый рендер: держим последний в ref.
const onDragEndRef = useRef(onDragEnd);
onDragEndRef.current = onDragEnd;
// Единая точка завершения (mouseup, blur окна, размонтирование); идемпотентна.
const finishDrag = useCallback(() => {
if (!isDraggingRef.current) return;
isDraggingRef.current = false;
setIsDragging(false);
onDragEndRef.current?.();
}, []);
const handleMouseDown = (e) => {
if (e.button !== 0) return;
e.preventDefault();
isDraggingRef.current = true;
setIsDragging(true);
lastXRef.current = e.clientX;
onDragStart?.();
};
const handleMouseMove = useCallback(
@ -25,24 +39,27 @@ const ColumnResizer = ({ reportDragDeltaX }) => {
[isDragging, reportDragDeltaX],
);
const handleMouseUp = useCallback(() => {
setIsDragging(false);
}, []);
useEffect(() => {
if (isDragging) {
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
}
if (!isDragging) return;
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', finishDrag);
window.addEventListener('blur', finishDrag);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
window.removeEventListener('mouseup', finishDrag);
window.removeEventListener('blur', finishDrag);
};
}, [isDragging, handleMouseMove, handleMouseUp]);
}, [isDragging, handleMouseMove, finishDrag]);
// Размонтирование посреди перетаскивания не должно оставить MRT с isResizingColumn.
// Отдельный эффект: очистка эффекта подписок срабатывает при каждой смене handleMouseMove.
useEffect(() => () => finishDrag(), [finishDrag]);
return (
<div
data-col-resize-id={columnId}
onMouseDown={handleMouseDown}
style={{
position: 'absolute',

View File

@ -2,6 +2,7 @@ import React, { useMemo } from 'react';
import ColumnResizer from './ColumnResizer';
import { formatHeaderYear } from '../utils/headerYearUtils';
import { getOrderedVisibleColumns } from '../utils/columnLayoutUtils';
import { endColumnResize, startColumnResize } from '../utils/columnResizeInfo';
const getColumnPinnedStyles = (column) => {
const pinningPosition = column.getIsPinned();
@ -174,7 +175,12 @@ const HeaderCell = ({ header, table, pinningPosition, onClick, onChangeWidth, bu
}}>
{header.isPlaceholder ? null : formatHeaderYear(column.columnDef.header, budgetYear)}
</div>
<ColumnResizer reportDragDeltaX={handleChangeWidth} />
<ColumnResizer
columnId={column.id}
reportDragDeltaX={handleChangeWidth}
onDragStart={() => table.setColumnSizingInfo(startColumnResize(column.id))}
onDragEnd={() => table.setColumnSizingInfo(endColumnResize)}
/>
</div>
</div>
);