无障碍功能优先:下拉菜单(选择)
我一直在思考如何将这个无法自定义的select元素变成一个易于使用的、类似 jQuery 的选择菜单。当然,如果不需要自定义,那就尽量select使用原生实现,原生实现总是更胜一筹。
我当时已经有了一个不错的“概念验证”,正打算把它完善成一篇文章,结果就在这时,@emmabostian发表了一篇类似的精彩文章。我建议你也读读她的文章,真的很不错。
这是我的成品,下面我将详细介绍我的制作过程和原因:
要求
为此,我们需要同时支持键盘和鼠标,所以让我们看看规范,了解一下预期行为是什么。
键盘
- 按 Enter 键,切换列表框可见性
- 按 Esc 键,隐藏列表框
- 向下箭头,选择列表中的下一个选项
- 按向上箭头,选择列表中的上一个选项
- 主屏幕钥匙,选择列表中的第一个选项
- 按结束键,选择列表中的最后一个选项
老鼠
- 点击输入框,切换列表框可见性
- 点击输入框外,隐藏列表框
- 单击该选项,将其设置为活动状态并隐藏列表框。
根据规格来看,我认为这就是我们需要的全部内容,但我随时欢迎指正。
标记
为此,我使用了<details>内置了我想要的显示和隐藏功能的元素。
我还使用了一组单选按钮,以便存储哪个值是正确的。checked如果您需要设置默认选项,只需将其添加到默认选项即可。
<details id="example_select" class="select_container">
<summary>--</summary>
<div class="select">
<label class="select__option">
<input type="radio" name="example" value="slower">Slower
</label>
<label class="select__option">
<input type="radio" name="example" value="slow">Slow
</label>
<label class="select__option">
<input type="radio" name="example" value="medium">Medium
</label>
<label class="select__option">
<input type="radio" name="example" value="fast">Fast
</label>
<label class="select__option">
<input type="radio" name="example" value="faster">Faster
</label>
</div>
</details>
如果没有样式,你就能真正看出它的工作原理了。我们只有一列单选按钮,就这么简单。
款式
和往常一样,我不会赘述太多细节,这只是我个人的偏好。你可能已经注意到我把它做得像 jQuery 版本,但你可以随意发挥。
details.select_container {
display: inline-block;
width: 200px;
border: 1px solid #c5c5c5;
border-radius: 3px;
position: relative;
color: #454545;
}
details.select_container[open] {
border-radius: 3px 3px 0 0;
}
details.select_container summary::after {
content: "\00203A";
position: absolute;
right: 12px;
top: calc(50%);
transform: translateY(-50%) rotate(90deg);
pointer-events: none;
}
details.select_container[open] summary::after {
content: "\002039";
}
details.select_container summary {
cursor: pointer;
padding: 6px 12px;
background: #f6f6f6;
list-style: none;
}
details.select_container summary::-webkit-details-marker {
display: none;
}
details.select_container summary:hover {
background: #ededed;
}
details.select_container .select {
position: absolute;
display: flex;
flex-direction: column;
border: 1px solid #c5c5c5;
width: 100%;
left: -1px;
border-radius: 0 0 3px 3px;
background: #fff;
}
details.select_container .select__option {
cursor: pointer;
padding: 6px 12px;
}
details.select_container .select:hover .select__option.active {
background: #fff;
color: #454545;
}
details.select_container .select__option.active,
details.select_container .select:hover .select__option.active:hover,
details.select_container .select__option:hover {
background: #007fff;
color: #fff;
}
details.select_container .select__option input {
display: none;
}
所有的智能功能都是通过 JavaScript 实现的。
JavaScript
与以往的项目不同,这次我使用 JS 来设置所有 aria 属性,这意味着您无需记住手动设置,这无疑是一大优势。和以往一样,我使用了类,这并非强制要求,而是因为我喜欢类。
我不会赘述太多细节,您可以自己阅读。如果您有任何不明白的地方,请随时提问。但我会提及它this.mouseDown以及它存在的原因。
我原本打算focusout在选择菜单失去焦点时关闭它,但发现我的click事件处理程序不再起作用了。经过一番研究,我意识到焦点是在鼠标按下时丢失的,而点击事件却是在鼠标抬起时触发的。为了解决这个问题,我必须监听选项菜单的鼠标按下事件,以防止focusout出现问题。
class detailSelect {
constructor(container) {
this.container = document.querySelector(container);
this.options = document.querySelectorAll(`${container} > .select > .select__option`);
this.value = this.container.querySelector('summary').textContent;
this.mouseDown = false;
this._addEventListeners();
this._setAria();
this.updateValue();
}
// Private function to set event listeners
_addEventListeners() {
this.container.addEventListener('toggle', () => {
if (this.container.open) return;
this.updateValue();
})
this.container.addEventListener('focusout', e => {
if (this.mouseDown) return;
this.container.removeAttribute('open');
})
this.options.forEach(opt => {
opt.addEventListener('mousedown', () => {
this.mouseDown = true;
})
opt.addEventListener('mouseup', () => {
this.mouseDown = false;
this.container.removeAttribute('open');
})
})
this.container.addEventListener('keyup', e => {
const keycode = e.which;
const current = [...this.options].indexOf(this.container.querySelector('.active'));
switch (keycode) {
case 27: // ESC
this.container.removeAttribute('open');
break;
case 35: // END
e.preventDefault();
if (!this.container.open) this.container.setAttribute('open', '');
this.setChecked(this.options[this.options.length - 1].querySelector('input'))
break;
case 36: // HOME
e.preventDefault();
if (!this.container.open) this.container.setAttribute('open', '');
this.setChecked(this.options[0].querySelector('input'))
break;
case 38: // UP
e.preventDefault();
if (!this.container.open) this.container.setAttribute('open', '');
this.setChecked(this.options[current > 0 ? current - 1 : 0].querySelector('input'));
break;
case 40: // DOWN
e.preventDefault();
if (!this.container.open) this.container.setAttribute('open', '');
this.setChecked(this.options[current < this.options.length - 1 ? current + 1 : this.options.length - 1].querySelector('input'));
break;
}
})
}
_setAria() {
this.container.setAttribute('aria-haspopup', 'listbox');
this.container.querySelector('.select').setAttribute('role', 'listbox');
const summary = this.container.querySelector('summary');
summary.setAttribute('aria-label', `unselected listbox`);
summary.setAttribute('aria-live', `polite`);
this.options.forEach(opt => {
opt.setAttribute('role', 'option');
});
}
updateValue(e) {
const that = this.container.querySelector('input:checked');
if (!that) return;
this.setValue(that)
}
setChecked(that) {
that.checked = true;
this.setValue(that)
}
setValue(that) {
if (this.value == that.value) return;
const summary = this.container.querySelector('summary');
const pos = [...this.options].indexOf(that.parentNode) + 1;
summary.textContent = that.parentNode.textContent;
summary.setAttribute('aria-label', `${that.value}, listbox ${pos} of ${this.options.length}`);
this.value = that.value;
this.options.forEach(opt => {
opt.classList.remove('active');
opt.setAttribute('aria-selected', 'false');
})
that.parentNode.classList.add('active');
that.parentNode.setAttribute('aria-selected', 'true');
this.container.dispatchEvent(new Event('change'));
}
}
const details = new detailSelect('#example_select');
然后我们创建一个实例
const details = new detailSelect('#example_select');
如果我有什么可以改进的地方,请告诉我,我很想听听您的意见。
鳍
本系列文章又更新完毕Accessibility first。感谢阅读,如有任何疑问,欢迎随时提问,没有愚蠢的问题。和往常一样,您可以随意使用文中提到的任何技巧,如果我说错了什么,也请随时指正。
再次感谢
。❤🦄🦄🧠❤🦄❤❤🦄
