本课程概要介绍了如何构建响应迅速、自适应且无障碍的多选组件,以提供排序和过滤用户体验。
在本文中,我想分享有关构建多选组件的思考。试用演示版。
如果您更喜欢视频,请观看此帖子的 YouTube 版本:
概览
系统通常会向用户显示项,有时会显示很多项,在这种情况下,最好提供一种缩减列表的方法,以防止选择过载。本文将探讨如何通过过滤界面来减少选项。它通过显示用户可以选择或取消选择的项属性来实现这一点,从而减少结果数量,进而减少选择过载。
互动次数
目标是为所有用户及其不同的输入类型快速浏览过滤条件。这将通过一对自适应且响应迅速的组件提供。传统的边栏复选框,适用于桌面设备、键盘和屏幕阅读器,<select
multiple>
则适用于触摸屏用户。
决定为触控设备(而非桌面设备)使用内置多选功能,既节省了工作量,也增加了工作量,但我认为,与在一个组件中构建整个响应式体验相比,这种做法可以提供合适的体验,同时减少代码债务。
轻触
触摸组件可节省空间,并有助于提高移动设备上的用户互动准确性。它会将整个边栏复选框收起到 <select>
内置叠加层触控体验中,从而节省空间。它通过显示系统提供的大型触摸叠加层体验,有助于提高输入准确性。
键盘和游戏手柄
下面演示了如何通过键盘使用 <select multiple>
。
此内置多选项无法设置样式,并且仅采用紧凑的布局,不适合显示大量选项。您是否发现,在该小框中,您无法看到丰富的选项?虽然您可以更改其大小,但它仍然不如复选框边栏那样易用。
Markup
这两个组件将包含在同一 <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 来完成此工作。
嵌套兴奋
使用 CSS 嵌套-1 时,计数器算法非常棒,因为我可以将所有逻辑都放入一个代码块中。便于携带和集中管理,方便阅读和更新。
aside {
counter-reset: filters;
& :checked {
counter-increment: filters;
}
& #applied-filters::before {
content: counter(filters) " filters ";
}
}
布局
本部分介绍这两个组件之间的布局。大多数布局样式适用于桌面复选框组件。
表单
为了优化用户可读性和可扫描性,表单的宽度上限为 30 个字符,这实际上是为每个过滤条件标签设置了光学线宽。该表单使用网格布局和 gap
属性来设置各个 fieldset 之间的间距。
form {
display: grid;
gap: 2ch;
max-inline-size: 30ch;
}
<select>
元素
标签和复选框列表在移动设备上占用太多空间。 因此,布局会检查用户的主要指针设备,以更改触控体验。
@media (pointer: coarse) {
select[multiple] {
display: block;
}
}
值为 coarse
表示用户无法使用其主要输入设备与屏幕进行高精度互动。在移动设备上,指针值通常为 coarse
,因为主要互动是触摸。在桌面设备上,指针值通常为 fine
,因为通常会连接鼠标或其他高精度输入设备。
字段集
具有 <legend>
的 <fieldset>
的默认样式和布局是独特的:
通常,为了给子元素设置间距,我会使用 gap
属性,但 <legend>
的独特定位方式使得很难创建间距均匀的一组子元素。使用相邻同胞选择器和 margin-block-start
,而不要使用 gap
。
fieldset {
padding: 2ch;
& > div + div {
margin-block-start: 2ch;
}
}
这样一来,系统只会定位到 <div>
子项,而不会调整 <legend>
的间距。
过滤条件标签和复选框
作为 <fieldset>
的直接子项且在表单 30ch
的最大宽度范围内,如果标签文本过长,则可能会换行。文本换行很好,但文本和复选框之间不对齐则不太好。Flexbox 非常适合此用途。
fieldset > div {
display: flex;
gap: 2ch;
align-items: baseline;
}
动画网格
布局动画由 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>
元素选项。
counter()
中反映的 <select>
元素选择
在“数据归一化”部分中,我们已在输入时创建了监听器。在此函数结束时,所选过滤条件的数量以及这些过滤条件的结果数量已知。可以像这样将值传递给状态角色元素。
let statusRoleElement = document.querySelector('#applied-filters')
statusRoleElement.style.counterSet = selectData.length
反映在 role="status"
元素中的结果
:checked
提供了一种内置方法,可将所选过滤条件的数量传递给状态角色元素,但无法查看过滤后的结果数量。JavaScript 可以监控与复选框的互动,并在过滤网格后,像 <select>
元素一样添加 textContent
。
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 条结果”这一通知。
现在,无论用户以何种方式与辅助技术互动,都能获得出色的辅助技术体验。
总结
现在您已经知道我是如何解决的,您会怎么做? 🙂?
让我们多元化我们的方法,了解在 Web 上构建的所有方式。 制作一个演示版,在推特上向我发送链接,我会将其添加到下方的社区混剪部分!
社区混剪作品
此处尚无可显示的内容!