在许多情况下,可用的 HTML 表单小组件是不够的。如果要在某些小部件(例如HTML的<select>元素)上执行高级样式,或者如果要提供自定义行为,则别无选择,只能构建自己的小部件。

在本文中,我们通过一个例子:构建<select>元素,您将看到怎样构建这样一个元素控件。

注意: 我们将专注于构建小控件,而不是怎样让代码更通用或可复用,同时也会涉及一些上下文未知的JavaScript代码和DOM操作,这些超过了这篇文章的范围。

设计, 结构, 和语义

在构建一个自定义控件之前,首先你要确切的知道你要什么。 这将为您节省宝贵的时间。 特别地,清楚地定义控件的所有状态非常重要。 为了做到这一点,从状态和行为表现都众所周知的现有小控件开始是很好的选择,你也可以尽可能多的模仿这些控件。

在我们的示例中,我们将重建HTML<select>元素,这是我们希望实现的结果:

The three states of a select box

上面图片显示了我们控件的三个主要状态:正常状态(左侧); 活动状态(中间)和打开状态(右侧)。

在行为方面,我们希望我们的控件像任何本地控件一样对鼠标和键盘都可用。 让我们从定义控件怎样到达每个状态开始:

以下情况控件将会呈现正常状态:
  • 页面加载
  • 控件处于活动状态,但用户点击控件以外的任何位置
  • 控件是活动状态,但用户使用键盘将焦点移动到另一个小部件

注意: 在页面上移动焦点通常是通过按Tab键来完成的,但这并不是标准的。 例如,通过使用Option + Tab组合在Safari中完成页面上的链接切换。

以下情况控件将会呈现活动状态:
  • 用户点击
  • 用户按下tab让控件重新获得焦点。
  • 控件呈现打开状态然后用户点击控件。
以下情况控件将会呈现打开状态:
  • 控件在非打开状态时被用户点击。

我们应该知道如何改变状态,定义如何改变小工具的值还很重要:

以下情况控件的值将会被改变:
  • 控件在打开状态下用户点击一个选项
  • 控件在打开状态下用户按下键盘上方向键或者下方向键

最后,让我们定义控件的选项将要怎么变化:

  • 当控件在打开状态时,选项将被突出显示
  • 当鼠标悬停在某个选项上时,该选项将被突出显示,并且之前突出显示的选项将返回正常的状态

就我们的例子而言,我们将就此结束;但是,如果你是一个认真的读者,你会注意到我们省略了一些东西,例如,你认为用户在小部件处于打开状态时点击tab键会发生什么?答案是:什么也不会发生。好吧,正确的行为似乎很明显,但事实是,由于其没有在我们的规范中定义,我们很容易忽略这种行为,尤其是在团队环境中,设计小部件行为的人与实现的人通常是不同的。

另外一个有趣的例子是:当小部件处于打开状态时,用户按下键盘上方向键和下方向键将会发生什么?这个问题有些棘手,如果你认为活动状态和打开状态是完全不同的,那么答案就是“什么都不会发生”,因为我们没有定义任何在打开状态下键盘的交互行为,从另一个方面看,如果你认为活动状态和打开状态是有重叠定义部分,那么控件的值会被改变,但是被选中的选项不会正确的进行突出显示,也是因为我们没有在控件打开状态下定义任何键盘交互事件(我们仅仅定义了控件打开会发生什么,而没有定义在其打开后会发生什么)

在我们的例子中,缺少相关的规范是显而易见的,所以我们将处理他们,但是对于一些很新的小部件而言,没有人有合适其正确行为的想法。所以在设计阶段花费时间是值得的,因为如果你定义的行为不够好,或者忘记定义了一个行为,那么在用户实际使用时,将会很难去重新定义它们。如果你在定义时有疑问,请征询他人的意见,如果你有足够的时间去做好这件事情,请不要犹豫的去进行用户可行性测试,这个过程被称为UX design (User Experience Design) 用户体验设计,如果你想要深入的学习相关的内容,请查阅下面这些有用资源:

注意: 另外, 在绝大多数系统中,还有一种方法能够打开<select>元素来观察其所有的选项(这种方法的结果和点击<select>元素直接打开是一样的)通过windows下的Alt + 下箭头实现,在我们的例子中没有实现,但是这样做很容易,因为鼠标点击事件的表现和它的表现是一样的。

定义语义化的 HTML 结构

现在控件的基本功能已经决定了,可以开始构建自定义控件了。第一步是要确定 HTML 结构并给予一些基本的语义规则。我们需要重构<select>元素:

<!-- This is our main container for our widget.
     The tabindex attribute is what allows the user to focus the widget. 
     We'll see later that it's better to set it through JavaScript. -->
<div class="select" tabindex="0">
  
  <!-- This container will be used to display the current value of the widget -->
  <span class="value">Cherry</span>
  
  <!-- This container will contain all the option available for our widget.
       Because it's a list, it makes sense to use the ul element. -->
  <ul class="optList">
    <!-- Each option only contain the value to be displayed, we'll see later
         how to handle the real value that will be send with the form data -->
    <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>

注意类名的使用;这些定义了每一个对应类对象,并且不考虑实际的HTML中是哪个元素用了他们。这很重要,因为这样做能确保我们的CSS和JavaScript没有和HTML结构强绑定,这能让我们在之后能做一些改进,而不用担心破坏那些使用控件的代码。比如,如果你希望增加一个等价的<optgroup>元素。

使用 CSS 创建外观

现在我们有了控件结构,我们可以开始设计我们的控件了。构建自定义控件的意义是让它变得和我们需要的完全一致。为了达到这个目的,我们将 CSS部分的工作分为两部分:第一点是CSS规则,当然必须是让我们的控件变得像一个<select>元素,第二点是需要符合我们的设计稿。

所需的样式

所需的样式是那些用以处理我们组件的三种状态的必须样式。

.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;
}

我们需要一个额外的类来处理选项列表隐藏时的情况。为了处理激活状态和开启状态之间并不准确匹配时的差异,这是有必要的。

.select .optList.hidden {
  /* This is a simple way to hide the list in an accessible way, 
     we will talk more about accessibility in the end */
  max-height: 0;
  visibility: hidden;
}

美化

所以现在我们的基本功能已经就位,有趣的事情就可以开始了。下面是一个可行的简单的例子,和本文开头的截图是相对应的。然而,你应该自由地尝试并且看看你可以实现什么样的效果。

.select {
  /* All sizes will be expressed with the em value for accessibility reasons
     (to make sure the widget remains resizable if the user uses the  
     browser's zoom in a text-only mode). The computations are made
     assuming 1em == 16px which is the default value in most browsers.
     If you are lost with px to em conversion, try 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;

  /* We need extra room for the down arrow we will add */
  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 */
  
  /* The first declaration is for browsers that do not support linear gradients.
     The second declaration is because WebKit based browsers haven't unprefixed it yet.
     If you want to support legacy browsers, try 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 {
  /* Because the value can be wider than our widget, we have to make sure it will not
     change the widget's width */
  display  : inline-block;
  width    : 100%;
  overflow : hidden;

  vertical-align: top;

  /* And if the content overflows, it's better to have a nice ellipsis. */
  white-space  : nowrap;
  text-overflow: ellipsis;
}

我们不需要一个额外的元素来设计向下的箭头,而使用:after 伪类来替代。然而,这也可以通过使用一张加在select class上的简单的背景图像来实现。

.select:after {
  content : "▼"; /* We use the unicode caracter U+25BC; see http://www.utf8-chartable.de */
  position: absolute;
  z-index : 1; /* This will be important to keep the arrow from overlapping the list of options */
  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; /* We explicitly said the list of options will always overlap the down arrow */

  /* this will reset the default style of the ul element */
  list-style: none;
  margin : 0;
  padding: 0;

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

  /* This will ensure that even if the values are smaller than the widget,
     the list of options will be as large as the widget itself */
  min-width : 100%;

  /* In case the list is too long, its content will overflow vertically 
     (which will add a vertical scrollbar automatically) but never horizontally 
     (because we haven't set a width, the list will adjust its width automatically. 
     If it can't, the content will be truncated) */
  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;
}

所以这里我们的三种结果:

基本状态 活动状态 打开状态
Check out the source code

通过JavaScript让您的小部件生动起来

现在我们的设计和结构已经完成了。我们可以写些JavaScript代码来让这个部件真正生效。

警告:下面的代码仅仅是教学性质的,并且不应该像这样使用。这种方案是会过时的,而且可能在旧浏览器上会不工作。这里面还有冗余的部分,在生产环境下,代码需要优化。

注意:创建可复用的组件可能是一件需要些技巧的事情。W3C 网络组件草案 是对这类特定问题的特定答案之一。X-Tag 项目 是对这一规格的实验性实现;我们鼓励你看看它。

它为什么不生效?

在我们开始之前,要记住一件和 JavaScript 有关的非常重要的事情。在浏览器中,这是一种不可靠的技术。当你构建一个自定义组件时,你会不得不得依赖于 JavaScript,因为这是将所有的东西联系在一起的线索。但是,很多情况下,JavaScript 不能在浏览器中运行。

  • 用户关掉了 JavaScript: 这是最不常见的情形。现在只有很少的人会关掉 JavaScript。
  • 脚本没有加载。这是最常见的情形,特别是在移动端上,在世界上那些网络非常不可靠的地方。
  • 脚本是有问题的。你应该总是考虑这种可能性。
  • 脚本和第三方脚本冲突。这可能会由用户使用的跟踪脚本和任何用户使用的书签引发。
  • 脚本与一个浏览器的拓展冲突,或者受其影响。 (比如 Firefox 的 NoScript 拓展 或者 Chrome 的 NotScripts 拓展)。
  • 用户在使用老旧的浏览器,而且你需要的一些功能没有被支持。当你使用一些最新的 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>

第二,我们需要两个新的 classes 来隐藏不需要的元素。(那就是,当我们的脚本没有运行时显示的"真正的" <select> 元素, 而如果脚本正常运行就会显示自定义组件)。注意默认情况下,我们的 HTML 代码会隐藏我们的自定义组件。

.widget select,
.no-widget .select {
  /* This CSS selector basically says:
     - either we have set the body class to "widget" and thus we hide the actual <select> element
     - or we have not changed the body class, therefore the body class is still "no-widget",
       so the elements whose class is "select" must be hidden */
  position : absolute;
  left     : -5000em;
  height   : 0;
  overflow : hidden;
}

接下来我们需要一个 JavaScript 开关来确定脚本是否运行。这个开关非常简单:如果页面加载时,我们的脚本运行了,它将会移除 no-widget class ,并添加  widget class,由此切换 <select> 元素和自定义组件的可视性。

window.addEventListener("load", function () {
  document.body.classList.remove("no-widget");
  document.body.classList.add("widget");
});
无 JS 有 JS
Check out the source code

注意: 如果你真的想让你的代码变得通用和可重用,最好不要做一个 class 选择器开关,而是通过添加一个组件 class 的方式来隐藏<select> 元素,并且动态地在每一个<select> 元素后面添加 DOM 树表示来表示页面中的自定义组件。

让工作变得更简单

在我们将要构建的代码之中,我们将会使用标准的 DOM
API 来完成我们所要做的所有工作。尽管 DOM API 在浏览器中得到了更好支持,但是在旧的浏览器上还是会出现问题。( 特别是非常老的 Internet Explorer)。

如果你想要避免旧浏览器带来的麻烦,这儿有两种解决方案:使用专门的框架,比如 jQuery, $dom, prototype, Dojo, YUI, 或者类似的框架,或者通过填充你想使用的缺失的特性 (这可以通过传统的方式轻松实现——加载例如像 yepnope 这样的库。

我们打算使用的特性如下所示(按照风险程度从高到低排列):

  1. classList
  2. addEventListener
  3. forEach (这不是一个 DOM,但是是现代 JavaScript 的方法)
  4. querySelector 和 querySelectorAll

除了那些特定特性的的可用性以外,在开始之前,仍然存在一个问题。由函数querySelectorAll() 返回的对象是一个NodeList 而不是  Array。这一点非常重要,因为 Array  对象支持 forEach 函数,但是 NodeList 不支持。因为 NodeList 看起来实在是像一个 Array 并且 因为 forEach 是这样的便于使用。我们可以轻易地为 NodeList 添加一个支持以便于让我们的生活更轻松一些,像这样:

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

当我说这很容易做到的时候,我可没开玩笑。

构造事件回调

基础已经准备好了,我们现在可以开始定义所有会在用户每次同我们的组件交互时用到的函数了。

// This function will be used each time we want to deactivate a custom widget
// It takes one parameter
// select : the DOM node with the `select` class to deactivate
function deactivateSelect(select) {

  // If the widget is not active there is nothing to do
  if (!select.classList.contains('active')) return;

  // We need to get the list of options for the custom widget
  var optList = select.querySelector('.optList');

  // We close the list of option
  optList.classList.add('hidden');

  // and we deactivate the custom widget itself
  select.classList.remove('active');
}

// This function will be used each time the user wants to (de)activate the widget
// It takes two parameters:
// select : the DOM node with the `select` class to activate
// selectList : the list of all the DOM nodes with the `select` class
function activeSelect(select, selectList) {

  // If the widget is already active there is nothing to do
  if (select.classList.contains('active')) return;

  // We have to turn off the active state on all custom widgets
  // Because the deactivateSelect function fulfill all the requirement of the
  // forEach callback function, we use it directly without using an intermediate
  // anonymous function.
  selectList.forEach(deactivateSelect);

  // And we turn on the active state for this specific widget
  select.classList.add('active');
}

// This function will be used each time the user wants to open/closed the list of options
// It takes one parameter:
// select : the DOM node with the list to toggle
function toggleOptList(select) {

  // The list is kept from the widget
  var optList = select.querySelector('.optList');

  // We change the class of the list to show/hide it
  optList.classList.toggle('hidden');
}

// This function will be used each time we need to highlight an option
// It takes two parameters:
// select : the DOM node with the `select` class containing the option to highlight
// option : the DOM node with the `option` class to highlight
function highlightOption(select, option) {

  // We get the list of all option available for our custom select element
  var optionList = select.querySelectorAll('.option');

  // We remove the highlight from all options
  optionList.forEach(function (other) {
    other.classList.remove('highlight');
  });

  // We highlight the right option
  option.classList.add('highlight');
};

这是你需要的所有用来处理组件不同状态的代码。

接下来,我们将这些函数绑定到合适的事件上:

// We handle the event binding when the document is loaded.
window.addEventListener('load', function () {
  var selectList = document.querySelectorAll('.select');

  // Each custom widget needs to be initialized
  selectList.forEach(function (select) {

    // as well as all its `option` elements
    var optionList = select.querySelectorAll('.option');

    // Each time a user hovers their mouse over an option, we highlight the given option
    optionList.forEach(function (option) {
      option.addEventListener('mouseover', function () {
        // Note: the `select` and `option` variable are closures
        // available in the scope of our function call.
        highlightOption(select, option);
      });
    });

    // Each times the user click on a custom select element
    select.addEventListener('click', function (event) {
      // Note: the `select` variable is a closure
      // available in the scope of our function call.

      // We toggle the visibility of the list of options
      toggleOptList(select);
    });

    // In case the widget gain focus
    // The widget gains the focus each time the user clicks on it or each time
    // they use the tabulation key to access the widget
    select.addEventListener('focus', function (event) {
      // Note: the `select` and `selectList` variable are closures
      // available in the scope of our function call.

      // We activate the widget
      activeSelect(select, selectList);
    });

    // In case the widget loose focus
    select.addEventListener('blur', function (event) {
      // Note: the `select` variable is a closure
      // available in the scope of our function call.

      // We deactivate the widget
      deactivateSelect(select);
    });
  });
});

此时,我们的组件会根据我们的设计改变状态,但是它的值仍然没有更新。我们接下来会处理这件事。

Live example
Check out the source code

处理组件的值

既然我们的组件已经开始工作了,我们必须添加代码,使其能够根据用户的输入更新取值,并且能将取值随表单数据一同发送。

实现这一点的最简单的方式是使用一个被掩盖的原生组件。这样的一个组件会使用所有浏览器提供的内置控制器跟踪值,并且,在表单提交时,取值也会像往常情况一样发送。当所有一切都已经实现时,我们再造一遍轮子就毫无意义了。

像前面所看到的那样,处于可访问性的原因,我们已经使用了一个原生的选择组件作为后备显示内容;我们可以容易地通过它,将它的值与我们的自定义组件之间的值同步。

// This function updates the displayed value and synchronizes it with the native widget.
// It takes two parameters:
// select : the DOM node with the class `select` containing the value to update
// index  : the index of the value to be selected
function updateValue(select, index) {
  // We need to get the native widget for the given custom widget
  // In our example, that native widget is a sibling of the custom widget
  var nativeWidget = select.previousElementSibling;

  // We also need  to get the value placeholder of our custom widget
  var value = select.querySelector('.value');

  // And we need the whole list of options
  var optionList = select.querySelectorAll('.option');

  // We set the selected index to the index of our choice
  nativeWidget.selectedIndex = index;

  // We update the value placeholder accordingly
  value.innerHTML = optionList[index].innerHTML;

  // And we highlight the corresponding option of our custom widget
  highlightOption(select, optionList[index]);
};

// This function returns the current selected index in the native widget
// It takes one parameter:
// select : the DOM node with the class `select` related to the native widget
function getIndex(select) {
  // We need to access the native widget for the given custom widget
  // In our example, that native widget is a sibling of the custom widget
  var nativeWidget = select.previousElementSibling;

  return nativeWidget.selectedIndex;
};

通过这两个函数,我们可以将原生组件绑定到自定义的组件上。

// We handle event binding when the document is loaded.
window.addEventListener('load', function () {
  var selectList = document.querySelectorAll('.select');

  // Each custom widget needs to be initialized
  selectList.forEach(function (select) {
    var optionList = select.querySelectorAll('.option'),
        selectedIndex = getIndex(select);

    // We make our custom widget focusable
    select.tabIndex = 0;

    // We make the native widget no longer focusable
    select.previousElementSibling.tabIndex = -1;

    // We make sure that the default selected value is correctly displayed
    updateValue(select, selectedIndex);

    // Each time a user click on an option, we update the value accordingly
    optionList.forEach(function (option, index) {
      option.addEventListener('click', function (event) {
        updateValue(select, index);
      });
    });

    // Each time a user us its keyboard on a focused widget, we update the value accordingly
    select.addEventListener('keyup', function (event) {
      var length = optionList.length,
          index  = getIndex(select);

      // When the user hit the down arrow, we jump to the next option
      if (event.keyCode === 40 && index < length - 1) { index++; }

      // When the user hit the up arrow, we jump to the previous option
      if (event.keyCode === 38 && index > 0) { index--; }

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

在上面的代码里,值得注意的是 tabIndex 属性的使用。使用这个属性是很有必要的,这可以确保原生组件将永远不会获得焦点,而且还可以确保当用户用户使用键盘和鼠标时,我们的自定义组件能够获得焦点。

做完上面这些后,我们就完成了!下面是结果:

Live example
Check out the source code

但是等等,我们真的做完了嘛?

使其具有可访问性

我们构建了一个能够生效的东西,尽管这离一个特性齐全的选择框还差得远,但是它效果不错。但是我们已经做完的事情并不比摆弄DOM多多少。这个组件并没有真正的语义,甚至说它虽然看起来像一个选择框,但是从浏览器的角度来看并不是,所以辅助技术并不能明白这是一个选择框。简单说,这个全新的选择框并不具备可访问性!

幸运的是,有一种解决方案叫做 ARIA。ARIA代表"无障碍富互联网应用"。这是一个 W3C 规范,专为我们现在做的事情设计:使网络应用和自定义组件可访问,它基本上是一组拓展 HTML 的属性集,因此我们能够更好的描述角色,状态和属性,就好像我们刚才设计的元素是是它试图传达的元素本身。使用这些属性非常简单,所以让我们来试试看。

 role 属性

ARIA 使用的关键属性是 role 属性。role 属性接受一个值,该值定义了一个元素的用途。每一个 role 定义了它自己的需求和行为。在我们的例子中,我们会使用 listbox 这一 role。这是一个 "合成角色",表示具有该角色的元素应该有子元素,每个子元素都有特定的角色。(在这个案例中,至少有一个子元素具有option 角色)。

还有一件值得注意的事情是,ARIA定义了应用于标准 HTML 标记的角色。例如,<table> 元素匹配了角色 grid,而 <ul> 元素匹配了角色 list。因为我们使用了一个 <ul> 元素,我们想要确保我们组件的 listbox 角色能替代 <ul> 元素的list 角色。为了达到这一点,我们会使用角色 presentation。这个角色被设计成让我们来表示一个没有特殊含义的元素,并且仅仅用于提供信息。我们会将其应用到<ul> 元素上。

为了支持 listbox 角色,我们只需要将我们 TML 改成这样:

<!-- We add the role="listbox" attribute to our top element -->
<div class="select" role="listbox">
  <span class="value">Cherry</span>
  <!-- We also add the role="presentation" to the ul element -->
  <ul class="optList" role="presentation">
    <!-- And we add the role="option" attribute to all the li elements -->
    <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');

  // We make sure that all the options are not selected
  optionList.forEach(function (other) {
    other.setAttribute('aria-selected', 'false');
  });

  // We make sure the chosen option is selected
  optionList[index].setAttribute('aria-selected', 'true');

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

这是经过所有的改变之后的最终结果。 ( 藉由 NVDA or VoiceOver 这样的辅助技术尝试它,你会对此有更好的体会):

在线示例
Check out the final source code

总结

我们已经了解了所有和构建一个自定义表单组件相关的基础知识,但是如你所见做这件事非常繁琐,并且通常情况下依赖第三方库,而不是自己从头写起会更容易 ,也更好(当然,除非你的目的就是构建一个这样的库)。

这儿有一些库,在你编写自己的之前应该了解一下:

如果你想更进一步, 本例中的代码需要一些改进,才能变得更加通用和可重用。这是一个你可以尝试去做的联系。这里有两个提示可以帮到你:我们所有函数的第一个参数是相同的,这意味着这些函数需要相同的上下文。构建一个对象来共享那些上下文是更聪明的做法。还有,你需要让它的特性适用性更好;也就是说,它要能在一系列对Web标准的兼容性不同的浏览器上工作良好。祝愉快!

 

 

在本单元中

 

文档标签和贡献者

标签: 
最后编辑者: lonelywhisper,