在许多情况下,[可用的HTML表单组件]()是不够的。若你想在诸如<select>
元素的组件上[应用高级样式]()、或者想定制组件的行为,你就只能选择创建自己的表单组件。
我们将通过本文学习如何构建一个表单组件。为达到目的,我们选择重构<select>
元素作为例子。
注意:我们会专注于构建组件,但不会关注如何保证代码的通用和可重用。构建组件时会涉及到一些特殊的JavaScript代码和未知上下文中的DOM操作,而这些内容已经超出了本文的讨论范围。
设计,结构和语义
在构建一个定制组件前,应先从明确你想要达到的效果开始,这会节省你宝贵的时间。具体来讲,清晰地定义组件的所有状态是很重要的。要做到这点,最好从一个已经存在的、状态和行为已经为人所熟知的组件开始,这样你就只需尽可能地模仿该组件即可。
在我们的例子中,我们会重构<select>
元素。下面是我们期望达到的结果:
上面的截屏展示了我们组件的三个主要状态:普通状态(左)、激活状态(中)和打开状态(右)。
至于组件的行为,我们希望可以像其他原生组件一样,通过鼠标和键盘来操控它。先从定义组件如何到达各个状态开始:
组件变为普通状态:
页面加载
组件激活且用户点击了组件外任意地方
组件激活且用户用键盘把焦点移动到别的组件
注意:在页面上移动焦点通常是通过敲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:你要经常考虑这一可能性。
脚本和第三方脚本冲突了:使用了追踪脚本或用户自用的书签时会发生这种情况。
用户使用了老旧浏览器,并且你需要的一种特性不被支持:这通常发生在你用了很新的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库)。
我们计划使用的特性如下(从风险最大到最安全排列):
forEach(不属于DOM但是现代JavaScript的特性)
除了上述特性的可用性,在开发之前仍存在一个问题。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]);
};
上述修改的最终效果如下(访问该组件时使用无障碍技术,譬如NVDA或VoiceOver,会有更好的体验):
结论
至此我们已经了解了创建定制表单组件的所有基本知识,但如你所见,这么做并不简单,如果使用第三方库的话会比自己从头写起更好、更简单(当然除非你是想构建这样一个库)。
下面是你在自己开发之前应该参考下的库:
若你想更进一步使用本例,为让其中的代码变得通用和可复用,还要对代码做一些改进。这个练习你可以自己尝试下,这里有两个提示:首先,所有函数的第一个参数都相同,这就意味着这些函数需要有同一个执行上下文,使用一个对象来共享执行上下文是很明智的。此外,代码还得保证兼容,即代码最好能在兼容不同Web标准的多种浏览器下运行。