правка анимации
This commit is contained in:
@@ -51,7 +51,10 @@ export const ApprovalMatrix: React.FC<IApprovalMatrixProps> = ({
|
|||||||
// подсвечивается пол-таблицы и непонятно, что именно переехало. Запоминаем
|
// подсвечивается пол-таблицы и непонятно, что именно переехало. Запоминаем
|
||||||
// ключи блока ДО перестановки: после неё индексы уже другие.
|
// ключи блока ДО перестановки: после неё индексы уже другие.
|
||||||
const [activeKeys, setActiveKeys] = React.useState<string[]>([]);
|
const [activeKeys, setActiveKeys] = React.useState<string[]>([]);
|
||||||
const registerRow = useFlipRows(activeKeys, rowKeys);
|
// Обёртка таблицы: её высоту useFlipRows анимирует вместе с переездом строк,
|
||||||
|
// чтобы пикер и следующая карточка не подпрыгивали при удалении шага.
|
||||||
|
const scrollRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
const registerRow = useFlipRows(activeKeys, rowKeys, scrollRef);
|
||||||
|
|
||||||
// Строка → шаг, которому она принадлежит. Нужно, чтобы отрисовать ячейку
|
// Строка → шаг, которому она принадлежит. Нужно, чтобы отрисовать ячейку
|
||||||
// «Шаг» один раз на весь шаг (rowSpan) и понять, можно ли двигать его выше/ниже.
|
// «Шаг» один раз на весь шаг (rowSpan) и понять, можно ли двигать его выше/ниже.
|
||||||
@@ -110,7 +113,7 @@ export const ApprovalMatrix: React.FC<IApprovalMatrixProps> = ({
|
|||||||
<div className={styles.matrixNote}>{emptyText}</div>
|
<div className={styles.matrixNote}>{emptyText}</div>
|
||||||
)}
|
)}
|
||||||
{!waiting && !loading && !error && rows.length > 0 && (
|
{!waiting && !loading && !error && rows.length > 0 && (
|
||||||
<div className={styles.matrixScroll}>
|
<div className={styles.matrixScroll} ref={scrollRef}>
|
||||||
<table className={styles.matrix}>
|
<table className={styles.matrix}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -109,10 +109,16 @@ function measureTops(nodes: Map<string, HTMLTableRowElement>): Map<string, numbe
|
|||||||
* @param rowKeys ключи строк в ТЕКУЩЕМ порядке — единственный сигнал, по
|
* @param rowKeys ключи строк в ТЕКУЩЕМ порядке — единственный сигнал, по
|
||||||
* которому решаем, действительно ли что-то переставили (см. ниже, почему
|
* которому решаем, действительно ли что-то переставили (см. ниже, почему
|
||||||
* геометрии для этого недостаточно).
|
* геометрии для этого недостаточно).
|
||||||
|
* @param containerRef прокручиваемая обёртка таблицы (.matrixScroll). Её высоту
|
||||||
|
* анимируем в такт переезду строк: без этого React схлопывает высоту таблицы
|
||||||
|
* мгновенно, а строки едут 380 мс — и всё, что стоит НИЖЕ маршрута (пикер
|
||||||
|
* «Добавить согласующего», следующая карточка), рывком подпрыгивает на высоту
|
||||||
|
* удалённой строки. Необязателен: без него анимируются только строки.
|
||||||
*/
|
*/
|
||||||
export function useFlipRows(
|
export function useFlipRows(
|
||||||
activeKeys: string[],
|
activeKeys: string[],
|
||||||
rowKeys: string[]
|
rowKeys: string[],
|
||||||
|
containerRef?: React.RefObject<HTMLElement>
|
||||||
// eslint-disable-next-line @rushstack/no-new-null -- null здесь диктует React: ref-колбэк получает именно null при отвязке узла
|
// eslint-disable-next-line @rushstack/no-new-null -- null здесь диктует React: ref-колбэк получает именно null при отвязке узла
|
||||||
): (key: string) => (el: HTMLTableRowElement | null) => void {
|
): (key: string) => (el: HTMLTableRowElement | null) => void {
|
||||||
const nodes = React.useRef(new Map<string, HTMLTableRowElement>());
|
const nodes = React.useRef(new Map<string, HTMLTableRowElement>());
|
||||||
@@ -122,6 +128,10 @@ export function useFlipRows(
|
|||||||
const prevTops = React.useRef(new Map<string, number>());
|
const prevTops = React.useRef(new Map<string, number>());
|
||||||
// Порядок строк на прошлый коммит — см. проверку orderChanged ниже.
|
// Порядок строк на прошлый коммит — см. проверку orderChanged ниже.
|
||||||
const prevRowKeys = React.useRef<string[]>([]);
|
const prevRowKeys = React.useRef<string[]>([]);
|
||||||
|
// Высота обёртки на прошлый коммит. Нужна именно запомненная: layout-эффект
|
||||||
|
// работает УЖЕ ПОСЛЕ коммита, и к этому моменту высота на экране новая —
|
||||||
|
// «откуда ехать» из текущей раскладки не узнать.
|
||||||
|
const prevHeight = React.useRef<number | undefined>(undefined);
|
||||||
// Свои анимации держим сами, чтобы отменять ИМЕННО их: el.getAnimations()
|
// Свои анимации держим сами, чтобы отменять ИМЕННО их: el.getAnimations()
|
||||||
// вернул бы заодно чужие (CSS-переходы и т.п.).
|
// вернул бы заодно чужие (CSS-переходы и т.п.).
|
||||||
const running = React.useRef<Animation[]>([]);
|
const running = React.useRef<Animation[]>([]);
|
||||||
@@ -157,10 +167,13 @@ export function useFlipRows(
|
|||||||
rowKeys.length !== prevRowKeys.current.length || rowKeys.some((k, i) => k !== prevRowKeys.current[i]);
|
rowKeys.length !== prevRowKeys.current.length || rowKeys.some((k, i) => k !== prevRowKeys.current[i]);
|
||||||
prevRowKeys.current = rowKeys;
|
prevRowKeys.current = rowKeys;
|
||||||
|
|
||||||
|
const container = containerRef ? containerRef.current : null;
|
||||||
|
|
||||||
if (!orderChanged) {
|
if (!orderChanged) {
|
||||||
// Порядок тот же — просто обновляем базовую геометрию для следующего
|
// Порядок тот же — просто обновляем базовую геометрию для следующего
|
||||||
// ДЕЙСТВИТЕЛЬНОГО сравнения, без единой анимации на этом проходе.
|
// ДЕЙСТВИТЕЛЬНОГО сравнения, без единой анимации на этом проходе.
|
||||||
prevTops.current = measureTops(nodes.current);
|
prevTops.current = measureTops(nodes.current);
|
||||||
|
prevHeight.current = container ? container.getBoundingClientRect().height : undefined;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,6 +181,10 @@ export function useFlipRows(
|
|||||||
// она в этот момент ещё летит, и новая анимация обязана стартовать от её
|
// она в этот момент ещё летит, и новая анимация обязана стартовать от её
|
||||||
// текущего положения на экране, иначе будет рывок.
|
// текущего положения на экране, иначе будет рывок.
|
||||||
const visualTops = measureTops(nodes.current);
|
const visualTops = measureTops(nodes.current);
|
||||||
|
// То же и для высоты обёртки: если анимация высоты ещё идёт, здесь мы
|
||||||
|
// получим её промежуточное значение (после cancel ниже высота схлопнется к
|
||||||
|
// натуральной, и «откуда ехать» было бы уже не восстановить).
|
||||||
|
const visualHeight = container ? container.getBoundingClientRect().height : undefined;
|
||||||
|
|
||||||
running.current.forEach((a) => a.cancel());
|
running.current.forEach((a) => a.cancel());
|
||||||
running.current = [];
|
running.current = [];
|
||||||
@@ -196,7 +213,38 @@ export function useFlipRows(
|
|||||||
|
|
||||||
prevTops.current = current;
|
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 }) => {
|
moved.forEach(({ el, key, shift }) => {
|
||||||
const isActive = activeKeys.indexOf(key) !== -1;
|
const isActive = activeKeys.indexOf(key) !== -1;
|
||||||
|
|||||||
Reference in New Issue
Block a user