Создание Github-подобного прогресс-бара для вашего приложения Remix

Этот пост посвящен прогресс-бару, который отображается в верхней части изображения обложки 🤓.

Это последующий пост

Если вы не читали первый пост, ознакомьтесь с ним: Добавление глобального индикатора прогресса в приложение 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.

Вот и все! Теперь у нас есть наш прогресс-бар в 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.

Оцените статью
devanswers.ru
Добавить комментарий