Этот пост посвящен прогресс-бару, который отображается в верхней части изображения обложки 🤓.
Это последующий пост
Если вы не читали первый пост, ознакомьтесь с ним: Добавление глобального индикатора прогресса в приложение Remix
Вступление
Теперь, когда мы знаем, как создать глобальный индикатор прогресса в наших приложениях Remix, мы хотим немного пофантазировать.
Создание индикатора прогресса с реальным процентом загрузки/выгрузки может быть довольно сложным. Но с помощью всего нескольких изменений в нашем компоненте GlobalLoading
, используя возможные состояния transition.state
, мы можем добиться гораздо лучшего UX.
Начните с правильной стилизации
Измените возвращаемый JSX компонента в предыдущем посте.
<div
role="progressbar"
aria-hidden={!active}
aria-valuetext={active ? "Loading" : undefined}
className="fixed inset-x-0 top-0 z-50 h-1 animate-pulse"
>
<div
className={cx(
"h-full bg-gradient-to-r from-blue-500 to-cyan-500 transition-all duration-500 ease-in-out",
active ? "w-full" : "w-0 opacity-0 transition-none"
)}
/>
</div>
Мы немного изменились, мы больше не будем использовать SVG спиннер, теперь нам просто нужен div
с некоторым стилем в нашем контейнере прогресс-бара. Основные изменения следующие:
Теперь классы перехода transition-all duration-500 ease-in-out
размещены на дочернем div
, потому что это то, что мы будем анимировать.
Теперь все должно выглядеть следующим образом:
Проблема в том, что время анимации (500 мс) не соответствует времени запроса/ответа, и анимация линейна. Мы хотим добавить несколько остановок по пути, чтобы это больше походило на реальный прогресс-бар.
Представляем transition.state
.
Кроме "idle"
, есть еще несколько состояний, к которым мы можем стремиться, чтобы прогресс-бар действительно ощущался как «прогрессирующий». Просто немного изменив код, мы уже добавили шаг на этом пути:
<div role="progressbar" {...}>
<div
className={cx(
"h-full bg-gradient-to-r from-blue-500 to-cyan-500 transition-all duration-500 ease-in-out",
transition.state === "idle" && "w-0 opacity-0 transition-none",
transition.state === "submitting" && "w-1/2",
transition.state === "loading" && "w-full"
)}
/>
</div>
Когда сеть простаивает, полоса прогресса имеет ширину 0 и является прозрачной. Мы также добавляем transition-none
на этом этапе, чтобы полоса не анимировалась обратно от w-full
к w-0
.
Когда произойдет отправка формы, полоса анимируется от w-0
до w-1/2
за 500 мс, а когда загрузчики выполнят проверку, она перейдет от w-1/2
к w-full
.
Это уже выглядит довольно круто:
Теперь полоса анимируется от w-0
до w-full
, когда отправляется только загрузчик, и остановится на середине пути, если мы отправляем данные на сервер! Опять же, Remix здесь для нас!
Я бы хотел, чтобы был 4-й шаг.
Я бы хотел, чтобы прогресс-бар останавливался в двух местах, чтобы он был больше похож на Github. Проблема в том, что у нас нет дополнительного состояния при переходе.
На самом деле я хочу сказать компьютеру следующее:
- во время запроса анимировать от 0 до 25% примерно
- во время ответа анимировать до 75% примерно
- во время простоя снова быстро дойти до 100% и исчезнуть. 🤔
Да, это можно сделать, нам просто нужно сделать этот последний шаг!
Я назову эту переменную animationComplete
и покажу, как ее использовать, позже я покажу, как ее определить:
<div
className={cx(
"h-full bg-gradient-to-r from-blue-500 to-cyan-500 transition-all duration-500 ease-in-out",
transition.state === "idle" &&
animationComplete &&
"w-0 opacity-0 transition-none",
transition.state === "submitting" && "w-4/12",
transition.state === "loading" && "w-10/12",
transition.state === "idle" && !animationComplete && "w-full"
)}
/>
Хорошо, как мы собираемся это сделать?
Существует API для элементов DOM под названием Element.getAnimations
, который можно использовать для возврата массива обещаний, которые будут выполнены, когда анимация будет завершена!
Promise.allSettled(
someDOMElement
.getAnimations()
.map((animation) => animation.finished)
).then(() => console.log('All animations are done!')
С помощью небольшого ref
от моего друга React для получения элемента DOM и некоторого состояния React мы можем выполнить работу! Вот обновленный код для компонента:
import * as React from "react";
import { useTransition } from "@remix-run/react";
import { cx } from "~/utils";
function GlobalLoading() {
const transition = useTransition();
const active = transition.state !== "idle";
const ref = React.useRef<HTMLDivElement>(null);
const [animationComplete, setAnimationComplete] = React.useState(true);
React.useEffect(() => {
if (!ref.current) return;
if (active) setAnimationComplete(false);
Promise.allSettled(
ref.current.getAnimations().map(({ finished }) => finished)
).then(() => !active && setAnimationComplete(true));
}, [active]);
return (
<div role="progressbar" {...}>
<div ref={ref} {...} />
</div>
);
}
export { GlobalLoading };
Понимание важных частей
У нас уже были первые 2 строки, определяющие transition
и active
. Теперь мы добавили:
useRef
для хранения DOM-элемента внутреннегоdiv
.- Определение состояния
animationComplete
. - Эффект
useEffect
, который будет запускаться всякий раз, когда состояниеactive
перехода меняется сidle
и обратно. В этом эффекте мы:- устанавливаем состояние animationCompleted в
false
для запуска - ждем завершения всех анимаций элемента
ref
, чтобы установитьanimationCompleted
обратно вtrue
. Это произойдет, только еслиtransition.state
снова станетidle
.
- устанавливаем состояние animationCompleted в
Вот и все! Теперь у нас есть наш прогресс-бар в 4 шага с небольшим количеством кода:
Окончательный код
import * as React from "react";
import { useTransition } from "@remix-run/react";
import { cx } from "~/utils";
function GlobalLoading() {
const transition = useTransition();
const active = transition.state !== "idle";
const ref = React.useRef<HTMLDivElement>(null);
const [animationComplete, setAnimationComplete] = React.useState(true);
React.useEffect(() => {
if (!ref.current) return;
if (active) setAnimationComplete(false);
Promise.allSettled(
ref.current.getAnimations().map(({ finished }) => finished)
).then(() => !active && setAnimationComplete(true));
}, [active]);
return (
<div
role="progressbar"
aria-hidden={!active}
aria-valuetext={active ? "Loading" : undefined}
className="fixed inset-x-0 top-0 left-0 z-50 h-1 animate-pulse"
>
<div
ref={ref}
className={cx(
"h-full bg-gradient-to-r from-blue-500 to-cyan-500 transition-all duration-500 ease-in-out",
transition.state === "idle" &&
animationComplete &&
"w-0 opacity-0 transition-none",
transition.state === "submitting" && "w-4/12",
transition.state === "loading" && "w-10/12",
transition.state === "idle" && !animationComplete && "w-full"
)}
/>
</div>
);
}
export { GlobalLoading };
Я надеюсь, что эти два поста были полезны для вас! Я буду рад узнать, если вы добавите этот код в свой проект или даже усовершенствуете его, или придумаете лучшее решение. Дайте мне знать 😉
PS: Чтобы увидеть полный код для обоих постов, посмотрите этот pull request.