Разработка индикатора прогресса — одна из самых простых задач для фронтенд-разработчика. Все, что вам нужно, это базовые знания HTML и CSS. JavaScript используется только для расчета процента выполнения задачи.

Однако эта простота обманчива. Интернет кишит множеством решений сообщества, предлагающих создать индикатор прогресса с вложенными div-контейнерами и CSS, чтобы оживить ситуацию. Держитесь подальше от этих решений! Это почти преступление против человечества, поскольку они ухудшают семантику шаблона и нарушают доступность.

В этой статье я покажу, как в Taiga UI мы разрабатывали компоненты Angular: ProgressBar и ProgressCircle.

Разработка ProgressBar

Формирование задачи

Имеется встроенный HTML-тег ‹прогресс /›. МДН утверждает:

HTML-элемент ‹progress› отображает индикатор, показывающий ход выполнения задачи, обычно отображаемый в виде индикатора выполнения.

Важно то, что каждый браузер по-разному отображает этот HTML-тег. Нам нужен такой индикатор прогресса, который выглядит одинаково во всех браузерах.

Неправильное решение/неправильное решение

Первая мысль, которая приходит в голову, может быть поверхностной: «Давайте вообще не будем использовать встроенный тег HTML. Это не красиво и выглядит по-разному в каждом браузере». Эта идея культивирует еще одно неправильное решение в сообществе. Предложение вращается вокруг создания двух <div/>-контейнеров: первый контейнер — это трекер прогресса, а второй — индикатор прогресса. Следующим шагом является настройка контейнеров и изменение ширины индикатора выполнения с помощью JavaScript (от 0 до 100%).

Упрощенная версия этого неправильного решения следующая. Файл HTML содержит два упомянутых контейнера:

<div class="track">
  <div class="indicator"></div>
</div>

CSS-файл:

.track {
  position: relative;
  background-color: grey;
    
  width: 300px;
  height: 20px;
}

.indicator {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  width: 50%; /* change it via JS */
  background-color: yellow;
}

Это работает визуально. Но это только визуально. Однако что, если пользователь с ослабленным зрением (например, не различающий цвета или слепой) решит посетить веб-сайт и не увидит индикатор выполнения?

В таких случаях люди используют программы для чтения с экрана. Эти программы зачитывают пользователю вслух все важное, что происходит на экране. Тем не менее, в случае решения «div» программа чтения с экрана не сможет понять этот тип индикатора прогресса. Он «увидит» только два <div />-контейнера, залитых разными цветами, и не озвучит их пользователю.

Это предлагаемое решение не только ухудшает семантику HTML, но и нарушает доступность нашего веб-приложения. Конечно, мы можем помочь скринридеру обнаружить индикатор прогресса: мы можем установить role="progressbar", а также добавить aria-valuenow, aria-valuemax и aria-valuemin во внешний контейнер. Однако не нужно изобретать велосипед и усложнять базовые понятия!

Улучшение решения DIV

Существует популярный подход, который может исправить доступность предыдущего решения. Мы можем поместить визуально скрытый собственный HTML-тег <progress /> внутри нашего пользовательского компонента прогресса. Затем мы должны передать ему атрибуты value и max, а все остальное оставить как было в предыдущем решении.

Существует несколько подходов к визуальному скрытию HTML-контейнера, но оставлению его обнаруживаемым для программ чтения с экрана. Обычно это sr-only CSS-класс. Например, такие классы есть в таких популярных CSS-фреймворках, как Bootstrap и Tailwind CSS.

Внимание! Не используйте display: none, height: 0 и width: 0. Они скрывают контент не только визуально, но и для программ чтения с экрана. Подробнее об этом читайте в статье «CSS в действии: невидимый контент только для пользователей программ чтения с экрана».

После него индикатор прогресса не только визуально приятен, но и не нарушает принцип доступности. Например, встроенный в Mac OS скринридер произносит это как «n-процентный индикатор прогресса» (где n — 100 × value / max).

Несмотря на прогресс, мы все еще недовольны семантикой шаблона! Кроме того, мы должны создать много @Input()-свойств в нашем компоненте, чтобы передать все необходимые атрибуты внедренному нативному <progress />-тегу без каких-либо мутаций. Пока это только value и max, и кто знает, может завтра нам нужно будет добавить id или один из data-* атрибутов. В результате наш компонент несет бесполезные атрибуты!

Наше решение с атрибутивным компонентом Angular

Мы предлагаем решение, не требующее дополнительных тегов HTML. Все, что нам нужно сделать, это расширить родной элемент <progress />. Всякий раз, когда мы расширяем любой нативный элемент HTML, официальная документация Angular рекомендует создавать компонент, который использует селектор атрибутов с этим элементом. Эта практика активно используется в нашей библиотеке UI-Kit Taiga: например, компоненты кнопка, ссылка и ярлык.

Давайте создадим файл TypeScript с нашим компонентом атрибутов:

import {
  ChangeDetectionStrategy,
  Component,
  HostBinding,
  Input
} from '@angular/core';

@Component({
  selector: 'progress[tuiProgressBar]',
  template: '',
  styleUrls: ['./progress-bar.component.less'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TuiProgressBarComponent {
  @Input()
  @HostBinding('style.--tui-progress-color')
  color?: string;
}

Файл Less включает в себя несколько less-mixins.

Следующий удаляет все встроенные настройки из браузера:

.clearProgress() {
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    border: none;
}

И это помогает настроить трекер прогресса:

.progressTrack(@property, @value) {
    @{property}: @value; // Edge | Mozilla

    &::-webkit-progress-bar {
        @{property}: @value; // Chrome | Opera | Safari
    }
}

Последний миксин помогает настроить цвет индикатора прогресса:

.progressIndicatorColor(@color) {
    color: @color; // Not Chromium Edge

    &::-webkit-progress-value {
        background: @color; // Chromium Edge | Chrome | Opera | Safari
    }

    &::-moz-progress-bar {
        background: @color; // Mozilla
    }
}

Наконец, примените все созданные миксины к :host-элементу нашего компонента (напоминаю: это нативный <progress />, к которому был применен наш атрибут-компонент).

:host {
    .clearProgress();
    .progressIndicatorColor(var(--tui-progress-color, currentColor));
    .progressTrack(background-color, grey);

    color: yellow;
}

Вот и все! Мы разработали хороший компонент атрибута Angular, который обернут вокруг собственного элемента <progress />. Цвет этого индикатора можно задать через свойство CSS color или через свойство ввода компонента (например, если мы хотим создать сложный цвет градиента). В коде компонент объявляется следующим образом:

<progress tuiProgressBar value="60" max="100"></progress>

Вы можете увидеть компонент в действии на витрине нашего проекта Taiga UI. Окончательный исходный код доступен на GitHub.

Развитие ProgressCircle

К сожалению, создание компонента ProgressCircle только из нативного <progress />-элемента невозможно. Нам нужны дополнительные теги шаблона.

И здесь дело обстоит так же, любопытные интернет-пользователи могут наткнуться на различные несерьёзные решения, предлагающие создать круговой индикатор прогресса из div-контейнеров, округлённых с помощью border-radius: 50%. Также такие решения используют значительное количество JavaScript.

Но так как мы в "клубе бездивов", то эту задачу можно решить и без обращения к div-ам. Мы будем использовать svg-тег <circle />. Более того, мы будем использовать как можно меньше JavaScript. В этом нам помогают препроцессор Less и переменные CSS.

Создадим TypeScript файл нашего будущего компонента:

import {
    ChangeDetectionStrategy,
    Component,
    HostBinding,
    Input,
} from '@angular/core';

@Component({
    selector: 'tui-progress-circle',
    templateUrl: './progress-circle.template.html',
    styleUrls: ['./progress-circle.style.less'],
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TuiProgressCircleComponent {
    @Input()
    value = 0;

    @Input()
    max = 1;

    @Input()
    @HostBinding('style.--tui-progress-color')
    color: string | null = null;

    @Input()
    @HostBinding('attr.data-size')
    size: 'm' | 'l' = 'm';

    @HostBinding('style.--progress-percentage')
    get progressPercentage(): number {
        return this.value / this.max;
    }
}

HTML-файл содержит:

<progress
  class="hidden-progress"
  [value]="value"
  [max]="max"
></progress>

<svg class="svg" height="100%" width="100%" aria-hidden="true">
  <circle
    class="track"
    cx="50%"
    cy="50%"
  ></circle>

  <circle
    class="progress"
    cx="50%"
    cy="50%"
  ></circle>
</svg>

И последний шаг — создание LESS-файла. Мы будем использовать многие возможности Less: примеси, Карты и встроенные Математические функции.

Во-первых, давайте создадим константы карты, которые хранят значения для разных размеров круговых индикаторов (в нашем проекте 4 размера, но для простоты кода мы оставим только 2). Стоит отметить, что Safari не поддерживает rem-единиц внутри svg-элементов. Однако он поддерживает em единиц. Поэтому мы устанавливаем font-size: 1rem для основного элемента, чтобы использовать внутри него em-юнитов.

@width: {
    @m: 2em;
    @l: 7em;
};

@track-stroke: {
    @m: 0.5em;
    @l: 0.25em;
};

@progress-stroke: {
    @m: 0.5em;
    @l: 0.375em;
};

Процесс заполнения прогресса происходит за счет установки stroke-dasharray и перерасчета stroke-dashoffset. Прочтите статью Быстро построить кольцо прогресса, чтобы понять, как это работает. Наше решение — это просто улучшенная версия того, что было предложено его автором. Создайте миксин, который вычисляет состояние индикатора прогресса для разных размеров компонентов:

.circle-params(@size) {
  width: @width[ @@size];
  height: @width[ @@size];

  .track {
    r: (@width[ @@size] - @track-stroke[ @@size]) / 2;
    stroke-width: @track-stroke[ @@size];
  }

  .progress {
    @radius: (@width[ @@size] - @progress-stroke[ @@size]) / 2;
    @circumference: 2 * pi() * @radius;
    
    r: @radius;
    stroke-width: @progress-stroke[ @@size];
    stroke-dasharray: @circumference;
    stroke-dashoffset: calc(@circumference - var(--progress-percentage) * @circumference);
  }
}

Наконец, примените наш миксин к элементу host и добавьте некоторые косметические улучшения:

:host {
    display: block;
    position: relative;
    color: yellow;
    transform: rotate(-90deg);
    transform-origin: center;
    font-size: 1rem;

    &[data-size='m'] {
        .circle-params(m);
    }

    &[data-size='l'] {
        .circle-params(l);
    }
}

.track {
    fill: transparent;
    stroke: grey;
}

.progress {
    fill: transparent;
    stroke: var(--tui-progress-color, currentColor);
    transition: stroke-dashoffset 300 linear;
}

.hidden-progress {
    .sr-only(); // see this mixin in the chapter with ProgressBar
}

.svg {
    overflow: unset;
}

Вот и все! Вы можете увидеть компонент в действии на витрине нашего проекта Taiga UI. Окончательный исходный код доступен на GitHub.

💡 Совет. Теперь вы можете развернуть вновь созданные семантические компоненты на такой платформе, как Bit, чтобы их можно было повторно использовать во всех ваших проектах. С Bit у вас будет независимое управление версиями, тесты и документация для ваших компонентов, что упростит другим понимание и использование вашего кода. Это позволит вашей команде повторно использовать компоненты и совместно работать над ними для написания масштабируемого кода, ускорения разработки и поддержки согласованного пользовательского интерфейса. Узнайте больше здесь.

Подведение итогов

В фронтенд-разработке нет абсолютно правильных решений. Один и тот же функционал может быть реализован по-разному. Но есть поверхностные и ленивые решения, которые дают конечный продукт, жертвуя удобством и оптимизацией. Я показал, какие ошибки можно допустить, если вы хотите создать индикаторы прогресса: плохая семантика и пренебрежение доступностью.

В этой статье я показал свою точку зрения на эти компоненты. Я не утверждаю, что мои решения являются лучшими в этой области. И все же при их разработке я учел все упомянутые проблемы, обеспечил совместимость со всеми современными браузерами и получил простые решения с минимальным использованием JavaScript.

Создавайте приложения с повторно используемыми компонентами, как Lego

Инструмент с открытым исходным кодом Bit помогает более чем 250 000 разработчиков создавать приложения с компонентами.

Превратите любой пользовательский интерфейс, функцию или страницу в компонент многократного использования — и поделитесь им со своими приложениями. Легче сотрудничать и строить быстрее.

Подробнее

Разделите приложения на компоненты, чтобы упростить разработку приложений, и наслаждайтесь наилучшими возможностями для рабочих процессов, которые вы хотите:

Микро-интерфейсы

Система дизайна

Совместное использование кода и повторное использование

Монорепо

Узнать больше