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'; import { useCallback, useEffect, useRef, useState } from 'react';
const ColumnResizer = ({ reportDragDeltaX }) => { const ColumnResizer = ({ columnId, reportDragDeltaX, onDragStart, onDragEnd }) => {
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const isDraggingRef = useRef(false);
const lastXRef = useRef(0); 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) => { const handleMouseDown = (e) => {
if (e.button !== 0) return; if (e.button !== 0) return;
e.preventDefault(); e.preventDefault();
isDraggingRef.current = true;
setIsDragging(true); setIsDragging(true);
lastXRef.current = e.clientX; lastXRef.current = e.clientX;
onDragStart?.();
}; };
const handleMouseMove = useCallback( const handleMouseMove = useCallback(
@ -25,24 +39,27 @@ const ColumnResizer = ({ reportDragDeltaX }) => {
[isDragging, reportDragDeltaX], [isDragging, reportDragDeltaX],
); );
const handleMouseUp = useCallback(() => {
setIsDragging(false);
}, []);
useEffect(() => { useEffect(() => {
if (isDragging) { if (!isDragging) return;
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp); window.addEventListener('mousemove', handleMouseMove);
} window.addEventListener('mouseup', finishDrag);
window.addEventListener('blur', finishDrag);
return () => { return () => {
window.removeEventListener('mousemove', handleMouseMove); 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 ( return (
<div <div
data-col-resize-id={columnId}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
style={{ style={{
position: 'absolute', position: 'absolute',

View File

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