Кнопки с несколькими состояниями
Существуют традиционные способы выбора пользователем одного варианта из многих. Классическими являются <select>
или группа элементов <input type="radio">
.
Но неплохо иметь больше вариантов. Иногда, когда пользователю нужно выбрать одну опцию из множества, хорошо иметь один элемент, переключающийся между доступными вариантами одним быстрым кликом. Практическим примером такого одиночного UI является элемент управления тегами, переходящий через различные состояния при каждом клике. Любой тег в подобном интерфейсе может находиться в трёх различных состояниях:
- Не учитывается в результатах поиска (состояние по умолчанию)
- Результаты поиска должны включать тег
- Результаты поиска должны исключать тег
Вот пример такого UI:
План
Мы создадим такой элемент управления с помощью набора HTML радиокнопок в стеке.
Функциональность пользовательского интерфейса — переход через различные состояния при каждом клике — реализуется с помощью небольшого трюка с использованием только CSS. Мы будем изменять значение CSS-свойства pointer-events
в радиокнопках при выборе одной из них.
Свойство pointer-events, применяемое к HTML-элементам, определяет, происходит ли в элементе событие от указателя, такое как click
или hover
— через указатель мыши, сенсорное событие, использование стилуса и т. д. — или нет. По умолчанию события происходят в элементах, что эквивалентно установке pointer-events: auto;
.
Если установлено значение pointer-events: none;
, то элемент не будет получать никаких событий от указателя. Это удобно при работе со стеками или вложенными элементами, когда требуется, чтобы верхний элемент игнорировал события от указателя, чтобы элементы под ним становились целью.
То же самое будет использовано для создания элемента управления с несколькими состояниями в этой статье.
Базовая демонстрация
Ниже представлен базовый элемент управления, который будем кодировать для демонстрации техники. В конце я также добавлю Pen для демонстрации тегов фильма, показанного ранее.
<div class="control">
<label class="three">
<input type="radio" name="radio" />
Third state
</label>
<label class="two">
<input type="radio" name="radio" />
Second state
</label>
<label class="one">
<input type="radio" name="radio" checked />
First state
</label>
</div>
.control {
width: 100px;
line-height: 100px;
label {
width: inherit;
position: absolute;
text-align: center;
border: 2px solid;
border-radius: 10px;
cursor: pointer;
input {
appearance: none;
margin: 0;
}
}
.one {
pointer-events: none;
background: rgb(247 248 251);
border-color: rgb(199 203 211);
}
.two {
background: rgb(228 236 248);
border-color: rgb(40 68 212);
}
.three {
background: rgb(250 230 229);
border-color: rgb(231 83 61);
}
}
В HTML, показанном выше, есть три радиокнопки <input>
(для трёх состояний), вложенных в соответствующие элементы <label>
.
Элементы label
располагаются друг над другом внутри родительского элемента <div>
(.control
), имея одинаковые размеры и стиль. Внешний вид радиокнопок по умолчанию удалён. Естественно, элементы label
будут вызывать установку/снятие флажков для радиокнопок внутри них.
Каждый label
имеет свой цвет в CSS. По умолчанию самый верхний label
(.one
) при загрузке страницы проверяется на наличие HTML атрибута checked
. В CSS свойство pointer-events
установлено на none
.
Это означает, что когда мы кликаем по элементу управления, самый верхний label
больше не является целью. Вместо этого он кликает label
под ним и устанавливает флажок на его радиокнопке. Поскольку одновременно может быть отмечена только одна радиокнопка в группе с одинаковым атрибутом name
, при установке флажка на нижний label
его радиокнопка снимает флажок с самого верхнего label
. Таким образом, управление переходит из первого состояния во второе.
Это основа кодирования многосоставного элемента управления. Вот как он запрограммирован в CSS для всех label
и, соответственно, их радиокнопок:
label:has(:checked) {
~ label {
opacity: 0;
}
&:is(:not(:first-child)) {
pointer-events: none;
~ label { pointer-events: none; }
}
&:is(:first-child) {
~ label { pointer-events: auto; }
}
}
Когда радиокнопка label
установлена, следующие label
в исходном коде скрываются с opacity: 0
, чтобы только она одна была видна пользователю.
Если label
отмеченной радиокнопки не является первым в исходном коде (самым нижним на экране), он и все label
после него получают pointer-events: none
. Это означает, что label
, расположенный под ней на экране, становится целью всех последующих событий от указателя.
Если label
отмеченной радиокнопки является первой в исходном коде (самой нижней на экране), то все label
после неё получают значение pointer-events: auto
, что позволяет им получать будущие события от указателя. Это сбрасывает элемент управления.
Предостережение при использовании
Хотя этот метод применим к любому количеству состояний, я бы рекомендовал ограничиться тремя для типичных пользовательских элементов управления, таких как теги, если только это не забавная игра, в которой пользователь многократно нажимает на одно и то же поле и каждый раз видит что-то новое. Кроме того, следует подумать о том, будет ли поддерживаться клавиатурная навигация или нет. Если да, то практичнее будет принять такой вариант, при котором пользователи смогут видеть все доступные опции с помощью клавиш табуляции и навигации, а не показывать один единственный UI.
Продвинутая демонстрация
Ниже представлен прототип кластера тегов, состоящего из трёх состояний и предназначенного для фильтрации результатов поиска фильмов по жанрам. Например, если пользователь хочет отфильтровать комедийные фильмы (comedy), не являющиеся боевиками (action), он может просто нажать на комедию один раз (comedy), чтобы включить её, и на боевик (action) дважды, чтобы исключить его. Если вам интересно, как подсчитывается количество включённых и исключённых тегов в демонстрации ниже, ознакомьтесь со списком в разделе Дополнительные материалы.
Дополнительные материалы
Дополнение из комментариев
Поскольку элемент управления представлен в виде кнопки, его можно активировать с помощью Space или Enter. Однако это не работает, используя клавиатуру можно изменять значение только с помощью клавиш со стрелками для перебора радиоэлементов, но как пользователь узнает, что это нужно делать, если он не знает, что элемент основан на радиокнопках?
Чтобы заставить кнопку вести себя как кнопка, требуется дополнительный скрипт.
Вот пример того, как можно сделать эту функциональность доступной. Этот пример был разработан как многопозиционный чекбокс, использующий слайдер (input type="range"
) в качестве базового элемента управления, но он может быть стилизован под кнопку с тем же эффектом: