Фундаментальный обзор того, как создать отзывчивый, адаптивный и доступный компонент с множественным выбором для сортировки и фильтрации пользовательского опыта.
В этом посте я хочу поделиться мыслями о том, как создать компонент с множественным выбором. Попробуйте демо .
Если вы предпочитаете видео, вот версия этого поста на YouTube:
Обзор
Пользователям часто предлагаются элементы, иногда множество элементов, и в таких случаях может быть хорошей идеей предоставить способ сократить список, чтобы предотвратить перегрузку выбором . В этом сообщении блога рассматривается фильтрация пользовательского интерфейса как способ сокращения выбора. Это достигается путем предоставления атрибутов элемента, которые пользователи могут выбрать или отменить выбор, сокращая результаты и, следовательно, уменьшая перегрузку выбором.
Взаимодействия
Цель состоит в том, чтобы обеспечить быстрый обход параметров фильтра для всех пользователей и их различных типов ввода. Это будет реализовано с помощью адаптируемой и отзывчивой пары компонентов. Традиционная боковая панель с флажками для рабочего стола, клавиатуры и программ чтения с экрана, а также <select multiple>
для сенсорных пользователей.
Это решение использовать встроенный множественный выбор для сенсорного управления, а не для рабочего стола, экономит работу и создает работу, но я считаю, что обеспечивает соответствующий опыт с меньшим количеством кода, чем создание всего адаптивного интерфейса в одном компоненте.
Трогать
Сенсорный компонент экономит место и повышает точность взаимодействия с пользователем на мобильных устройствах. Он экономит место, сворачивая всю боковую панель флажков во встроенный сенсорный наложенный элемент <select>
. Это повышает точность ввода, демонстрируя большое количество сенсорных наложений, предоставляемых системой.
Клавиатура и геймпад
Ниже приведена демонстрация того, как использовать <select multiple>
с клавиатуры.
Этот встроенный множественный выбор не может быть стилизован и предлагается только в компактном макете, не подходящем для представления большого количества опций. Видите, как вы не можете увидеть широту возможностей в этой крошечной коробочке? Хотя вы можете изменить его размер, он все равно не так удобен, как боковая панель флажков.
Разметка
Оба компонента будут содержаться в одном элементе <form>
. Результаты этой формы, будь то флажки или множественный выбор, будут отслеживаться и использоваться для фильтрации сетки, но также могут быть отправлены на сервер.
<form>
</form>
Компонент флажков
Группы флажков должны быть заключены в элемент <fieldset>
и иметь <legend>
. Когда HTML структурирован таким образом, программы чтения с экрана и FormData автоматически понимают взаимосвязь элементов.
<form>
<fieldset>
<legend>New</legend>
… checkboxes …
</fieldset>
</form>
Создав группировку, добавьте <label>
и <input type="checkbox">
для каждого фильтра. Я решил обернуть свой элемент в <div>
, чтобы свойство CSS- gap
могло распределять их равномерно и сохранять выравнивание, когда метки становятся многострочными.
<form>
<fieldset>
<legend>New</legend>
<div>
<input type="checkbox" id="last 30 days" name="new" value="last 30 days">
<label for="last 30 days">Last 30 Days</label>
</div>
<div>
<input type="checkbox" id="last 6 months" name="new" value="last 6 months">
<label for="last 6 months">Last 6 Months</label>
</div>
</fieldset>
</form>
<select multiple>
компонент
Редко используемая функция элемента <select>
— multiple
. Когда атрибут используется с элементом <select>
, пользователю разрешено выбирать многие элементы из списка. Это похоже на изменение взаимодействия со списка радио на список флажков.
<form>
<select multiple="true" title="Filter results by category">
…
</select>
</form>
Чтобы пометить и создать группы внутри <select>
, используйте элемент <optgroup>
и присвойте ему атрибут label
и значение. Этот элемент и значение атрибута аналогичны элементам <fieldset>
и <legend>
.
<form>
<select multiple="true" title="Filter results by category">
<optgroup label="New">
…
</optgroup>
</select>
</form>
Теперь добавьте элементы <option>
для фильтра.
<form>
<select multiple="true" title="Filter results by category">
<optgroup label="New">
<option value="last 30 days">Last 30 Days</option>
<option value="last 6 months">Last 6 Months</option>
</optgroup>
</select>
</form>
Отслеживание входных данных с помощью счетчиков для информирования вспомогательных технологий
В этом пользовательском интерфейсе используется метод роли статуса для отслеживания и ведения учета фильтров для программ чтения с экрана и других вспомогательных технологий. Видео на YouTube демонстрирует эту функцию. Интеграция начинается с HTML и атрибута role="status"
.
<div role="status" class="sr-only" id="applied-filters"></div>
Этот элемент будет читать вслух изменения, внесенные в содержимое. Мы можем обновлять содержимое с помощью счетчиков CSS , когда пользователи взаимодействуют с флажками. Для этого нам сначала нужно создать счетчик с именем в родительском элементе входов и элементе состояния.
aside {
counter-reset: filters;
}
По умолчанию счетчик будет равен 0
, и это здорово, в этом проекте по умолчанию ничего не :checked
.
Далее, чтобы увеличить наш вновь созданный счетчик, мы нацелимся на дочерние элементы элемента <aside>
, которые :checked
. Когда пользователь меняет состояние входов, счетчик filters
будет подсчитываться.
aside :checked {
counter-increment: filters;
}
CSS теперь знает общую информацию о пользовательском интерфейсе флажка, а элемент роли статуса пуст и ожидает значений. Поскольку CSS хранит подсчеты в памяти, функция counter()
позволяет получить доступ к значению из содержимого псевдоэлемента :
aside #applied-filters::before {
content: counter(filters) " filters ";
}
HTML-код элемента роли статуса теперь будет сообщать программе чтения с экрана «2 фильтра». Это хорошее начало, но мы можем добиться большего, например поделиться результатами, обновленными фильтрами. Мы проделаем эту работу с помощью JavaScript, поскольку счетчики не могут этого сделать.
Гнездовой азарт
Алгоритм счетчиков отлично сочетался с CSSnesting-1 , поскольку мне удалось поместить всю логику в один блок. Ощущается портативным и централизованным для чтения и обновления.
aside {
counter-reset: filters;
& :checked {
counter-increment: filters;
}
& #applied-filters::before {
content: counter(filters) " filters ";
}
}
Макеты
В этом разделе описывается расположение между двумя компонентами. Большинство стилей макета предназначены для компонента флажка рабочего стола.
Форма
Чтобы оптимизировать удобочитаемость и удобство сканирования для пользователей, максимальная ширина формы составляет 30 символов, что фактически устанавливает оптическую ширину линии для каждой метки фильтра. В форме используется сетка и свойство gap
для разделения наборов полей.
form {
display: grid;
gap: 2ch;
max-inline-size: 30ch;
}
Элемент <select>
Список ярлыков и флажков занимают слишком много места на мобильном устройстве. Таким образом, макет проверяет основное указательное устройство пользователя, чтобы изменить взаимодействие с сенсорным экраном.
@media (pointer: coarse) {
select[multiple] {
display: block;
}
}
Значение coarse
указывает, что пользователь не сможет взаимодействовать с экраном с высокой степенью точности со своим основным устройством ввода. На мобильном устройстве значение указателя часто бывает coarse
, поскольку основное взаимодействие — прикосновение. На настольном устройстве значение указателя часто fine
, поскольку обычно к нему подключена мышь или другое высокоточное устройство ввода.
Наборы полей
Стиль и макет по умолчанию для <fieldset>
с <legend>
уникальны:
Обычно для разделения дочерних элементов я бы использовал свойство gap
, но уникальное расположение <legend>
затрудняет создание равномерного набора дочерних элементов. Вместо gap
используются соседний одноуровневый селектор и margin-block-start
.
fieldset {
padding: 2ch;
& > div + div {
margin-block-start: 2ch;
}
}
Это исключает возможность регулировки пространства <legend>
, ориентируясь только на дочерние элементы <div>
.
Метка фильтра и флажок
Поскольку он является прямым дочерним элементом <fieldset>
и находится в пределах максимальной ширины 30ch
формы, текст метки может переноситься, если он слишком длинный. Перенос текста — это хорошо, но несовпадение текста и флажка — нет. Flexbox идеально подходит для этого.
fieldset > div {
display: flex;
gap: 2ch;
align-items: baseline;
}
![Снимок экрана, показывающий, как галочка выравнивается по первой строке текста в сценарии многострочного переноса.](https://web.dev/static/articles/building/a-multi-select-component/image/screenshot-showing-the-c-b135285ef7d8a.png?authuser=1&hl=ru)
Анимированная сетка
Анимацию макета выполняет Isotope . Производительный и мощный плагин для интерактивной сортировки и фильтрации.
JavaScript
Помимо помощи в организации аккуратной анимированной интерактивной сетки, JavaScript используется для сглаживания некоторых неровностей.
Нормализация пользовательского ввода
Этот дизайн имеет одну форму с двумя разными способами ввода данных, и они не сериализуются одинаково. Однако с помощью JavaScript мы можем нормализовать данные.
Я решил привести структуру данных элемента <select>
в соответствие со структурой сгруппированных флажков. Для этого к элементу <select>
добавляется прослушиватель событий input
, после чего сопоставляются его selectedOptions
.
document.querySelector('select').addEventListener('input', event => {
// make selectedOptions iterable then reduce a new array object
let selectData = Array.from(event.target.selectedOptions).reduce((data, opt) => {
// parent optgroup label and option value are added to the reduce aggregator
data.push([opt.parentElement.label.toLowerCase(), opt.value])
return data
}, [])
})
Теперь можно безопасно отправить форму или, как в случае с этой демонстрацией, указать Isotope, по чему фильтровать.
Завершение элемента роли статуса
Элемент только подсчитывает и объявляет количество фильтров на основе взаимодействия с флажком, но я посчитал хорошей идеей дополнительно поделиться количеством результатов и обеспечить подсчет выбора элемента <select>
.
Выбор элемента <select>
отражается в counter()
В разделе нормализации данных уже был создан прослушиватель на входе. В конце этой функции известно количество выбранных фильтров и количество результатов для этих фильтров. Значения можно передать элементу роли состояния следующим образом.
let statusRoleElement = document.querySelector('#applied-filters')
statusRoleElement.style.counterSet = selectData.length
Результаты, отраженные в элементе role="status"
:checked
предоставляет встроенный способ передачи количества выбранных фильтров в элемент роли статуса, но не обеспечивает видимость отфильтрованного количества результатов. JavaScript может отслеживать взаимодействие с флажками и после фильтрации сетки добавлять textContent
, как это сделал элемент <select>
.
document
.querySelector('aside form')
.addEventListener('input', e => {
// isotope demo code
let filterResults = IsotopeGrid.getFilteredItemElements().length
document.querySelector('#applied-filters').textContent = `giving ${filterResults} results`
})
В целом эту работу завершает анонс "2 фильтра, дающие 25 результатов".
Теперь наши превосходные ассистивные технологии будут доступны всем пользователям, независимо от того, как они с ними взаимодействуют.
Заключение
Теперь, когда вы знаете, как я это сделал, как бы вы‽ 🙂
Давайте разнообразим наши подходы и изучим все способы разработки в Интернете. Создайте демо, пришлите мне ссылку в Твиттере , и я добавлю ее в раздел ремиксов сообщества ниже!
Ремиксы сообщества
Здесь пока нечего смотреть!