We're looking for a user researcher to understand the needs of developers and designers. Is this you or someone you know? Check out the post: https://mzl.la/2IGzdXS

这篇翻译不完整。请帮忙从英语翻译这篇文章

某些情况下可用的 HTML 表单小组件力有不逮。如欲在某组件上 表现高级风格 例如 <select> 元素,又如欲提供定制行为等,别无选择,唯有构造自己的表单组件。

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

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

设计, 结构, 和语义

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

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

The three states of a select box

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

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

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

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

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

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

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

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

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

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

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

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

Note: 另外, 在绝大多数系统中,还有一种方法能够打开<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>元素,第二点是需要符合我们的设计稿。

所需的样式

The required styles are those necessary to handle the three states of our widget.

.select {
  /* This will create a positioning context for the list of options */
  position: relative;
 
  /* This will make our widget become part of the text flow and sizable at the same time */
  display : inline-block;
}

We need an extra class active to define the look and feel of our widget when it is in its active state. Because our widget is focusable, we double this custom style with the :focus pseudo-class in order to be sure they will behave the same.

.select.active,
.select:focus {
  outline: none;
 
  /* This box-shadow property is not exactly required, however it's so important to be sure
     the active state is visible that we use it as a default value, feel free to override it. */
  box-shadow: 0 0 3px 1px #227755;
}

Now, let's handle the list of options:

/* The .select selector here is syntactic sugar to be sure the classes we define are
   the ones inside our widget. */
.select .optList {
  /* This will make sure our list of options will be displayed below the value
     and out of the HTML flow */
  position : absolute;
  top      : 100%;
  left     : 0;
}

We need an extra class to handle when the list of options is hidden. This is necessary in order to manage the differences between the active state and the open state that do not exactly match.

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

美化

So now that we have the basic functionality in place, the fun can start. The following is just an example of what is possible, and will match the screenshot at the beginning of this article. However, you should feel free to experiment and see what you can come up with.

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

We don't need an extra element to design the down arrow; instead, we're using the :after pseudo-element. However, it could also be implemented using a simple background image on the 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;
}

Next, let's style the list of options:

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

For the options, we need to add a highlight class to be able to identify the value the user will pick (or has picked).

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

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

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

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

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

Now that our design and structure are ready, we can write the JavaScript code to make the widget actually work.

Warning: The following code is educational and should not be used as-is. Among many things, as we'll see, it is not future-proof and it will not work on legacy browsers. It also has redundant parts that should be optimized in production code.

Note: Creating reusable widgets is something that can be a bit tricky. The W3C Web Component draft is one of the answers to this specific issue. The X-Tag project is a test implementation of this specification; we encourage you to take a look at it.

Why isn't it working?

Before we start, it's important to remember something very important about JavaScript: inside a browser, it's an unreliable technology. When you are building custom widgets, you'll have to rely on JavaScript because it's a necessary thread to tie everything together. However, there are many cases in which JavaScript isn't able to run in the browser:

  • The user has turned off JavaScript: This is the most unusual case ever; very few people turn off JavaScript nowadays.
  • The script is not loading. This is one of the most common cases, especially in the mobile world where the network is not very reliable.
  • The script is buggy. You should always consider this possibility.
  • The script is in conflict with a third party script. This can happen with tracking scripts or any bookmarklets the user uses.
  • The script is in conflict with, or is affected by, a browser extension (such as Firefox's NoScript extension or Chrome's NotScripts extension).
  • The user is using a legacy browser, and one of the features you require is not supported. This will happen frequently when you make use of cutting-edge APIs.

Because of these risks, it's really important to seriously consider what will happen if JavaScript isn't working. Dealing in detail with this issue is out of the scope of this article because it's closely linked to how you want to make your script generic and reusable, but we'll consider the basics of this in our example.

In our example, if our JavaScript code isn't running, we'll fall back to displaying a standard <select> element. To achieve this, we need two things.

First, we need to add a regular <select> element before each use of our custom widget. This is actually also required in order to be able to send data from our custom widget along with the rest of our form data; more about this later.

<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>

Second, we need two new classes to let us hide the unneeded element (that is, the "real" <select> element if our script isn't running, or the custom widget if it is running). Note that by default, our HTML code hides our custom widget.

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

Now we just need a JavaScript switch to determine if the script is running or not. This switch is very simple: if at page load time our script is running, it will remove the no-widget class and add the widget class, thereby swapping the visibility of the <select> element and of the custom widget.

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

Note: If you really want to make your code generic and reusable, instead of doing a class switch it's far better to just add the widget class to hide the <select> elements, and to dynamically add the DOM tree representing the custom widget after every <select> element in the page.

Making the job easier

In the code we are about to build, we will use the standard DOM API to do all the work we need. However, although DOM API support has gotten much better in browsers, there are always issues with legacy browsers (especially with good old Internet Explorer).

If you want to avoid trouble with legacy browsers, there are two ways to do so: using a dedicated framework such as jQuery, $dom, prototype, Dojo, YUI, or the like, or by polyfilling the missing feature you want to use (which can easily be done through conditional loading, with the yepnope library for example).

The features we plan to use are the following (ordered from the riskiest to the safest):

  1. classList
  2. addEventListener
  3. forEach (This is not DOM but modern JavaScript)
  4. querySelector and querySelectorAll

Beyond the availability of those specific features, there is still one issue remaining before starting. The object returned by the querySelectorAll() function is a NodeList rather than an Array. This is important because Array objects support the forEach function, but NodeList doesn't. Because NodeList really looks like an Array and because forEach is so convenient to use, we can easily add the support of forEach to NodeList in order to make our life easier, like so:

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

We weren't kidding when we said it's easy to do.

Building event callbacks

The ground is ready, we can now start to define all the functions that will be used each time the user interacts with our widget.

// 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');
};

That's all you need in order to handle the various states of the custom widget.

Next, we bind these functions to the appropriate events:

// 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);
    });
  });
});

At that point, our widget will change state according to our design, but its value doesn't get updated yet. We'll handle that next.

Live example
Check out the source code

Handling the widget's value

Now that our widget is working, we have to add code to update its value according to user input and make it possible to send the value along with form data.

The easiest way to do this is to use a native widget under the hood. Such a widget will keep track of the value with all the built-in controls provided by the browser, and the value will be sent as usual when a form is submitted. There's no point in reinventing the wheel when we can have all this done for us.

As seen previously, we already use a native select widget as a fallback for accessibility reasons; we can simply synchronize its value with that of our custom widget:

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

With these two functions, we can bind the native widgets to the custom ones:

// 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);
    });
  });
});

In the code above, it's worth noting the use of the tabIndex property. Using this property is necessary to ensure that the native widget will never gain focus, and to make sure that our custom widget gains focus when the user uses his keyboard or his mouse.

With that, we're done! Here's the result:

Live example
Check out the source code

But wait a second, are we really done?

Make it accessible

We have built something that works and though we're far from a fully-featured select box, it works nicely. But what we've done is nothing more than fiddle with the DOM. It has no real semantics, and even though it looks like a select box, from the browser's point of view it isn't one, so assistive technologies won't be able to understand it's a select box. In short, this pretty new select box isn't accessible!

Fortunately, there is a solution and it's called ARIA. ARIA stands for "Accessible Rich Internet Application", and it's a W3C specification specifically designed for what we are doing here: making web applications and custom widgets accessible. It's basically a set of attributes that extend HTML so that we can better describe roles, states and properties as though the element we've just devised was the native element it tries to pass for. Using these attributes is dead simple, so let's do it.

The role attribute

The key attribute used by ARIA is the role attribute. The role attribute accepts a value that defines what an element is used for. Each role defines its own requirements and behaviors. In our example, we will use the listbox role. It's a "composite role", which means elements with that role expect to have children, each with a specific role (in this case, at least one child with the option role).

It's also worth noting that ARIA defines roles that are applied by default to standard HTML markup. For example, the <table> element matches the role grid, and the <ul> element matches the role list. Because we use a <ul> element, we want to make sure the listbox role of our widget will supersede the list role of the <ul> element. To that end, we will use the role presentation. This role is designed to let us indicate that an element has no special meaning, and is used solely to present information. We will apply it to our <ul> element.

To support the listbox role, we just have to update our HTML like this:

<!-- 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>

Note: Including both the role attribute and a class attribute is only necessary if you want to support legacy browsers that do not support the CSS attribute selectors.

The aria-selected attribute

Using the role attribute is not enough. ARIA also provides many states and property attributes. The more and better you use them, the better your widget will be understood by assistive technologies. In our case, we will limit our usage to one attribute: aria-selected.

The aria-selected attribute is used to mark which option is currently selected; this lets assistive technologies inform the user what the current selection is. We will use it dynamically with JavaScript to mark the selected option each time the user chooses one. To that end, we need to revise our updateValue() function:

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]);
};

Here is the final result of all these changes (you'll get a better feel for this by trying it with an assistive technology such as NVDA or VoiceOver):

Live example
Check out the final source code

Conclusion

We have seen all the basics of building a custom form widget, but as you can see it's not trivial to do, and often it's better and easier to rely on third-party libraries instead of coding them from scratch yourself (unless, of course, your goal is to build such a library).

Here are a few libraries you should consider before coding your own:

If you want to move forward, the code in this example needs some improvement before it becomes generic and reusable. This is an exercise you can try to perform. Two hints to help you in this: the first argument for all our functions is the same, which means those functions need the same context. Building an object to share that context would be wise. Also, you need to make it feature-proof; that is, it needs to be able to work better with a variety of browsers whose compatibility with the Web standards they use vary. Have fun!

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

文档标签和贡献者

最后编辑者: tinyhare,