构建多选组件

基本概述了如何构建响应式、自适应、无障碍、多选组件,以便对用户体验进行排序和过滤。

在这篇博文中,我将分享关于构建多选组件的思路。查看演示

演示

如果你更喜欢视频,可以参考本博文的 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 屏幕阅读器的屏幕截图,其中显示了激活的过滤器数量。

兴奋不已

使用 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,因为连接鼠标或其他高精度输入设备的情况很常见。

字段集

包含 <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;
}
屏幕截图:在多行换行场景中,对勾标记如何与文本的第一行对齐。
在此 Codepen 中玩更多

动画网格

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

JavaScript

除了帮助编排整洁的互动式动画网格之外,JavaScript 还用于润色一些粗糙的边缘。

将用户输入标准化

此设计采用一种形式,提供两种不同提供输入的方式,并且它们不会序列化相同的内容。不过,借助一些 JavaScript,我们可以对数据进行标准化

开发者工具 JavaScript 控制台的屏幕截图,其中显示了目标标准化数据结果。

我选择了将 <select> 元素数据结构与分组的复选框结构对齐。为此,需要将 input 事件监听器添加到 <select> 元素中,此时其 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 可以监控与复选框的互动,并在过滤网格后像 <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 条结果”这一公告。

MacOS 屏幕阅读器的屏幕截图(宣布结果)。

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

总结

现在你已经知道我是怎么做的,希望你怎么办 ‽ 🙂?

下面,我们就来介绍一下我们的方法多样化,并了解在 Web 上构建网站的所有方法。 只需创建一个演示,点击 tweet me 链接,我就会将其添加到下方的“社区混剪”部分中!

社区混剪作品

此处尚无可显示的内容!