From d4736ac096fbde0ca8c70f43d229079f437a0bcd Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Tue, 18 Aug 2026 13:31:45 +0500 Subject: [PATCH] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B0=20=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=BC=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shared/approval/ui/ApprovalMatrix.tsx | 7 ++- .../shared/approval/ui/useFlipRows.ts | 52 ++++++++++++++++++- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/src/webparts/requestPortal/shared/approval/ui/ApprovalMatrix.tsx b/src/webparts/requestPortal/shared/approval/ui/ApprovalMatrix.tsx index f84a6d0..85f70b1 100644 --- a/src/webparts/requestPortal/shared/approval/ui/ApprovalMatrix.tsx +++ b/src/webparts/requestPortal/shared/approval/ui/ApprovalMatrix.tsx @@ -51,7 +51,10 @@ export const ApprovalMatrix: React.FC = ({ // подсвечивается пол-таблицы и непонятно, что именно переехало. Запоминаем // ключи блока ДО перестановки: после неё индексы уже другие. const [activeKeys, setActiveKeys] = React.useState([]); - const registerRow = useFlipRows(activeKeys, rowKeys); + // Обёртка таблицы: её высоту useFlipRows анимирует вместе с переездом строк, + // чтобы пикер и следующая карточка не подпрыгивали при удалении шага. + const scrollRef = React.useRef(null); + const registerRow = useFlipRows(activeKeys, rowKeys, scrollRef); // Строка → шаг, которому она принадлежит. Нужно, чтобы отрисовать ячейку // «Шаг» один раз на весь шаг (rowSpan) и понять, можно ли двигать его выше/ниже. @@ -110,7 +113,7 @@ export const ApprovalMatrix: React.FC = ({
{emptyText}
)} {!waiting && !loading && !error && rows.length > 0 && ( -
+
diff --git a/src/webparts/requestPortal/shared/approval/ui/useFlipRows.ts b/src/webparts/requestPortal/shared/approval/ui/useFlipRows.ts index b436a2b..aca8d59 100644 --- a/src/webparts/requestPortal/shared/approval/ui/useFlipRows.ts +++ b/src/webparts/requestPortal/shared/approval/ui/useFlipRows.ts @@ -109,10 +109,16 @@ function measureTops(nodes: Map): Map // eslint-disable-next-line @rushstack/no-new-null -- null здесь диктует React: ref-колбэк получает именно null при отвязке узла ): (key: string) => (el: HTMLTableRowElement | null) => void { const nodes = React.useRef(new Map()); @@ -122,6 +128,10 @@ export function useFlipRows( const prevTops = React.useRef(new Map()); // Порядок строк на прошлый коммит — см. проверку orderChanged ниже. const prevRowKeys = React.useRef([]); + // Высота обёртки на прошлый коммит. Нужна именно запомненная: layout-эффект + // работает УЖЕ ПОСЛЕ коммита, и к этому моменту высота на экране новая — + // «откуда ехать» из текущей раскладки не узнать. + const prevHeight = React.useRef(undefined); // Свои анимации держим сами, чтобы отменять ИМЕННО их: el.getAnimations() // вернул бы заодно чужие (CSS-переходы и т.п.). const running = React.useRef([]); @@ -157,10 +167,13 @@ export function useFlipRows( rowKeys.length !== prevRowKeys.current.length || rowKeys.some((k, i) => k !== prevRowKeys.current[i]); prevRowKeys.current = rowKeys; + const container = containerRef ? containerRef.current : null; + if (!orderChanged) { // Порядок тот же — просто обновляем базовую геометрию для следующего // ДЕЙСТВИТЕЛЬНОГО сравнения, без единой анимации на этом проходе. prevTops.current = measureTops(nodes.current); + prevHeight.current = container ? container.getBoundingClientRect().height : undefined; return; } @@ -168,6 +181,10 @@ export function useFlipRows( // она в этот момент ещё летит, и новая анимация обязана стартовать от её // текущего положения на экране, иначе будет рывок. const visualTops = measureTops(nodes.current); + // То же и для высоты обёртки: если анимация высоты ещё идёт, здесь мы + // получим её промежуточное значение (после cancel ниже высота схлопнется к + // натуральной, и «откуда ехать» было бы уже не восстановить). + const visualHeight = container ? container.getBoundingClientRect().height : undefined; running.current.forEach((a) => a.cancel()); running.current = []; @@ -196,7 +213,38 @@ export function useFlipRows( prevTops.current = current; - if (prefersReducedMotion() || (moved.length === 0 && entered.length === 0)) return; + // Высота: «куда» — натуральная (анимации уже отменены). «Откуда» — то, что + // было видно: промежуточное значение недоигранной анимации, если она шла + // (visualHeight разошёлся с натуральным), иначе высота прошлого коммита. + const heightTo = container ? container.getBoundingClientRect().height : undefined; + const heightFrom = + visualHeight !== undefined && heightTo !== undefined && Math.abs(visualHeight - heightTo) >= MIN_SHIFT_PX + ? visualHeight + : prevHeight.current; + prevHeight.current = heightTo; + const heightChanged = + !!container && + heightFrom !== undefined && + heightTo !== undefined && + Math.abs(heightFrom - heightTo) >= MIN_SHIFT_PX; + + // heightChanged в условии — отдельным слагаемым: при удалении ПОСЛЕДНЕЙ + // строки ехать некому (moved и entered пусты), но высота всё равно + // меняется, и без анимации нижняя карточка снова подпрыгнула бы. + if (prefersReducedMotion() || (moved.length === 0 && entered.length === 0 && !heightChanged)) return; + + if (heightChanged && container) { + // Та же длительность и easing, что у переезда строк: нижняя граница + // обёртки должна идти ровно за последней строкой, иначе рассинхрон видно. + // Обрезку лишнего на время анимации берёт на себя overflow-y: hidden + // (см. .matrixScroll в approval.module.scss). + running.current.push( + container.animate([{ height: `${heightFrom as number}px` }, { height: `${heightTo as number}px` }], { + duration: MOVE_MS, + easing: MOVE_EASING, + }) + ); + } moved.forEach(({ el, key, shift }) => { const isActive = activeKeys.indexOf(key) !== -1;