构建多选组件

关于如何构建响应式、自适应且无障碍的多选组件以实现排序和过滤用户体验的基础概览。

在这篇博文中,我想分享一下关于如何构建多选组件的想法。试用演示版

演示

如果您更喜欢视频,可以观看此帖子的 YouTube 版本:

概览

用户经常会看到很多商品,有时甚至非常多,在这种情况下,最好提供一种方法来缩减商品列表,以防止出现选择过载的情况。这篇博文探讨了如何通过过滤界面来减少选择。为此,它会显示用户可以选择或取消选择的商品属性,从而减少结果数量,进而减少选择过载。

互动次数

我们的目标是让所有用户都能快速浏览过滤条件选项,无论他们使用何种输入类型。它将通过一对自适应且响应迅速的组件来实现。面向桌面设备、键盘和屏幕阅读器用户的传统复选框边栏,以及面向触控用户的 <select multiple>

对比屏幕截图:显示了桌面版浅色主题和深色主题(带有复选框的边栏),以及移动版 iOS 和 Android(带有多选元素)。

决定在触控设备上使用内置多选功能,而在桌面设备上不使用,这既节省了工作量,也增加了工作量,但我认为,与在一个组件中构建整个自适应体验相比,这种做法可以提供适当的体验,同时减少代码债务。

触摸

触控组件可节省空间,并有助于提高移动设备上的用户互动准确性。它通过将整个复选框边栏折叠为 <select> 内置叠加层触控体验来节省空间。它通过显示系统提供的大型触控叠加层体验来提高输入准确性。

Android 版、iPhone 版和 iPad 版 Chrome 中多选元素的屏幕截图预览。iPad 和 iPhone 的多选开关处于开启状态,并且各自获得针对屏幕尺寸优化的独特体验。

键盘和游戏手柄

以下演示了如何通过键盘使用 <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 完成这项工作,因为这超出了计数器的功能范围。

屏幕截图:MacOS 屏幕阅读器宣布有效过滤器的数量。

嵌套兴奋

使用 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> 的默认样式和布局是独一无二的:

fieldset 和 legend 的默认样式的屏幕截图。

通常,为了设置子元素的间距,我会使用 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;
}
屏幕截图:显示了在多行换行场景中,对勾标记如何与第一行文字对齐。
Codepen 中体验更多乐趣

动画网格

布局动画由 Isotope 完成。一款高性能且强大的插件,可用于实现互动式排序和过滤。

JavaScript

除了有助于精心编排动画交互式网格之外,JavaScript 还用于修饰一些粗糙的边缘。

对用户输入进行归一化处理

此设计有一个表单,其中包含两种不同的输入方式,并且它们不会以相同的方式进行序列化。不过,借助一些 JavaScript,我们可以对数据进行归一化

DevTools 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 个结果”的公告。

一张屏幕截图,显示了 macOS 屏幕阅读器播报结果。

现在,无论用户如何与设备互动,我们都能为所有用户提供出色的辅助技术体验。

总结

现在您已经知道我是如何做到的,那么您会怎么做呢?🙂

让我们丰富方法,了解在网络上构建内容的所有方式。 制作演示视频,通过 Twitter 向我发送链接,我会将其添加到下方的社区混音部分!

社区混音作品

此处尚无可显示的内容!