diff --git a/src/webparts/requestPortal/shared/approval/ui/useFlipRows.ts b/src/webparts/requestPortal/shared/approval/ui/useFlipRows.ts index 869c12e..9fc8145 100644 --- a/src/webparts/requestPortal/shared/approval/ui/useFlipRows.ts +++ b/src/webparts/requestPortal/shared/approval/ui/useFlipRows.ts @@ -67,6 +67,39 @@ function drop(row: HTMLTableRowElement): void { }); } +/** + * Положение строк ОТНОСИТЕЛЬНО их , а не окна. + * + * Это принципиально: getBoundingClientRect() отдаёт координату относительно + * ОКНА, а прокрутка страницы не вызывает ре-рендер — значит, запомненные с + * прошлого рендера позиции остаются от прежнего положения прокрутки. Стоит + * пользователю прокрутить страницу к маршруту и нажать стрелку, как в сдвиг + * попадает вся дельта прокрутки (сотни пикселей), и строки улетают за пределы + * таблицы. Именно так выглядел баг «первое нажатие ломает, дальше нормально»: + * повторные клики уже сравнивались с позициями, замеренными при текущей + * прокрутке, поэтому шли правильно. + * + * Относительная база от этого избавляет: и строка, и замеряются в + * одном кадре, поэтому прокрутка сокращается. Заодно это чинит и случай, когда + * блок маршрута целиком уезжает из-за изменений НАД ним (например, свернулся + * блок вложений): tbody уезжает вместе со строками, и мнимого сдвига нет. + */ +function measureTops(nodes: Map): Map { + const tops = new Map(); + + let firstRow: HTMLTableRowElement | undefined; + nodes.forEach((el) => { + if (!firstRow) firstRow = el; + }); + if (!firstRow) return tops; + + const container = (firstRow as HTMLTableRowElement).parentElement; + const base = container ? container.getBoundingClientRect().top : 0; + + nodes.forEach((el, key) => tops.set(key, el.getBoundingClientRect().top - base)); + return tops; +} + /** * Возвращает функцию, которая по ключу строки отдаёт ref-колбэк для её . * @param activeKeys ключи строк того шага, который двигает пользователь — @@ -126,31 +159,28 @@ export function useFlipRows( if (!orderChanged) { // Порядок тот же — просто обновляем базовую геометрию для следующего // ДЕЙСТВИТЕЛЬНОГО сравнения, без единой анимации на этом проходе. - const current = new Map(); - nodes.current.forEach((el, key) => current.set(key, el.getBoundingClientRect().top)); - prevTops.current = current; + prevTops.current = measureTops(nodes.current); return; } // Сначала запоминаем, где строка ВИДНА сейчас: при быстрых повторных кликах // она в этот момент ещё летит, и новая анимация обязана стартовать от её // текущего положения на экране, иначе будет рывок. - const visualTops = new Map(); - nodes.current.forEach((el, key) => visualTops.set(key, el.getBoundingClientRect().top)); + const visualTops = measureTops(nodes.current); running.current.forEach((a) => a.cancel()); running.current = []; nodes.current.forEach(drop); const previous = prevTops.current; - const current = new Map(); + // После отмены анимаций transform снят — это истинные новые положения. + const current = measureTops(nodes.current); const moved: Array<{ el: HTMLTableRowElement; key: string; shift: number }> = []; const entered: HTMLTableRowElement[] = []; nodes.current.forEach((el, key) => { - // После отмены анимаций transform снят — это истинное новое положение. - const layoutTop = el.getBoundingClientRect().top; - current.set(key, layoutTop); + const layoutTop = current.get(key); + if (layoutTop === undefined) return; const before = previous.get(key); if (before === undefined) {