diff --git a/openspec/changes/aurora-1839-cell-memo/design.md b/openspec/changes/aurora-1839-cell-memo/design.md index ef4e43d..daa242e 100644 --- a/openspec/changes/aurora-1839-cell-memo/design.md +++ b/openspec/changes/aurora-1839-cell-memo/design.md @@ -451,3 +451,7 @@ const pinnedLayoutKey = useMemo( - Ширина измеряется одним `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` на значении фильтра-массива (поведение базы). Цена ошибки: одна лишняя перерисовка на редком действии. +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, поведение как раньше. diff --git a/web/src/components/RealtimeTable/RealtimeTable.jsx b/web/src/components/RealtimeTable/RealtimeTable.jsx index 8b0c398..99245b0 100644 --- a/web/src/components/RealtimeTable/RealtimeTable.jsx +++ b/web/src/components/RealtimeTable/RealtimeTable.jsx @@ -15,7 +15,7 @@ import { toast } from 'react-toastify'; import { SheetModals } from './Modals/SheetModals'; import { additionExpenseRowTable, additionVspRowTable } from './constants/addingRowConfig'; 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 { useLockedCellsStore, useRealtimeActions, useVspOptions } from './contexts/RealtimeContext'; import { useBudgetYear } from './hooks/useBudgetYear'; @@ -39,11 +39,13 @@ import { FormsNoCanAddRow } from './constants/formConfig'; const CONFIG_CACHE = new Map(); -const ROW_VIRTUALIZER_OPTIONS = createFixedRowVirtualizerOptions({ +const ROW_VIRTUALIZER_PARAMS = { rowHeight: getTableRowHeightPx, overscan: 2, extra: { scrollPaddingStart: 0, scrollPaddingEnd: 0 }, -}); +}; +// H-1: функция от table, чтобы initialOffset видел scrollTop контейнера (см. virtualizerOptions.js) +const getRowVirtualizerOptions = ({ table }) => createRowVirtualizerOptionsForTable(table, ROW_VIRTUALIZER_PARAMS); const hasVspDropdown = (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, rowVirtualizerInstanceRef: rowVirtualizerRef, columnVirtualizerInstanceRef: columnVirtualizerRef, - rowVirtualizerOptions: ROW_VIRTUALIZER_OPTIONS, + rowVirtualizerOptions: getRowVirtualizerOptions, onEditingCellChange: (cell) => { // AURORA-1891: защита на случай, если что-то кроме нашего собственного // onClick/onKeyDown откроет редактор для нередактируемой ячейки — diff --git a/web/src/components/RealtimeTable/utils/virtualizerOptions.js b/web/src/components/RealtimeTable/utils/virtualizerOptions.js index 25602f0..d117ca7 100644 --- a/web/src/components/RealtimeTable/utils/virtualizerOptions.js +++ b/web/src/components/RealtimeTable/utils/virtualizerOptions.js @@ -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. // Считается по window.innerWidth, чтобы не читать layout и не вызывать пересчёт стилей. const REM_BASE_PX = 16; diff --git a/web/src/components/RealtimeTable/utils/virtualizerOptions.test.js b/web/src/components/RealtimeTable/utils/virtualizerOptions.test.js index 4ec6735..1ac3b36 100644 --- a/web/src/components/RealtimeTable/utils/virtualizerOptions.test.js +++ b/web/src/components/RealtimeTable/utils/virtualizerOptions.test.js @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { createFixedRowVirtualizerOptions, getRootFontSizePx } from './virtualizerOptions.js'; +import { createFixedRowVirtualizerOptions, createRowVirtualizerOptionsForTable, getRootFontSizePx } from './virtualizerOptions.js'; const throwingElement = new Proxy( {}, @@ -49,3 +49,19 @@ test('getRootFontSizePx повторяет правило App.css', () => { assert.ok(Math.abs(getRootFontSizePx(1600) * 3 - 39.84) < 0.01); 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); +});