【译】怎样建立定制表单组件

在许多状况下,[可用的HTML表单组件]()是不够的。若你想在诸如<select>元素的组件上[应用高级样式]()、或者想定制组件的行为,你就只能选择建立本身的表单组件。java

咱们将经过本文学习如何构建一个表单组件。为达到目的,咱们选择重构<select>元素做为例子。jquery

注意:咱们会专一于构建组件,但不会关注如何保证代码的通用和可重用。构建组件时会涉及到一些特殊的JavaScript代码和未知上下文中的DOM操做,而这些内容已经超出了本文的讨论范围。git

设计,结构和语义

在构建一个定制组件前,应先从明确你想要达到的效果开始,这会节省你宝贵的时间。具体来说,清晰地定义组件的全部状态是很重要的。要作到这点,最好从一个已经存在的、状态和行为已经为人所熟知的组件开始,这样你就只需尽量地模仿该组件便可。github

在咱们的例子中,咱们会重构<select>元素。下面是咱们指望达到的结果:web

上面的截屏展现了咱们组件的三个主要状态:普通状态(左)、激活状态(中)和打开状态(右)。chrome

至于组件的行为,咱们但愿能够像其余原生组件同样,经过鼠标和键盘来操控它。先从定义组件如何到达各个状态开始:segmentfault

组件变为普通状态:数组

  • 页面加载

  • 组件激活且用户点击了组件外任意地方

  • 组件激活且用户用键盘把焦点移动到别的组件

注意:在页面上移动焦点一般是经过敲tab键来实现的,但不是全部地方都遵循这个惯例。好比Safari上默认是用Option+Tab组合键来实如今页面上移动焦点。

组件变为激活状态:

  • 用户点击了组件

  • 用户按tab键且组件得到了焦点

  • 组件处于打开状态且用户点击了组件

组件变为打开状态:

  • 组件处于其余非打开状态且用户点击了它

在知道如何改变状态后,定义组件的值如何被改变也是很重要的:

组件的值改变:

  • 在组件处于打开状态时,用户点击了一个选项

  • 在组件处于激活状态时,用户按了上下方向键

最后咱们来定义下组件选项的行为:

  • 当组件处于打开状态时,被选中的选项会高亮

  • 当鼠标移到一个选项上,该选项会高亮且原先高亮状态的选项会恢复到普通状态

考虑例子的演示目的,咱们的分析就到此为止;然而若是你认真读过上文,会发现咱们漏了一些效果。好比,当组件处于打开状态时,若是用户按了tab键会发生什么呢?答案是--什么都不会发生。正确的效果虽然显而易见(译注:参考select原生组件,也是什么都不会发生),但事实是咱们没有在上述说明中定义它,这个效果很容易就会被忽视。在团队协做中,若是设计组件的人和实现它的人不一样,这是特别容易出现的问题。

另外一个有趣的问题是:组件处于打开状态时,用户按上下方向键会发生什么?要回答它,须要一点技巧。若考虑激活状态和打开状态是彻底不相干的,那答案就仍是“什么都不会发生”,由于咱们并未给打开状态定义任何键盘交互。另外一方面,若是考虑激活状态和打开状态有部分重叠,那答案就是:值可能会改变但选项也所以不会被高亮(译注:大概由于组件已经处于激活状态了吧),这也是由于当组件处于打开状态时,咱们并未给选项未定义任何键盘交互(只是定义了组件打开时应该发生什么,却没定义打开后要干吗)。

在咱们的例子中,缺失的特性仍是比较明显的,因此咱们还能处理得了它;但当面对来自外部的新组件时,因为没人知道正确的行为是什么,这时就会形成真正的麻烦。所以,花些时间在设计阶段是颇有必要的,若是你此时定义了一个不佳的交互,或忘记了去定义,后续在用户使用了该交互时再去重定义是很困难的。若(处理交互时)你有疑问,应积极寻求他人的帮助;而若你心中有数,则应绝不犹豫地进行用户测试。上面讨论的过程,可称之为UX(译注:用户体验)设计。若是你想了解更多这方面的内容,能够参考下面这些资源:

注意:在多数系统中,还有有一种方法能够打开<select>元素以查看全部可用的选项(这和用鼠标点击<select>元素是同样的)。这个方法在Windows下是用Alt+下方向键来实现的,咱们的例子中并未实现它--但要这样作也很简单,由于整个操做的机制已经被用于实现click事件了。

定义HTML结构和语义

上面咱们肯定了组建的基本功能,如今能够来构建咱们的组件了。第一步咱们要定义其HMLT结构,并为其添加基本的语义。下面是咱们重构<select>元素所需的代码:

<!-- 这是组件的主要容器.
     tabindex 特性用于让用户能聚焦到该组件。 
     用JavaScript来设置它是一个更好的办法 -->
<div class="select" tabindex="0">
  
  <!-- 这个容器用于展现组件的当前值 -->
  <span class="value">Cherry</span>
  
  <!-- 这个容器会包含组件里的全部可用选项,由于选项是一个列表,全部采用ul元素更加合适 -->
  <ul class="optList">
    <!-- 每一个选项只会包含要展现的内容,稍后咱们会了解如何处理其真实值,用来和表单数据一块儿发出去 -->
    <li class="option">Cherry</li>
    <li class="option">Lemon</li>
    <li class="option">Banana</li>
    <li class="option">Strawberry</li>
    <li class="option">Apple</li>
  </ul>

</div>

要注意此处class名的使用;这些class标记了每一个相关的元素,而不须要依赖其实际使用的HTML元素。这么作能确保咱们不会把CSS和JavaScript与HTML结构做强关联,从而作到改变后续的组件代码实现时,不破坏使用该组件的代码。好比你想实现一个一样的<optgroup>元素时,可用直接用相同的代码来调用。

用CSS建立样式和交互

如今咱们已经有了组件的结构了,接下来要来设计组件了。建立这个自定义组件的目的,是为了用咱们想要的形式来给该组件添加样式。要作到这点,咱们要把CSS的编码工做拆为两部分:第一部分是让咱们组件和<select>元素看起来一致的必要CSS规则,第二部分是用来让组件变成咱们想要的样子的样式。

必要的样式

必要的样式是用来处理咱们组件的三个状态的。

.select {
  /* 给选项列表建立一个定位上下文 */
  position: relative;
 
  /* 让咱们的组件成为文本流的一部分,并使之可伸缩 */
  display : inline-block;
}

咱们须要一个额外类名active,来定义组件处于激活状态时的外观。由于咱们的组件是能够得到操做焦点的,因此还要将相同的样式用于:focus伪类,保证激活和得到焦点时的行为一致。

.select.active,
.select:focus {
  outline: none;
 
  /* box-shadow 属性不是必要的,但它能够做为默认值保证激活状态可见,去掉它也是能够的。 */
  box-shadow: 0 0 3px 1px #227755;
}

接下来处理选项列表:

/* 这里的 .select 选择器,用来确保后面选择器匹配的元素就是咱们组件中那个 */
.select .optList {
  /* 下面样式确保选项列表会展现在当前值下面、并在HTML文档流以外 */
  position : absolute;
  top      : 100%;
  left     : 0;
}

咱们须要一个额外的class来处理选项列表的隐藏状态。为了管理激活和展开两个不一样的状态,这么作是颇有必要的。

.select .optList.hidden {
  /* 下面是一个以无障碍方式来隐藏列表的简单方法,咱们会在文末讨论更多关于无障碍访问的内容。 */
  max-height: 0;
  visibility: hidden;
}

美化

在有了基本的功能以后,有趣的部分开始了。下面是一个可选的例子,效果和本文开头的那个截图一致。可是你也能够自由探索、看看你能实现怎样的效果。

.select {
  /* 全部的大小值都会采用em值来保证无障碍访问
  (保证组件在用户使用浏览器纯文字模式下的缩放时,还保留自适应的能力)。
  在计算时,假设1em == 16px,这也是大多数浏览器的默认值。
  若是你对px到em的转换感到困惑,能够访问:http://riddle.pl/emcalc/ */
  font-size   : 0.625em; /* this (10px) is the new font size context for em value in this context */
  font-family : Verdana, Arial, sans-serif;

  -moz-box-sizing : border-box;
  box-sizing : border-box;

  /* 须要额外的空间来添加向下箭头 */
  padding : .1em 2.5em .2em .5em; /* 1px 25px 2px 5px */
  width   : 10em; /* 100px */

  border        : .2em solid #000; /* 2px */
  border-radius : .4em; /* 4px */
  box-shadow    : 0 .1em .2em rgba(0,0,0,.45); /* 0 1px 2px */
  
  /* 第一句声明用于不支持线性渐变的浏览器。
  第二句声明是由于基于Webkit的浏览器对线性渐变属性还要加个前缀。
  若你还想支持老旧浏览器,可参考http://www.colorzilla.com/gradient-editor/ */
  background : #F0F0F0;
  background : -webkit-linear-gradient(90deg, #E3E3E3, #fcfcfc 50%, #f0f0f0);
  background : linear-gradient(0deg, #E3E3E3, #fcfcfc 50%, #f0f0f0);
}

.select .value {
  /* 由于value元素可能会比组件还宽,因此咱们得保障这不会改变组件的宽度 */
  display  : inline-block;
  width    : 100%;
  overflow : hidden;

  vertical-align: top;

  /* 若是内容溢出了,最好能有省略号来替代。 */
  white-space  : nowrap;
  text-overflow: ellipsis;
}

咱们不须要额外的元素来设计向下箭头,而是使用:after伪元素。但其实这也能在select类上用一个简单的背景图片来实现。

.select:after {
  content : "▼"; /* 使用 unicode 字符 U+25BC;参见 http://www.utf8-chartable.de */
  position: absolute;
  z-index : 1; /* 用来保证箭头会叠在选项列表上面 */
  top     : 0;
  right   : 0;

  -moz-box-sizing : border-box;
  box-sizing : border-box;

  height  : 100%;
  width   : 2em;  /* 20px */
  padding-top : .1em; /* 1px */

  border-left  : .2em solid #000; /* 2px */
  border-radius: 0 .1em .1em 0;  /* 0 1px 1px 0 */

  background-color : #000;
  color : #FFF;
  text-align : center;
}

接下来,给选项列表添加样式:

.select .optList {
  z-index : 2; /* 代表选项列表会始终叠在向下箭头之上 */

  /* 重置ul元素的默认样式 */
  list-style: none;
  margin : 0;
  padding: 0;

  -moz-box-sizing : border-box;
  box-sizing : border-box;

  /* 确保即便值太少让选项列表小于组件主体,也能让选项列表会和组件主体同样大 */
  min-width : 100%;

  /* 若是列表太长了,其内容会在垂直方向上溢出(默认会自动添加一个垂直方向的滚动条),
    但不会在水平方向上也这样(由于咱们没有设置宽度,列表会有个自适应宽度,若是不能自适应,
    内容就会被截断) */
  max-height: 10em; /* 100px */
  overflow-y: auto;
  overflow-x: hidden;

  border: .2em solid #000; /* 2px */
  border-top-width : .1em; /* 1px */
  border-radius: 0 0 .4em .4em; /* 0 0 4px 4px */

  box-shadow: 0 .2em .4em rgba(0,0,0,.4); /* 0 2px 4px */
  background: #f0f0f0;
}

对于选项,咱们须要添加一个highlight类来标明用户会选取(或已经选取)的值。

.select .option {
  padding: .2em .3em; /* 2px 3px */
}

.select .highlight {
  background: #000;
  color: #FFFFFF;
}

下面就是咱们三个状态的实现效果了:
效果

用JavaScript让组件“活”起来

如今咱们组件的结构和设计都已经作好,能够来写JavaScript代码让组件真正能运行起来了。

警告:下面的代码是教学代码,在实际编码时不能直接像下面同样使用。其中许多部分,并无将来使用的保障、并且也不能在老旧浏览器上使用。此外,这些代码也有在生产环境中应该被优化掉的冗余部分。

注意:建立可复用的组件是颇有技巧性的。W3C Web Component 草案是这个特定问题的一个解决方案。X-tag project是这一规范的实验性实现;咱们鼓励你好好了解下它。

为何不起做用?

在开始以前,咱们须要知道JavaScript的一个严重问题:在浏览器里,它是一个不可靠的技术。当你在建立自定义组件的时候,你不得不依赖JavaScript,由于它是把全部东西维系在一块儿的绳索。可是,在许多状况下JavaScript并不能在浏览器中运行:

  • 用户禁用了JavaScript:这已是个最不常见的状况了,如今不多有人会禁用JavaScript。

  • 脚本没有加载:这是最广泛的状况,特别是在网络不太可靠的移动端。

  • 脚本有bug:你要常常考虑这一可能性。

  • 脚本和第三方脚本冲突了:使用了追踪脚本或用户自用的书签时会发生这种状况。

  • 脚本和浏览器拓展(如火狐的NoScript拓展或Chrome的NoScripts拓展)发生冲突、或受到干扰。

  • 用户使用了老旧浏览器,而且你须要的一种特性不被支持:这一般发生在你用了很新的API时。

因为有这些风险,咱们须要认真考虑下JavaScript不起做用时会发生什么。深刻处理这个问题已经超出了本文的论述范围,由于这和你但愿如何让脚本通用和可复用密切相关,咱们不会在例子中考虑这点。

在本文的例子中,若JavaScript代码不能运行,咱们会回退到展现标准的<select>元素。要作到这点,得先来作两件事。

首先,咱们要在使用自定义组件以前,添加一个普通的<select>元素。而为了能让自定义组件的数据和剩下的表单数据一块儿发送,这一步也是颇有必要的。后边咱们还会详细介绍。

<body class="no-widget">
  <form>
    <select name="myFruit">
      <option>Cherry</option>
      <option>Lemon</option>
      <option>Banana</option>
      <option>Strawberry</option>
      <option>Apple</option>
    </select>

    <div class="select">
      <span class="value">Cherry</span>
      <ul class="optList hidden">
        <li class="option">Cherry</li>
        <li class="option">Lemon</li>
        <li class="option">Banana</li>
        <li class="option">Strawberry</li>
        <li class="option">Apple</li>
      </ul>
    </div>
  </form>

</body>

第二,咱们还得添加两个新的类名,实现隐藏不须要的元素(即在脚本能运行时的<select>元素、或脚本不能运行时的自定义组件)。要注意的是在默认状况下,此处的HTML代码会隐藏自定义组件。

.widget select,
.no-widget .select {
  /* 这个CSS选择器意思是:
     - 要么body的类名被设为"widget",此处就要隐藏`<select>`元素
     - 要么body的类名没有改变,还是"no-widget",那么类名为"select"的元素就要被隐藏了 */
  position : absolute;
  left     : -5000em;
  height   : 0;
  overflow : hidden;
}

至此,咱们只须要一个JavaScript开关来决定脚本是否能运行了。这个开关很简单:若页面加载了脚本并运行,就会移除no-widget类并添加widget类,实现对<select>元素和自定义组件可见与否的切换。

window.addEventListener("load", function () {
  document.body.classList.remove("no-widget");
  document.body.classList.add("widget");
});

效果

注意:若你真的想让你的组件变得通用和可复用,除了做类名的切换,更好的方法是(在脚本能执行时)只添加widget类名隐藏<select>元素,并在页面中的每一个<select>元素后面指定自定义的组件、动态添加到DOM树中。

让工做轻松些

在将要建立的代码中,咱们会使用标准的DOM API来完成工做。然而,尽管浏览器对DOM API的支持已经愈来愈好,但在老旧浏览器上仍存在一些问题(特别在很老的IE上)。

若你想避免老旧浏览器上的麻烦,有两种方法能够作到:使用诸如jQuery, $dom, prototype, Dojo, YUI之类的稳定框架;或者补充那些缺失的但你要用的特性(经过条件加载能够很容易作到这点,好比可使用yepnope库)。

咱们计划使用的特性以下(从风险最大到最安全排列):

  1. classList

  2. addEventListener

  3. forEach(不属于DOM可是现代JavaScript的特性)

  4. querySelectorquerySelectorAll

除了上述特性的可用性,在开发以前仍存在一个问题。querySelector()方法返回的是一个NodeList而不是数组。Array对象支持forEach方法、但NodeList不支持。由于NodeList看起来像数组、也由于forEach方法用起来很方便,因此咱们能够很简单地就给NodeList添加forEach支持、让咱们的工做轻松些,就像下面这样:

NodeList.prototype.forEach = function (callback) {
  Array.prototype.forEach.call(this, callback);
}

咱们说这很简单可不是瞎说的哦。

创建事件回调

前期工做已经作好了,咱们如今能够来定义用户和咱们的组件交互时要用到的全部函数了。

/* 这个函数会在取消激活自定义组件时被使用
    须要一个参数:
    select: 类名为`select`且要被取消激活的DOM节点
 */
function deactivateSelect(select) {

  /* 若组件未被激活,则什么都不作 */
  if (!select.classList.contains('active')) return;

  /* 获取自定义组件的选项列表 */
  var optList = select.querySelector('.optList');

  /* 关闭选项列表 */
  optList.classList.add('hidden');

  /* 取消自定义组件的激活状态 */
  select.classList.remove('active');
}

/* 该函数用于让用户(取消)激活组件
    须要两个参数:
    select:类名为`select`且要被激活的DOM节点
    selectList:类名为`select`的全部DOM节点的列表
 */
function activeSelect(select, selectList) {

  /* 若组件已经激活,则什么都不作 */
  if (select.classList.contains('active')) return;

  /* 全部自定义组件的激活状态都得取消,
    由于deactivateSelect函数知足了做为forEach回调函数的要求,
    因此咱们会直接使用它而不是用一个中间的匿名函数
 */
  selectList.forEach(deactivateSelect);

  /* 开启该组件的激活状态 */
  select.classList.add('active');
}
 
/* 该函数用于让用户打开和关闭选项列表
    须要一个参数:
    select:有一个列表要切换状态的DOM节点
 */
function toggleOptList(select) {

  /* 选项列表能够从组件那得到 */
  var optList = select.querySelector('.optList');

  /* 改变列表的类名来展现和隐藏它 */
  optList.classList.toggle('hidden');
}

/* 该函数用于高亮一个选项
    须要两个参数:
    select:类名为`select`且包含要被高亮选项的DOM节点
    option:类名为`option`且要被高亮的DOM节点
 */
function highlightOption(select, option) {

  /* 得到自定义select元素的全部可用选项 */
  var optionList = select.querySelectorAll('.option');

  /* 移除全部选项的高亮 */
  optionList.forEach(function (other) {
    other.classList.remove('highlight');
  });

  /* 高亮正确的选项 */
  option.classList.add('highlight');
};

上面就是处理自定义组件的多个状态所需的全部函数。

接下来,咱们把这些函数绑到合适的事件上:

/* 在文档加载出来后处理下事件绑定 */
window.addEventListener('load', function () {
  var selectList = document.querySelectorAll('.select');

  /* 每一个自定义组件都要被初始化 */
  selectList.forEach(function (select) {

    /* 全部的`select`元素也要被初始化 */
    var optionList = select.querySelectorAll('.option');

    /* 用户把鼠标放到一个选项上时,高亮该选项 */
    optionList.forEach(function (option) {
      option.addEventListener('mouseover', function () {
        /* 注意:在咱们的函数调用内,`select`和`option`变量都是局部的 */
        highlightOption(select, option);
      });
    });

    /* 用户点击了自定义的select元素 */
    select.addEventListener('click', function (event) {
      /* 注意:在咱们的函数调用内,`select`变量是局部的 */
      /* 改变选项列表的可见状态 */
      toggleOptList(select);
    });

    /* 组件得到焦点时
    /* 用户点击组件或用tab键访问组件时,组件会得到焦点 */
    select.addEventListener('focus', function (event) {
       /* 注意:在咱们的函数调用内,`select`和`selectList`变量都是局部的 */

      /* 激活该组件 */
      activeSelect(select, selectList);
    });

    /* 组件失去焦点时 */
    select.addEventListener('blur', function (event) {
       /* 注意:在咱们的函数调用内,`select`变量是局部的 */

      /* 取消激活该组件 */
      deactivateSelect(select);
    });
  });
});

至此,组件已经能根据咱们的设计来改变其状态了,但它的值目前还不会更新,接下来咱们就会处理这点。

效果

处理组件的值

如今组件已经能用了,但咱们还得加点代码,根据用户的输入更新它的值、并让其能随着表单数据一块儿发送它的值。

要作到这点,最简单的方式就是在私底下用一个原生组件。这样一来,自定义组件就会跟踪浏览器提供的内置控件的值,并和平时同样在表单提交时发送它的值。在浏览器已经为咱们作好这一切时,没有必要来从新发明轮子了。

如前所示,出于可访问性的缘由,咱们已经用了一个原生的select组件来做为回退;同步这个组件的值和自定义组件的值是很容易的:

// 该函数用于更新展现的值,并和原生组件做同步
// 须要两个参数:
// select:类名为`select`且值要更新的DOM节点
// index:选定的值的索引
function updateValue(select, index) {
  // 咱们得为给定的自定义组件获取原生组件
  // 本例中,原生组件是自定义组件的兄弟节点
  var nativeWidget = select.previousElementSibling;

  // 得到自定义组件的值容器
  var value = select.querySelector('.value');

  // 得到完整的选项列表
  var optionList = select.querySelectorAll('.option');

  // 设置选中索引为咱们选择的选项的索引
  nativeWidget.selectedIndex = index;

  // 更新对应的值容器
  value.innerHTML = optionList[index].innerHTML;

  // 高亮自定义组件中关联的选项
  highlightOption(select, optionList[index]);
};

// 该函数返回原生组件当前选中的索引
// 须要一个参数:
// select:类名为`select`且和原生组件关联的DOM节点
function getIndex(select) {
  // 咱们得为给定的自定义组件获取原生组件
  // 本例中,原生组件是自定义组件的兄弟节点
  var nativeWidget = select.previousElementSibling;

  return nativeWidget.selectedIndex;
};

咱们能够用上面这两个函数来绑定原生组件和自定义组件:

// 在文档加载出来后处理下事件绑定
window.addEventListener('load', function () {
  var selectList = document.querySelectorAll('.select');

  // 每一个自定义组件都要被初始化
  selectList.forEach(function (select) {
    var optionList = select.querySelectorAll('.option'),
        selectedIndex = getIndex(select);

    // 让自定义组件能聚焦
    select.tabIndex = 0;

    // 让原生组件不可聚焦
    select.previousElementSibling.tabIndex = -1;

    // 确保默认选择的值被正确展现
    updateValue(select, selectedIndex);

    // 用户点击选项时,更新对应的值
    optionList.forEach(function (option, index) {
      option.addEventListener('click', function (event) {
        updateValue(select, index);
      });
    });

    // 用户在聚焦的组件上按键盘时,更新对应的值
    select.addEventListener('keyup', function (event) {
      var length = optionList.length,
          index  = getIndex(select);

      // 当用户按下箭头时,跳到后一选项
      if (event.keyCode === 40 && index < length - 1) { index++; }

      // 当用户按上箭头时,跳到前一选项
      if (event.keyCode === 38 && index > 0) { index--; }

      updateValue(select, index);
    });
  });
});

上面的代码里,要注意tabIndex属性的使用。该属性用来确保原生组件不会得到焦点,并确保自定义组件能在用户用键盘或鼠标访问时得到焦点。

经过上面的工做,咱们已经完成任务了!下面就是结果:

效果

等等,咱们真的完成了吗?

让组件变得无障碍

咱们已经构建了一个能够运行的组件,虽然距离获得一个具备完整特性的选择框还很远,但它运行得还不错。然而,咱们以前所作的只是在处理DOM而已,这个组件并非真正语义化的,并且虽然它看起来像个选择框,但在浏览器的角度它却并非这样,所以无障碍技术也不会认为它是个选择框。简而言之,它就是个无障碍性不好的漂亮选择框!

幸运的是,咱们有个解决方案叫ARIA。ARIA表示“无障碍的富Internet应用”,它是个W3C规范,用来让web应用和自定义组件变得无障碍。基本上这个规范就是一系列拓展了HTML的特性,用这些特性,咱们能够更好地描述角色、状态和属性,让咱们刚才设计的元素变得像其尽力模仿的原生元素同样。使用这些特性很简单,下面咱们来试试。

role特性

ARIA使用的关键特性是role。该特性会接收一个定义了元素用途的值,每一个值都表明了元素的特色和行为。在本例中,咱们会使用一个listbox做为role值,这个值是个“复合的role”,指定的元素能够包含多个特定role的子元素(本例中,至少有一个元素role值为option)。

值得注意的是,ARIA定义的role默认会自动用于标准的HTML标签中。好比说,<table>元素对应grid<ul>元素对应list。由于咱们的组件使用了<ul>元素,因此得确保组件的listbox role能覆盖掉<ul>元素的list值。为此,可使用presentation这个role值,该值用来指明一个没有特殊含义的元素,并且该元素只用来展现信息而已。这里咱们会给<ul>应用presentation值。

要使用listbox这个role值,得像下面同样修改HTML:

<!-- 给最外层元素指定role="listbox" -->
<div class="select" role="listbox">
  <span class="value">Cherry</span>
  <!-- 给ul元素指定role="presentation" -->
  <ul class="optList" role="presentation">
    <!-- 给全部li元素指定role="presentation" -->
    <li role="option" class="option">Cherry</li>
    <li role="option" class="option">Lemon</li>
    <li role="option" class="option">Banana</li>
    <li role="option" class="option">Strawberry</li>
    <li role="option" class="option">Apple</li>
  </ul>
</div>

注意:若是你想兼容那些不支持CSS特性选择器的老旧浏览器,同时使用role特性和class特性这种作法是必须的。

aria-selected特性

仅使用role特性是不够的,ARIA自己也提供了不少许多状态和属性特性。对这些特性用得越多和越恰当,网页就越能被无障碍技术所理解。在咱们的例子中,只会用到一个特性:aria-selected

aria-selected特性用于标记当前选中的选项,这样无障碍技术就能提示用户当前选中项是什么。咱们会在JavaScript中动态地使用它,在用户选中一个选项时能标记该选中项。为此,得修改下updateValue()函数:

function updateValue(select, index) {
  var nativeWidget = select.previousElementSibling;
  var value = select.querySelector('.value');
  var optionList = select.querySelectorAll('.option');

  // 确保全部的选项未被选中
  optionList.forEach(function (other) {
    other.setAttribute('aria-selected', 'false');
  });

  // 确保选择的那个选项被选中
  optionList[index].setAttribute('aria-selected', 'true');

  nativeWidget.selectedIndex = index;
  value.innerHTML = optionList[index].innerHTML;
  highlightOption(select, optionList[index]);
};

上述修改的最终效果以下(访问该组件时使用无障碍技术,譬如NVDAVoiceOver,会有更好的体验):

效果

结论

至此咱们已经了解了建立定制表单组件的全部基本知识,但如你所见,这么作并不简单,若是使用第三方库的话会比本身从头写起更好、更简单(固然除非你是想构建这样一个库)。

下面是你在本身开发以前应该参考下的库:

若你想更进一步使用本例,为让其中的代码变得通用和可复用,还要对代码作一些改进。这个练习你能够本身尝试下,这里有两个提示:首先,全部函数的第一个参数都相同,这就意味着这些函数须要有同一个执行上下文,使用一个对象来共享执行上下文是很明智的。此外,代码还得保证兼容,即代码最好能在兼容不一样Web标准的多种浏览器下运行。

相关文章
相关标签/搜索