×
  • Web前端首页
  • Html4
  • select元素样式化:在本机选择元素和自定义选择元素之间取得平衡

select元素样式化:在本机选择元素和自定义选择元素之间取得平衡

作者:Terry2020.06.17来源:Web前端之家浏览:7499评论:0
关键词:htmlselect


这是计划!我们将构建一个样式化的select元素。不只是外部,而且内部也是如此。全面的样式控制。另外,我们将使其变得可访问。我们不会尝试使用本机元素来复制浏览器默认情况下所做的所有操作<select><select>当使用任何辅助技术时,我们将实际使用元素。但是,当使用鼠标时,我们将显示样式化的版本并将其用作选择元素。

这就是我所说的“混合”选择的意思:它们<select>在一个设计模式中既是本地选择又是样式选择替换。

1.png

为了美观和设计的一致性,通常使用自定义选择(左)代替本地选择(右)。

选择,下拉菜单,导航,菜单……名称很重要

在进行本文的研究时,我想到了许多在谈论选择时会被弄乱的名字,其中最常见的是“下拉菜单”和“菜单”。我们可能会犯两种类型的命名错误:为不同的事物赋予相同的名称,或为同一事物赋予不同的名称。选择可能会遇到两个错误。

在继续之前,让我尝试使用“下拉列表”一词来增加清晰度。这是我定义下拉菜单的含义的方法:

下拉列表:一个交互式组件,由一个按钮组成,该按钮显示和隐藏项目列表,通常在鼠标悬停时单击或点击。默认情况下,直到交互开始,该列表才可见。该列表通常在其他内容之上显示一个内容块(即选项)。我们可能会谈论不同类型的成分:

菜单:用户可以在页面内容中执行的命令或操作的列表。

导航:用于浏览网站的链接列表。

选择:表单控件(<select>),显示用户在表单中选择的选项列表。

确定我们要讨论的下拉列表类型可能是一项艰巨的任务。以下是一些来自网络的示例,这些示例与我如何对这三种不同类型进行分类相匹配。这是基于我的研究,有时,当我找不到合适的答案时,会根据我的经验得出直觉。

1.png

Dropdown-land:五种方案,其中在互联网上使用不同的下拉菜单。阅读下表以获取详细说明。

图标签情境下拉式
1个该下拉列表期望一个选定的选项在表单上下文中提交(例如,选择年龄)选择
2下拉列表不需要活动选项(例如,动作列表:复制,粘贴和剪切)菜单
3所选选项会影响内容。(例如排序列表)菜单或选择(稍后有更多信息)
4下拉菜单包含指向其他页面的链接。(例如带有网站链接的“ meganav”)披露导航
5下拉菜单中包含的内容不是列表。(例如日期选择器)其他不应该被称为下拉菜单的东西

并非每个人都以相同的方式感知互联网并与之互动。命名用户界面和定义设计模式是一个基本过程,尽管这一过程具有很大的个人解释空间。所有这些变化是驱动下拉式土地人口的原因。 

有一个下拉类型显然是菜单。它的用法是有关可访问性的讨论中的热门话题。在这里,我不会谈论太多,但是让我强调一下,该<menu>元素已被弃用,不再推荐使用。这是有关包含菜单和菜单按钮的详细说明,包括为何不应该将ARIA菜单角色用于网站导航。

由于WCAG社区缺乏实际使用案例,我们甚至还没有涉及其他位于灰色区域的元素,这些元素使下拉列表的分类变得更加模糊。

乌夫...很多。让我们忘记下拉列表的混乱情况,只关注显然是<select>元素的下拉类型。

让我们来谈谈 <select>

设置表单控件的样式是一个有趣的旅程。正如MDN所说的那样,有好有坏。诸如此类的东西<form>只是样式中的块级元素而已。不好的事情是诸如复选框之类的东西,虽然可以完成,但是有点麻烦。<select>绝对是在丑陋的地形中。

关于它的文章很多,即使在2020年,创建自定义选择仍然是一个挑战,一些用户仍然更喜欢简单的本机选择。 

在开发人员中,<select>是迄今为止最令人沮丧的表单控件,这主要是因为它缺乏样式支持。它背后的UX斗争如此艰巨,以至于我们在寻找其他替代方案。嗯,我想的第一条规则<select>是类似于ARIA:如果你可以使用它避免。

我可以在此处用“不要使用<select>,句号”结束本文。但让我们面对现实:在许多情况下,选择仍然是我们最好的解决方案。这可能包括以下情况:我们正在使用包含很多选项的列表,空间紧凑的布局,或者只是缺乏时间或预算来从头设计和实现出色的自定义交互式组件的场景。

定制<select>要求

当我们决定创建自定义选择(即使只是一个简单的选择)时,通常需要满足以下要求:

  • 有一个包含当前所选选项的按钮。

  • 单击该框可切换选项列表(也称为列表框)的可见性。

  • 单击列表框中的选项将更新所选值。按钮文本更改,列表框关闭。

  • 在组件外部单击将关闭列表框。

  • 触发器包含一个朝下的小三角形图标,指示有选项。

像这样:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WEB前端之家</title>
    <style>
        .select {
        position: relative;
        }

        .selectLabel {
        display: block;
        font-weight: bold;
        margin-bottom: 0.4rem;
        }

        .selectWrapper {
        position: relative;
        }

        .selectCustom {
        position: relative;
        width: 22rem;
        height: 4rem;
        }

        .selectCustom-trigger {
        font-size: 1.6rem;
        background-color: #fff;
        border: 1px solid #6f6f6f;
        border-radius: 0.4rem;
        cursor: pointer;
        }

        .selectCustom-trigger {
        position: relative;
        width: 100%;
        height: 100%;
        background-color: #fff;
        padding: 0.7rem 0.8rem;
        }

        .selectCustom-trigger::after {
        content: "▾";
        position: absolute;
        top: 0;
        line-height: 3.8rem;
        right: 0.8rem;
        }

        .selectCustom-trigger:hover {
        border-color: #8c00ff;
        }

        .selectCustom-options {
        position: absolute;
        top: calc(3.8rem + 0.8rem);
        left: 0;
        width: 100%;
        border: 1px solid #6f6f6f;
        border-radius: 0.4rem;
        background-color: #fff;
        box-shadow: 0 0 4px #e9e1f8;
        z-index: 1;
        padding: 0.8rem 0;
        display: none;
        }

        .selectCustom.isActive .selectCustom-options {
        display: block;
        }

        .selectCustom-option {
        position: relative;
        padding: 0.8rem;
        }

        .selectCustom-option:hover {
        background-color: #f7ecff;
        }

        .selectCustom-option:not(:last-of-type)::after {
        content: "";
        position: absolute;
        bottom: 0;
        left: 0.8rem;
        width: calc(100% - 1.6rem);
        border-bottom: 1px solid #d3d3d3;
        }


        html {
        font-size: 62.5%;
        }
        body {
        background: #f8f3ef;
        font-family: Arial, Helvetica, sans-serif;
        box-sizing: border-box;
        color: #343434;
        line-height: 1.5;
        font-size: 1.6rem;
        }

        body * {
        box-sizing: inherit;
        }

        .p {
        margin-bottom: 1rem;
        }

        strong {
        font-weight: 600;
        }

        .title {
        font-size: 2rem;
        font-weight: 600;
        margin: 1.6rem;
        line-height: 1.2;
        text-align: center;
        }

        .card {
        position: relative;
        margin: 2rem auto;
        max-width: calc(100% - 2rem);
        width: 40rem;
        background: white;
        padding: 3rem;
        box-shadow: 0.2rem 0.2rem #e9e1f8;
        }
    </style>
</head>
<body>
    <h1 class="title">Custom Select (not accessible)</h1>

    <!-- DO NOT USE this into production. It's a poor custom select -->
    <div class="card">
    <p class="p">You can select an option using the mouse or touch, but it's impossible using a keyboard.<p>
    <div class="select">
        <span class="selectLabel">Main job role</span>
        <div class="selectWrapper">
        <div class="selectCustom js-selectCustom">
            <div class="selectCustom-trigger">Select role...</div>
            <div class="selectCustom-options">
            <div class="selectCustom-option" data-value="ds">UI/UX Designer</div>
            <div class="selectCustom-option" data-value="fe">Frontend Engineer</div>
            <div class="selectCustom-option" data-value="be">Backend Engineer</div>
            <div class="selectCustom-option" data-value="qa">QA Engineer</div>
            <div class="selectCustom-option" data-value="un">Unicorn</div>
            </div>
        </div>
        </div>
    </div>

    <script>
        const elSelectCustom = document.getElementsByClassName("js-selectCustom")[0];
        const elSelectCustomValue = elSelectCustom.children[0];
        const elSelectCustomOptions = elSelectCustom.children[1];
        const defaultLabel = elSelectCustomValue.getAttribute("data-value");

        // Listen for each custom option click
        Array.from(elSelectCustomOptions.children).forEach(function (elOption) {
        elOption.addEventListener("click", (e) => {
            // Update custom select text too
            elSelectCustomValue.textContent = e.target.textContent;
            // Close select
            elSelectCustom.classList.remove("isActive");
        });
        });

        // Toggle select on label click
        elSelectCustomValue.addEventListener("click", (e) => {
        elSelectCustom.classList.toggle("isActive");
        });

        // close the custom select when clicking outside.
        document.addEventListener("click", (e) => {
        const didClickedOutside = !elSelectCustom.contains(event.target);
        if (didClickedOutside) {
            elSelectCustom.classList.remove("isActive");
        }
        });
    </script>
</body>
</html>

你们中的有些人可能认为这行得通,很乐意。但是,等等……它对所有人有用吗?并非所有人都使用鼠标(或触摸屏)。另外,本机<select>元素具有我们免费获得的更多功能,这些功能未包含在这些要求中,例如:

  • 所有用户无论其视觉能力如何,都可以选中该选项。

  • 该组件可以在所有浏览器中以可预测的方式与键盘交互(例如,使用箭头键进行导航,Enter选择,Esc取消等)。

  • 辅助技术(例如屏幕阅读器)会向用户清楚地宣布该元素,包括其角色,名称和状态。

  • 列表框的位置已调整。(即不会被切断屏幕)。

  • 该元素尊重用户的操作系统首选项(例如,高对比度,配色方案,运动等)。

这是大多数定制选择以某种方式失败的地方。看一下一些主要的UI组件库。我不会提及任何内容,因为网络是短暂的,但请尝试一下。您可能会注意到,一个框架中的select组件的行为与另一个框架不同。 

这里是要注意的其他特征:

  • 使用键盘导航时,是否立即激活了焦点的列表框选项?

  • 您可以使用Enter和/或Space选择一个选项吗?

  • 请问Tab键跳跃到列表框中跳下一个选项,或到下一个表单控件?

  • 使用箭头键到达列表框中的最后一个选项时会发生什么?它只是停留在最后一项,返回到第一个选项,还是最糟糕的是,焦点是否移至下一个窗体控件? 

  • 是否可以使用Page Down键直接跳到列表框中的最后一项?

  • 如果当前查看的内容超过列表框的内容,是否可以滚动查看?

这是本机<select>元素中包含的功能的一小部分。

一旦决定创建自己的自定义选择,我们将迫使人们以某种可能不是他们期望的方式使用它。

但情况变得更糟。甚至本地人在浏览器和屏幕阅读器之间的<select> 行为也有所不同。一旦决定创建自己的自定义选择,我们将迫使人们以某种可能不是他们期望的方式使用它。这是一个危险的决定,而且是在魔鬼居住的那些细节中。

建立一个“混合”选择

当我们构建一个简单的自定义选择时,我们会在不知不觉中进行权衡。具体来说,我们为了美观而牺牲了功能。情况应该相反。

如果我们改为默认提供本机选择并在可能的情况下将其替换为更美观的选择怎么办?这就是“混合”选择想法生效的地方。之所以是“混合型”,是因为它包含两个选择,并在适当的时候显示了适当的一个:

  • 默认情况下本机选择,可见和可访问

  • 自定义选择,隐藏起来直到可以安全地与鼠标交互

让我们从标记开始。首先,我们将在自定义选择器之前添加一个<select>带有<option>项的本机,以使其起作用。(我稍后会解释原因。)

任何表单控件都必须具有描述性标签。我们可以使用<label>,但这将在单击标签时集中于本机选择。为避免这种情况,我们将使用,<span>然后使用将其连接到select aria-labelledby

最后,我们需要使用告诉Assistive Technologies忽略自定义选择aria-hidden="true"。这样,无论如何,只有本地选择才由他们宣布。

<span class="selectLabel" id="jobLabel">Main job role</span>
<div class="selectWrapper">
  <select class="selectNative js-selectNative" aria-labelledby="jobLabel">
    <!-- options -->
    <option></option>
  </select>
  <div class="selectCustom js-selectCustom" aria-hidden="true">
     <!-- The beautiful custom select -->
  </div>
</div>

这将我们带入样式,在这种样式中,我们不仅使外观看起来很漂亮,而且还处理了从一个选择到另一个的切换。我们只需要几个新的声明就可以使所有魔术发生。

首先,本机选择和自定义选择必须具有相同的宽度和高度。这样可以确保人们在切换时不会看到布局上的主要差异。

.selectNative,
.selectCustom {
  position: relative;
  width: 22rem;
  height: 4rem;
}

有两种选择,但是只有一种可以决定容纳它们的空间。另一个位置必须绝对定位才能从文档流中删除。让我们对自定义选择执行此操作,因为只有在可以的情况下才使用“替换”。默认情况下,我们将其隐藏,因此任何人都无法访问它。

.selectCustom {
  position: absolute;
  top: 0;
  left: 0;
  display: none;
}

这是“有趣的”部分。我们需要检测是否有人在使用悬停作为主要输入一部分的设备,例如带鼠标的计算机。虽然我们通常将媒体查询考虑为响应性断点或检查功能支持,但我们也可以使用来使用它来检测悬停支持@media query (hover :hover),所有主要浏览器均支持。因此,让我们使用它来仅在具有悬停功能的设备上显示自定义选择:


@media (hover: hover) {
  .selectCustom {
    display: block;
  }
}

很好,但是即使在悬停的设备中使用键盘导航的人又如何呢?我们要做的是在本地选择集中时隐藏自定义选择。我们可以到达相邻的兄弟组合(+)。当本机选择成为焦点时,按DOM顺序隐藏其旁边的自定义选择。(这就是为什么将本机选择放置在自定义选择之前的原因。)

@media (hover: hover) {
  .selectNative:focus + .selectCustom {
    display: none;
  }
}

而已!完成两个选择之间的切换技巧!当然,还有其他CSS方法可以做到这一点,但是效果很好。

最后,我们需要一些JavaScript。让我们添加一些事件监听器:

  • 一个用于触发自定义选择的点击事件可打开并显示选项

  • 一个同步两个选择值。当一个选择值更改时,另一个选择值也将更新

  • 一个基本的键盘导航控件,如导航与UpDown键,选择与选项EnterSpace按键,和关闭与选择Esc

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WEB前端之家</title>
    <style>
        .selectNative,
        .selectCustom {
        position: relative;
        width: 22rem;
        height: 4rem;
        }

        .selectCustom {
        position: absolute;
        top: 0;
        left: 0;
        display: none;
        }

        @media (hover: hover) {
            .selectCustom {
                display: block;
            }

            .selectNative:focus + .selectCustom {
                display: none;
            }
        }

        /* Add the focus states too, They matter, always! */
        .selectNative:focus,
        .selectCustom.isActive .selectCustom-trigger {
        outline: none;
        box-shadow: white 0 0 0 0.2rem, #ff821f 0 0 0 0.4rem;
        }


        .select {
        position: relative;
        }

        .selectLabel {
        display: block;
        font-weight: bold;
        margin-bottom: 0.4rem;
        }

        .selectWrapper {
        position: relative;
        }

        .selectNative,
        .selectCustom-trigger {
        font-size: 1.6rem;
        background-color: #fff;
        border: 1px solid #6f6f6f;
        border-radius: 0.4rem;
        }

        .selectNative {
        -webkit-appearance: none;
        -moz-appearance: none;
        background-image: url("data:image/svg+xml;utf8,<svg fill='black' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/><path d='M0 0h24v24H0z' fill='none'/></svg>");
        background-repeat: no-repeat;
        background-position-x: 100%;
        background-position-y: 0.8rem;
        padding: 0rem 0.8rem;
        }

        .selectCustom-trigger {
        position: relative;
        width: 100%;
        height: 100%;
        background-color: #fff;
        padding: 0.8rem 0.8rem;
        cursor: pointer;
        }

        .selectCustom-trigger::after {
        content: "▾";
        position: absolute;
        top: 0;
        line-height: 3.8rem;
        right: 0.8rem;
        }

        .selectCustom-trigger:hover {
        border-color: #8c00ff;
        }

        .selectCustom-options {
        position: absolute;
        top: calc(3.8rem + 0.8rem);
        left: 0;
        width: 100%;
        border: 1px solid #6f6f6f;
        border-radius: 0.4rem;
        background-color: #fff;
        box-shadow: 0 0 4px #e9e1f8;
        z-index: 1;
        padding: 0.8rem 0;
        display: none;
        }

        .selectCustom.isActive .selectCustom-options {
        display: block;
        }

        .selectCustom-option {
        position: relative;
        padding: 0.8rem;
        padding-left: 2.5rem;
        }

        .selectCustom-option.isHover {
        background-color: #865bd7;
        color: white;
        }

        .selectCustom-option:not(:last-of-type)::after {
        content: "";
        position: absolute;
        bottom: 0;
        left: 0;
        width: 100%;
        border-bottom: 1px solid #d3d3d3;
        }

        .selectCustom-option.isActive::before {
        content: "✓";
        position: absolute;
        left: 0.8rem;
        }


        html {
        font-size: 62.5%;
        }
        body {
        background: #f8f3ef;
        font-family: Arial, Helvetica, sans-serif;
        box-sizing: border-box;
        color: #343434;
        line-height: 1.5;
        font-size: 1.6rem;
        min-height: 120vh; /* using arrow keys in the select, does not scroll the page */
        }

        body * {
        box-sizing: inherit;
        }

        strong {
        font-weight: 600;
        }

        .title {
        font-size: 2rem;
        font-weight: 600;
        margin: 1.6rem;
        line-height: 1.2;
        text-align: center;
        }

        .card {
        position: relative;
        margin: 2rem auto;
        max-width: calc(100% - 2rem);
        width: 40rem;
        background: white;
        padding: 3rem;
        box-shadow: 0.2rem 0.2rem #e9e1f8;
        }

        .inst {
        margin-bottom: 1rem;
        }

        .note {
        font-size: 1.4rem;
        margin: 2rem 0 0;
        color: #6b6b6b;
        }

        .link {
        display: inline-block;
        color: inherit;
        text-decoration-color: #9b78de;
        padding: 0.1rem 0.2rem;
        transform: translateX(-0.1em);
        margin-right: -0.1em;
        }

        .link:hover {
            color: #8c00ff;
        }

        .link:focus {
            outline: none;
            background-color: #e9e1f8;
        }
    </style>
</head>
<body>
<h1 class="title">Custom Select: "Hybrid Select"</h1>

<div class="card">
  <p class="inst">Try to select an option with whatever tool you are using (e.g. mouse, touch, keyboard, etc...)<p>
      <div class="select">
        <span class="selectLabel" id="jobLabel"> Main job role</span>
        <div class="selectWrapper">
            <select class="selectNative js-selectNative" aria-labelledby="jobLabel">
                <option value="sel" disabled="" selected=""> Select role...</span>
                <option value="ds">UI/UX Designer</option>
                <option value="fe">Frontend Engineer</option>
                <option value="be">Backend Engineer</option>
                <option value="qa">QA Engineer</option>
                <option value="un">Unicorn</option>
            </select>

            <!-- Hide the custom select from AT (e.g. SR) using aria-hidden -->
            <div class="selectCustom js-selectCustom" aria-hidden="true">
                <div class="selectCustom-trigger">Select role...</div>
                <div class="selectCustom-options">
                <div class="selectCustom-option" data-value="ds">UI/UX Designer</div>
                <div class="selectCustom-option" data-value="fe">Frontend Engineer</div>
                <div class="selectCustom-option" data-value="be">Backend Engineer</div>
                <div class="selectCustom-option" data-value="qa">QA Engineer</div>
                <div class="selectCustom-option" data-value="un">Unicorn</div>
                </div>
            </div>
            </div>
        </div>
    </div>

    <script>
        /* Features needed to make the selectCustom work for mouse users.

- Toggle custom select visibility when clicking the "box"
- Update custom select value when clicking in a option
- Navigate through options when using keyboard up/down
- Pressing Enter or Space selects the current hovered option
- Close the select when clicking outside of it
- Sync both selects values when selecting a option. (native or custom)

*/

const elSelectNative = document.getElementsByClassName("js-selectNative")[0];
const elSelectCustom = document.getElementsByClassName("js-selectCustom")[0];
const elSelectCustomBox = elSelectCustom.children[0];
const elSelectCustomOpts = elSelectCustom.children[1];
const customOptsList = Array.from(elSelectCustomOpts.children);
const optionsCount = customOptsList.length;
const defaultLabel = elSelectCustomBox.getAttribute("data-value");

let optionChecked = "";
let optionHoveredIndex = -1;

// Toggle custom select visibility when clicking the box
elSelectCustomBox.addEventListener("click", (e) => {
  const isClosed = !elSelectCustom.classList.contains("isActive");

  if (isClosed) {
    openSelectCustom();
  } else {
    closeSelectCustom();
  }
});

function openSelectCustom() {
  elSelectCustom.classList.add("isActive");
  // Remove aria-hidden in case this was opened by a user
  // who uses AT (e.g. Screen Reader) and a mouse at the same time.
  elSelectCustom.setAttribute("aria-hidden", false);

  if (optionChecked) {
    const optionCheckedIndex = customOptsList.findIndex(
      (el) => el.getAttribute("data-value") === optionChecked
    );
    updateCustomSelectHovered(optionCheckedIndex);
  }

  // Add related event listeners
  document.addEventListener("click", watchClickOutside);
  document.addEventListener("keydown", supportKeyboardNavigation);
}

function closeSelectCustom() {
  elSelectCustom.classList.remove("isActive");

  elSelectCustom.setAttribute("aria-hidden", true);

  updateCustomSelectHovered(-1);

  // Remove related event listeners
  document.removeEventListener("click", watchClickOutside);
  document.removeEventListener("keydown", supportKeyboardNavigation);
}

function updateCustomSelectHovered(newIndex) {
  const prevOption = elSelectCustomOpts.children[optionHoveredIndex];
  const option = elSelectCustomOpts.children[newIndex];

  if (prevOption) {
    prevOption.classList.remove("isHover");
  }
  if (option) {
    option.classList.add("isHover");
  }

  optionHoveredIndex = newIndex;
}

function updateCustomSelectChecked(value, text) {
  const prevValue = optionChecked;

  const elPrevOption = elSelectCustomOpts.querySelector(
    `[data-value="${prevValue}"`
  );
  const elOption = elSelectCustomOpts.querySelector(`[data-value="${value}"`);

  if (elPrevOption) {
    elPrevOption.classList.remove("isActive");
  }

  if (elOption) {
    elOption.classList.add("isActive");
  }

  elSelectCustomBox.textContent = text;
  optionChecked = value;
}

function watchClickOutside(e) {
  const didClickedOutside = !elSelectCustom.contains(event.target);
  if (didClickedOutside) {
    closeSelectCustom();
  }
}

function supportKeyboardNavigation(e) {
  // press down -> go next
  if (event.keyCode === 40 && optionHoveredIndex < optionsCount - 1) {
    let index = optionHoveredIndex;
    e.preventDefault(); // prevent page scrolling
    updateCustomSelectHovered(optionHoveredIndex + 1);
  }

  // press up -> go previous
  if (event.keyCode === 38 && optionHoveredIndex > 0) {
    e.preventDefault(); // prevent page scrolling
    updateCustomSelectHovered(optionHoveredIndex - 1);
  }

  // press Enter or space -> select the option
  if (event.keyCode === 13 || event.keyCode === 32) {
    e.preventDefault();

    const option = elSelectCustomOpts.children[optionHoveredIndex];
    const value = option && option.getAttribute("data-value");

    if (value) {
      elSelectNative.value = value;
      updateCustomSelectChecked(value, option.textContent);
    }
    closeSelectCustom();
  }

  // press ESC -> close selectCustom
  if (event.keyCode === 27) {
    closeSelectCustom();
  }
}

// Update selectCustom value when selectNative is changed.
elSelectNative.addEventListener("change", (e) => {
  const value = e.target.value;
  const elRespectiveCustomOption = elSelectCustomOpts.querySelectorAll(
    `[data-value="${value}"]`
  )[0];

  updateCustomSelectChecked(value, elRespectiveCustomOption.textContent);
});

// Update selectCustom value when an option is clicked or hovered
customOptsList.forEach(function (elOption, index) {
  elOption.addEventListener("click", (e) => {
    const value = e.target.getAttribute("data-value");

    // Sync native select to have the same value
    elSelectNative.value = value;
    updateCustomSelectChecked(value, e.target.textContent);
    closeSelectCustom();
  });

  elOption.addEventListener("mouseenter", (e) => {
    updateCustomSelectHovered(index);
  });

  // TODO: Toggle these event listeners based on selectCustom visibility
});
    </script>
</body>
</html>

可用性测试

我进行了一个很小的可用性测试,在测试中,我让一些残疾人尝试混合选择组件。使用最新版本的Chrome(81),Firefox(76)和Safari(13)对以下设备和工具进行了测试:

  • 仅使用鼠标的桌面设备

  • 仅使用键盘的桌面设备

  • 使用键盘在MacOS上使用VoiceOver

  • Windows上使用键盘的NVDA

  • 使用Safari的iPhone和iPad上的VoiceOver

所有这些测试均按预期工作,但我相信使用更多的人员和工具可以进行更多的可用性测试。如果您可以使用其他设备或工具(例如JAWS,Dragon等),请告诉我测试的进行方式。

在测试过程中发现一个问题。具体来说,问题出在VoiceOver设置“鼠标指针:将Voice Over光标移动”。如果用户使用鼠标打开选择,则将打开自定义选择(而不是本机选择),并且用户将不会体验本机选择。

我最喜欢这种方法的地方在于,它如何在不损害核心功能的情况下充分利用两者的优点:

  • 移动设备和平板电脑上的用户会获得本机选择,通常它比自定义选择提供更好的用户体验,包括性能优势。

  • 键盘用户可以按照他们期望的方式与本机交互。

  • 辅助技术可以像正常情况一样与本地选择进行交互。

  • 鼠标用户可以与增强的自定义选择进行交互。

这种方法为每个人提供了基本的本机功能,而无需花费大量的代码来实现所有本机功能。

不要误会我的意思。这种技术不是万能的解决方案。它可能适用于简单选择,但不适用于涉及复杂交互的情况。在这些情况下,我们需要使用ARIA和JavaScript来弥补差距并创建一个真正可访问的自定义select。

关于选择的注释,看起来像菜单

让我们回顾一下第三种Dropdown-land方案。如果您还记得,它是一个下拉菜单,始终具有选中的选项(例如,对某些内容进行排序)。我在灰色区域将其分类为菜单或选择项。 

这是我的想法:几年前,这种下拉菜单主要是使用native来实现的<select>。如今,常见的是从头开始使用自定义样式(可访问或不可访问)来实现它。我们最终得到的是一个看起来像菜单的select元素。 

1.png

<select>  是一种菜单。两者都有相似的语义和行为,尤其是在涉及一列选项始终被选中的情况下。现在,让我提及WCAG 3.2.2输入标准(A级):

更改任何用户界面组件的设置都不会自动导致上下文的更改,除非在使用该组件之前已告知用户该行为。

让我们付诸实践。想象一个可排序的学生名单。在视觉上,很明显排序是即时的,但不一定对每个人都正确。因此,在使用时<select>,由于页面内容已更改,因此我们有可能无法通过WCAG准则,并且高估地重新排列页面的内容被视为上下文的更改。

为了确保条件成功,我们必须在与元素进行交互之前向用户发出有关操作的警告,或者<button>在选择之后立即添加一个以确认更改。

<label for="sortStudents">
  Sort students
  <!-- Warn the user about the change when a confirmation button is not present. -->
  <span class="visually-hidden">(Immediate effect upon selection)</span>
</label>
<select id="sortStudents"> ... </select>

也就是说,<select>对于更改页面内容的简单菜单,使用a 或构建自定义菜单都是不错的方法。请记住,您的决定将决定使组件完全可访问所需的工作量。在这种情况下,可以使用混合选择方法。

总结

整个想法起初只是一个无辜的CSS技巧,但在进行了所有这些研究之后,再次提醒我,在不影响可访问性的情况下创建独特的体验并非易事。

构建真正可访问的精选组件(或任何类型的下拉列表)比看起来要难。WCAG提供了出色的指导和最佳实践,但是由于没有具体的示例和多样的实际使用案例,因此该指导原则大多是理想的。更不用说ARIA支持不高,并且本机<select>元素在浏览器中的外观和行为不同。

“混合”选择只是在获得尽可能多的本机功能的同时创建外观漂亮的选择的另一种尝试。不要将这种技术实验视为淡化可访问性的借口,而应作为服务于两个世界的尝试。如果您有足够的资源,时间和所需的技能,请正确执行此操作,并确保在将组件交付给世界之前先与其他用户进行测试。

您的支持是我们创作的动力!
温馨提示:本文作者系Terry ,经Web前端之家编辑修改或补充,转载请注明出处和本文链接:
https://jiangweishan.com/article/html20200617.html

网友评论文明上网理性发言 已有0人参与

发表评论: