Разработка индикатора прогресса — одна из самых простых задач для фронтенд-разработчика. Все, что вам нужно, это базовые знания 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 разработчиков создавать приложения с компонентами.
Превратите любой пользовательский интерфейс, функцию или страницу в компонент многократного использования — и поделитесь им со своими приложениями. Легче сотрудничать и строить быстрее.
Разделите приложения на компоненты, чтобы упростить разработку приложений, и наслаждайтесь наилучшими возможностями для рабочих процессов, которые вы хотите: