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) {