创建自定义、可访问的下拉菜单
步骤 1 - HTML
下拉列表选择 {
注意:我仍在学习可访问性,因此如果您发现我的方法有缺陷,请在下面的评论中告诉我!
创建自定义组件非常困难。你必须覆盖大量默认浏览器样式,这通常非常繁琐。在某些情况下,甚至无法设置 HTML 元素的样式。选择下拉菜单就是这种情况。
无法设置选择下拉菜单的样式,因为我们无法将<option>
元素集包装在容器中(为了将列表项绝对定位在父元素中,这是必需的)。
因此,我们必须“破解”自己的方法来创建下拉菜单。不幸的是,这通常会导致可访问性不足。
在本教程中,我们将学习如何创建自定义选择下拉菜单,同时遵守 W3C 可访问性标准。
步骤 1 - HTML
以下是我们将要创建的下拉菜单:
传统上,当创建选择下拉菜单时,您将使用以下内容:
<select>
<option value="option-1">Option 1</option>
<option value="option-2">Option 2</option>
<option value="option-3">Option 3</option>
</select>
使用该元素的问题<select>
在于,你无法将子<option>
元素包裹在容器中。为什么我们需要将这些元素包裹在容器中呢?为了将下拉列表放置在输入框下方。
在我们的例子中,我们希望列表项(<option>
元素)位于<select>
框的下方。浏览器默认将菜单项渲染为覆盖层:
要相对于父元素相对定位子元素,例如自定义下拉菜单的情况,您必须设置以下 CSS 属性:
.parent {
position: relative;
}
.child {
position: absolute;
top: 0;
left: 0;
}
您可能会想:“您不能将 HTML 重写为以下内容(使用上面的 CSS)吗?
<select class="parent">
<div class="child">
<option value="option-1">Option 1</option>
<option value="option-2">Option 2</option>
<option value="option-3">Option 3</option>
</div>
</select>
很遗憾,答案是否定的。您不能将 a<div>
放在 a 里面<select>
。
因此我们必须创建一个可行的解决方法。
创建自定义选择
由于我们不能使用<select>
元素,我选择使用一系列<ul>
和<li>
元素。
其结构看起来是这样的:
<ul class="dropdown">
<li class="dropdown__label">
Label
</li>
<!-- The "select" drop down -->
<li role="button" id="dropdown__selected" tabindex="0">Option 1</li>
<!-- Icon -->
<svg class="dropdown__arrow" width="10" height="5" viewBox="0 0 10 5" fill-rule="evenodd">
<path d="M10 0L5 5 0 0z"></path>
</svg>
<li class="dropdown__list-container">
<ul class="dropdown__list">
<li class="dropdown__list-item" id="option-1">Option 1</li>
<li class="dropdown__list-item" id="option-2">Option 2</li>
</ul>
</li>
</ul>
这很简单。
- 我们将整个组件包裹在一个无序列表中。
- 该标签是一个列表项。
- 选择也是一个列表项。
- 接下来是下拉箭头图标。最后,列表项菜单被包裹在一个子无序列表中。
但是……这个页面无法访问。如果视障用户借助辅助技术访问这个页面,他们根本不知道这是一个下拉菜单,也不知道该如何操作。此外,它完全无法通过键盘访问。
使自定义元素可访问
自定义元素在键盘导航和屏幕阅读器可访问性方面必须具有与语义元素相同的功能。
为了使此屏幕阅读器可访问,我们需要执行以下操作:
- 下拉标签必须有一个 ID。因为我们将使用 ,
aria-labelledby
它将<li>
充当选择下拉菜单,而此属性接受id
标记它的 HTML 的 。我将其 ID 设置为dropdown-label
。 <li>
作为选择下拉菜单的功能必须具有role="button"
以及aria-labelledby="dropdown-label"
。- 该
<svg>
元素需要额外的信息来描述它是什么。因此,我们可以添加一个<title>Open drop down</title>
作为 SVG 的第一个子元素。 - 下拉列表容器需要告知用户菜单是否展开。我们可以添加一个
aria-expanded="false"
属性来传达此信息。该属性必须随着状态的变化使用 JavaScript 进行更新。
为了使这个键盘可访问,我们需要执行以下操作:
- 用作
<li>
选择下拉菜单的需要一个,tabindex="0"
以便用户可以将焦点集中在元素上。 <li>
下拉菜单中的所有也需要tabindex="0"
。
以下是可访问的 HTML:
<ul class="dropdown">
<li id="dropdown-label" class="dropdown__label">
Label
</li>
<li
role="button"
aria-labelledby="dropdown-label"
id="dropdown__selected"
tabindex="0"
>
Option 1
</li>
<svg
class="dropdown__arrow"
width="10"
height="5"
viewBox="0 0 10 5"
fill-rule="evenodd"
>
<title>Open drop down</title>
<path d="M10 0L5 5 0 0z"></path>
</svg>
<li aria-expanded="false" role="list" class="dropdown__list-container">
<ul class="dropdown__list">
<li class="dropdown__list-item" tabindex="0" id="option-1">
Option 1
</li>
<li class="dropdown__list-item" tabindex="0" id="option-2">
Option 2
</li>
</ul>
</li>
</ul>
我们还需要添加一些 JavaScript 逻辑,以确保组件能够像原生下拉菜单一样运行。预期的交互如下:
- 用户可以使用键盘聚焦于该元素。
- 用户可以通过按空格键或 Enter 键来打开选择下拉菜单。
- 用户可以使用向上和向下箭头键或 Tab 键导航列表项元素。
- 用户可以通过聚焦列表项并按 Enter 键来更改选择。
- 用户可以按 Esc 键关闭下拉菜单。
- 一旦用户选择了列表项,列表就应该关闭。
那么现在让我们来实现它。
使用 JavaScript 实现键盘辅助功能
首先,我们需要获取空格键、Enter 键、上下箭头键以及 Esc 键的键码。(我见过空格键的键码是 0 和 32,所以为了安全起见,我把它设置为 0 和 32)。
const SPACEBAR_KEY_CODE = [0,32];
const ENTER_KEY_CODE = 13;
const DOWN_ARROW_KEY_CODE = 40;
const UP_ARROW_KEY_CODE = 38;
const ESCAPE_KEY_CODE = 27;
接下来,我们需要一些元素。我会将它们保存为常量。我们还需要跟踪列表项的 ID,因此我将声明一个空数组,以便填充它。
const list = document.querySelector(".dropdown__list");
const listContainer = document.querySelector(".dropdown__list-container");
const dropdownArrow = document.querySelector(".dropdown__arrow");
const listItems = document.querySelectorAll(".dropdown__list-item");
const dropdownSelectedNode = document.querySelector("#dropdown__selected");
const listItemIds = [];
接下来,我们需要为元素添加一些事件监听器,以确保它们能够响应用户交互。先别担心这里声明的函数,我们很快就会讲到它们。
dropdownSelectedNode.addEventListener("click", e =>
toggleListVisibility(e)
);
dropdownSelectedNode.addEventListener("keydown", e =>
toggleListVisibility(e)
);
// Add each list item's id to the listItems array
listItems.forEach(item => listItemIds.push(item.id));
listItems.forEach(item => {
item.addEventListener("click", e => {
setSelectedListItem(e);
closeList();
});
item.addEventListener("keydown", e => {
switch (e.keyCode) {
case ENTER_KEY_CODE:
setSelectedListItem(e);
closeList();
return;
case DOWN_ARROW_KEY_CODE:
focusNextListItem(DOWN_ARROW_KEY_CODE);
return;
case UP_ARROW_KEY_CODE:
focusNextListItem(UP_ARROW_KEY_CODE);
return;
case ESCAPE_KEY_CODE:
closeList();
return;
default:
return;
}
});
});
现在让我们创建一些我们刚刚在事件监听器中调用的函数。setSelectedListItem
接受一个事件并更新“选择”框中当前选定的项目。
function setSelectedListItem(e) {
let selectedTextToAppend = document.createTextNode(e.target.innerText);
dropdownSelectedNode.innerHTML = null;
dropdownSelectedNode.appendChild(selectedTextToAppend);
}
closeList
关闭列表并更新aria-expanded
值。
function closeList() {
list.classList.remove("open");
dropdownArrow.classList.remove("expanded");
listContainer.setAttribute("aria-expanded", false);
}
toggleListVisibility
接受一个事件。如果按下了 Esc 键,则关闭列表。否则,如果用户点击了列表,或者按下了空格键或 Enter 键,则切换展开状态并aria-expanded
相应地更新值。最后,如果按下了向下或向上箭头键,则将焦点移至下一个列表项。
function toggleListVisibility(e) {
let openDropDown = SPACEBAR_KEY_CODE.includes(e.keyCode) || e.keyCode === ENTER_KEY_CODE;
if (e.keyCode === ESCAPE_KEY_CODE) {
closeList();
}
if (e.type === "click" || openDropDown) {
list.classList.toggle("open");
dropdownArrow.classList.toggle("expanded");
listContainer.setAttribute(
"aria-expanded",
list.classList.contains("open")
);
}
if (e.keyCode === DOWN_ARROW_KEY_CODE) {
focusNextListItem(DOWN_ARROW_KEY_CODE);
}
if (e.keyCode === UP_ARROW_KEY_CODE) {
focusNextListItem(UP_ARROW_KEY_CODE);
}
}
focusNextListItem
方向可以是 constDOWN_ARROW_KEY_PRESSED
或UP_ARROW_KEY_PRESSED
。如果用户当前焦点在“select”上,则焦点位于第一个列表项上。否则,我们需要找到当前焦点列表项的索引。这时listItemsId
数组就派上用场了。现在我们知道了当前焦点项在列表中的位置,我们就可以决定下一步该怎么做了。
如果用户按下向下箭头键,且当前不在最后一个列表项,则将焦点移至下一个列表项。如果用户按下向上箭头键,且当前不在第一个列表项,则将焦点移至上一个列表项。
function focusNextListItem(direction) {
const activeElementId = document.activeElement.id;
if (activeElementId === "dropdown__selected") {
document.querySelector(`#${listItemIds[0]}`).focus();
} else {
const currentActiveElementIndex = listItemIds.indexOf(activeElementId);
if (direction === DOWN_ARROW_KEY_CODE) {
const currentActiveElementIsNotLastItem =
currentActiveElementIndex < listItemIds.length - 1;
if (currentActiveElementIsNotLastItem) {
const nextListItemId = listItemIds[currentActiveElementIndex + 1];
document.querySelector(`#${nextListItemId}`).focus();
}
} else if (direction === UP_ARROW_KEY_CODE) {
const currentActiveElementIsNotFirstItem =
currentActiveElementIndex > 0;
if (currentActiveElementIsNotFirstItem) {
const nextListItemId = listItemIds[currentActiveElementIndex - 1];
document.querySelector(`#${nextListItemId}`).focus();
}
}
}
}
就这样!现在你有一个完全兼容键盘访问的下拉菜单!我不会在这里介绍 Sass/CSS,但欢迎你在CodePen上查看。
文章来源:https://dev.to/emmabostian/creating-a-custom-accessible-drop-down-3gmo