diff --git a/src/webparts/requestPortal/shared/approval/ui/ApprovalMatrix.tsx b/src/webparts/requestPortal/shared/approval/ui/ApprovalMatrix.tsx index 0ed724e..f9d7778 100644 --- a/src/webparts/requestPortal/shared/approval/ui/ApprovalMatrix.tsx +++ b/src/webparts/requestPortal/shared/approval/ui/ApprovalMatrix.tsx @@ -40,7 +40,7 @@ export const ApprovalMatrix: React.FC = ({ // подсвечивается пол-таблицы и непонятно, что именно переехало. Запоминаем // ключи блока ДО перестановки: после неё индексы уже другие. const [activeKeys, setActiveKeys] = React.useState([]); - const registerRow = useFlipRows(activeKeys); + const registerRow = useFlipRows(activeKeys, rowKeys); // Строка → шаг, которому она принадлежит. Нужно, чтобы отрисовать ячейку // «Шаг» один раз на весь шаг (rowSpan) и понять, можно ли двигать его выше/ниже. diff --git a/src/webparts/requestPortal/shared/approval/ui/useFlipRows.ts b/src/webparts/requestPortal/shared/approval/ui/useFlipRows.ts index 9891a9a..869c12e 100644 --- a/src/webparts/requestPortal/shared/approval/ui/useFlipRows.ts +++ b/src/webparts/requestPortal/shared/approval/ui/useFlipRows.ts @@ -72,14 +72,22 @@ function drop(row: HTMLTableRowElement): void { * @param activeKeys ключи строк того шага, который двигает пользователь — * только он подсвечивается и поднимается над остальными (см. ApprovalMatrix). * Для прочих изменений состава (добавление/удаление) — пустой массив. + * @param rowKeys ключи строк в ТЕКУЩЕМ порядке — единственный сигнал, по + * которому решаем, действительно ли что-то переставили (см. ниже, почему + * геометрии для этого недостаточно). */ -// eslint-disable-next-line @rushstack/no-new-null -- null здесь диктует React: ref-колбэк получает именно null при отвязке узла -export function useFlipRows(activeKeys: string[]): (key: string) => (el: HTMLTableRowElement | null) => void { +export function useFlipRows( + activeKeys: string[], + rowKeys: string[] + // eslint-disable-next-line @rushstack/no-new-null -- null здесь диктует React: ref-колбэк получает именно null при отвязке узла +): (key: string) => (el: HTMLTableRowElement | null) => void { const nodes = React.useRef(new Map()); // Колбэк на один и тот же ключ должен быть стабильным, иначе React дёргал бы // ref(null)/ref(el) на каждый рендер и карта узлов пересобиралась бы впустую. const refCache = React.useRef(new Map void>()); const prevTops = React.useRef(new Map()); + // Порядок строк на прошлый коммит — см. проверку orderChanged ниже. + const prevRowKeys = React.useRef([]); // Свои анимации держим сами, чтобы отменять ИМЕННО их: el.getAnimations() // вернул бы заодно чужие (CSS-переходы и т.п.). const running = React.useRef([]); @@ -101,6 +109,29 @@ export function useFlipRows(activeKeys: string[]): (key: string) => (el: HTMLTab // useEffect: инвертировать положение нужно до того, как браузер покажет кадр, // иначе будет заметен скачок. React.useLayoutEffect(() => { + // ВАЖНО: «строка сдвинулась по пикселям» — недостаточный повод анимировать. + // Пример живого бага: заявку отправили на согласование → таблица стала + // read-only → у строк пропали колонки со стрелками/кнопками → ширина + // текстовых колонок изменилась → где-то текст перенёсся по-другому → + // строки СМЕСТИЛИСЬ ПО ВЫСОТЕ, хотя их состав и порядок не менялись вовсе. + // Раньше это трактовалось как «переезд» и запускало translateY на + // случайную величину — на кадре это выглядело как строки, которые на + // мгновение пропадают (уезжают за пределы видимой области) и выскакивает + // скролл. Настоящая перестановка меняет НАБОР/ПОРЯДОК ключей — вот это и + // проверяем, а не сам факт смещения. + const orderChanged = + rowKeys.length !== prevRowKeys.current.length || rowKeys.some((k, i) => k !== prevRowKeys.current[i]); + prevRowKeys.current = rowKeys; + + if (!orderChanged) { + // Порядок тот же — просто обновляем базовую геометрию для следующего + // ДЕЙСТВИТЕЛЬНОГО сравнения, без единой анимации на этом проходе. + const current = new Map(); + nodes.current.forEach((el, key) => current.set(key, el.getBoundingClientRect().top)); + prevTops.current = current; + return; + } + // Сначала запоминаем, где строка ВИДНА сейчас: при быстрых повторных кликах // она в этот момент ещё летит, и новая анимация обязана стартовать от её // текущего положения на экране, иначе будет рывок.