fix(table): прокрутка не сбрасывается наверх при перетаскивании ширины колонки (H-1)

MRT при isResizingColumn подменяет MRT_TableBody на Memo_MRT_TableBody, тело
монтируется заново, виртуализатор строк стартует с initialOffset=0 и сбрасывает
scrollTop. rowVirtualizerOptions теперь функция от table, initialOffset читает
текущий scrollTop контейнера. Воспроизведено на чистом MRT и на test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-11-08 09:25:41 +04:00
parent 6255f4a250
commit 54f5af6148
4 changed files with 36 additions and 5 deletions

View File

@ -451,3 +451,7 @@ const pinnedLayoutKey = useMemo(
- Ширина измеряется одним `boundingBox` на шаге 50 (пик хода), до хода — один вызов; цена — один дополнительный вызов внутри пробы из 100 шагов. - Ширина измеряется одним `boundingBox` на шаге 50 (пик хода), до хода — один вызов; цена — один дополнительный вызов внутри пробы из 100 шагов.
- Цена ошибки: замеры нового сценария несравнимы со снятыми по старой раскладке; сравнение с базой делается тем же сценарием на обеих ветках. - Цена ошибки: замеры нового сценария несравнимы со снятыми по старой раскладке; сравнение с базой делается тем же сценарием на обеих ветках.
22. **2026-10-28 (контроллер, R-6, M-3, O-3) — диапазон `['','']` не нормализуется в ключе; код не меняется.** В TanStack такой диапазон не снимает фильтр колонки, ключ отличается от «без фильтров»; это безвредный лишний сброс. Наблюдение O-3 (вне скоупа, follow-up): `Cell/Cell/Cell.jsx` ~112 `createHighlightedContent` вызывает `q.replace` на значении фильтра-массива (поведение базы). Цена ошибки: одна лишняя перерисовка на редком действии. 22. **2026-10-28 (контроллер, R-6, M-3, O-3) — диапазон `['','']` не нормализуется в ключе; код не меняется.** В TanStack такой диапазон не снимает фильтр колонки, ключ отличается от «без фильтров»; это безвредный лишний сброс. Наблюдение O-3 (вне скоупа, follow-up): `Cell/Cell/Cell.jsx` ~112 `createHighlightedContent` вызывает `q.replace` на значении фильтра-массива (поведение базы). Цена ошибки: одна лишняя перерисовка на редком действии.
23. **2026-11-08 (H-1, замечание PotapovaA в PR #240) — `rowVirtualizerOptions` стал функцией от table с `initialOffset`, читающим `scrollTop` контейнера.**
- Причина: `MRT_Table.tsx` (MRT 3.2.1) при `columnSizingInfo.isResizingColumn` меняет `MRT_TableBody` на `Memo_MRT_TableBody`. Это другой тип элемента, тело монтируется заново на начале и на конце перетаскивания. Виртуализатор строк живёт в теле (`useMRT_RowVirtualizer`), новый стартует с `initialOffset = 0` и в `_willUpdate` вызывает `_scrollToOffset(0)`, прокрутка сбрасывается наверх. Воспроизведено на чистом MRT без нашего кода, с управляемым и неуправляемым `columnSizingInfo`, то есть и на `test`; с `initialOffset` прокрутка сохраняется.
- Не берём: подмену `isResizingColumn` для MRT (ломает заморозку тела, D3/D4).
- Цена ошибки: при нулевом `scrollTop` или отсутствии контейнера `initialOffset` равен 0, поведение как раньше.

View File

@ -15,7 +15,7 @@ import { toast } from 'react-toastify';
import { SheetModals } from './Modals/SheetModals'; import { SheetModals } from './Modals/SheetModals';
import { additionExpenseRowTable, additionVspRowTable } from './constants/addingRowConfig'; import { additionExpenseRowTable, additionVspRowTable } from './constants/addingRowConfig';
import { BASE_TABLE_CONFIG, TABLE_ROW_HEIGHT, TABLE_CONTAINER_SX, getTablePaperStyles, getTableRowHeightPx } from './constants/tableConfig'; import { BASE_TABLE_CONFIG, TABLE_ROW_HEIGHT, TABLE_CONTAINER_SX, getTablePaperStyles, getTableRowHeightPx } from './constants/tableConfig';
import { createFixedRowVirtualizerOptions } from './utils/virtualizerOptions'; import { createRowVirtualizerOptionsForTable } from './utils/virtualizerOptions';
import { getActiveCellSx } from './utils/activeCellCss'; import { getActiveCellSx } from './utils/activeCellCss';
import { useLockedCellsStore, useRealtimeActions, useVspOptions } from './contexts/RealtimeContext'; import { useLockedCellsStore, useRealtimeActions, useVspOptions } from './contexts/RealtimeContext';
import { useBudgetYear } from './hooks/useBudgetYear'; import { useBudgetYear } from './hooks/useBudgetYear';
@ -39,11 +39,13 @@ import { FormsNoCanAddRow } from './constants/formConfig';
const CONFIG_CACHE = new Map(); const CONFIG_CACHE = new Map();
const ROW_VIRTUALIZER_OPTIONS = createFixedRowVirtualizerOptions({ const ROW_VIRTUALIZER_PARAMS = {
rowHeight: getTableRowHeightPx, rowHeight: getTableRowHeightPx,
overscan: 2, overscan: 2,
extra: { scrollPaddingStart: 0, scrollPaddingEnd: 0 }, extra: { scrollPaddingStart: 0, scrollPaddingEnd: 0 },
}); };
// H-1: функция от table, чтобы initialOffset видел scrollTop контейнера (см. virtualizerOptions.js)
const getRowVirtualizerOptions = ({ table }) => createRowVirtualizerOptionsForTable(table, ROW_VIRTUALIZER_PARAMS);
const hasVspDropdown = (columns) => const hasVspDropdown = (columns) =>
columns.some((column) => column.editType === 'vsp_dropdown' || (column.columns?.length && hasVspDropdown(column.columns))); columns.some((column) => column.editType === 'vsp_dropdown' || (column.columns?.length && hasVspDropdown(column.columns)));
@ -451,7 +453,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
enableColumnVirtualization: true, enableColumnVirtualization: true,
rowVirtualizerInstanceRef: rowVirtualizerRef, rowVirtualizerInstanceRef: rowVirtualizerRef,
columnVirtualizerInstanceRef: columnVirtualizerRef, columnVirtualizerInstanceRef: columnVirtualizerRef,
rowVirtualizerOptions: ROW_VIRTUALIZER_OPTIONS, rowVirtualizerOptions: getRowVirtualizerOptions,
onEditingCellChange: (cell) => { onEditingCellChange: (cell) => {
// AURORA-1891: защита на случай, если что-то кроме нашего собственного // AURORA-1891: защита на случай, если что-то кроме нашего собственного
// onClick/onKeyDown откроет редактор для нередактируемой ячейки — // onClick/onKeyDown откроет редактор для нередактируемой ячейки —

View File

@ -17,6 +17,15 @@ export const createFixedRowVirtualizerOptions = ({ rowHeight, overscan, extra =
}; };
}; };
// Опции для rowVirtualizerOptions в форме функции от table. MRT при начале и конце перетаскивания
// ширины подменяет MRT_TableBody на Memo_MRT_TableBody (MRT_Table.tsx), тело монтируется заново,
// новый виртуализатор стартует с initialOffset = 0 и в _willUpdate сбрасывает scrollTop контейнера.
// initialOffset читает текущий scrollTop, и позиция прокрутки переживает перемонтирование (AURORA-1839, H-1).
export const createRowVirtualizerOptionsForTable = (table, params) => ({
...createFixedRowVirtualizerOptions(params),
initialOffset: () => table?.refs?.tableContainerRef?.current?.scrollTop ?? 0,
});
// Размер 1rem в px по правилу App.css: .83vw при ширине окна <= 1920px, иначе 16px. // Размер 1rem в px по правилу App.css: .83vw при ширине окна <= 1920px, иначе 16px.
// Считается по window.innerWidth, чтобы не читать layout и не вызывать пересчёт стилей. // Считается по window.innerWidth, чтобы не читать layout и не вызывать пересчёт стилей.
const REM_BASE_PX = 16; const REM_BASE_PX = 16;

View File

@ -1,6 +1,6 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import { createFixedRowVirtualizerOptions, getRootFontSizePx } from './virtualizerOptions.js'; import { createFixedRowVirtualizerOptions, createRowVirtualizerOptionsForTable, getRootFontSizePx } from './virtualizerOptions.js';
const throwingElement = new Proxy( const throwingElement = new Proxy(
{}, {},
@ -49,3 +49,19 @@ test('getRootFontSizePx повторяет правило App.css', () => {
assert.ok(Math.abs(getRootFontSizePx(1600) * 3 - 39.84) < 0.01); assert.ok(Math.abs(getRootFontSizePx(1600) * 3 - 39.84) < 0.01);
assert.equal(getRootFontSizePx(undefined), 16); assert.equal(getRootFontSizePx(undefined), 16);
}); });
// AURORA-1839 (H-1): MRT при начале/конце перетаскивания ширины подменяет компонент тела таблицы
// (MRT_TableBody <-> Memo_MRT_TableBody), тело монтируется заново, виртуализатор строк создаётся
// с initialOffset = 0 и сбрасывает scrollTop контейнера. initialOffset должен читать текущий scrollTop.
test('createRowVirtualizerOptionsForTable: initialOffset читает scrollTop контейнера таблицы', () => {
const table = { refs: { tableContainerRef: { current: { scrollTop: 30000 } } } };
const options = createRowVirtualizerOptionsForTable(table, { rowHeight: 37, overscan: 2 });
assert.equal(options.initialOffset(), 30000);
table.refs.tableContainerRef.current.scrollTop = 120;
assert.equal(options.initialOffset(), 120);
assert.equal(options.estimateSize(), 37);
});
test('createRowVirtualizerOptionsForTable: без контейнера initialOffset = 0', () => {
assert.equal(createRowVirtualizerOptionsForTable({ refs: { tableContainerRef: { current: null } } }, { rowHeight: 37 }).initialOffset(), 0);
});