提问人:Oscar R 提问时间:11/18/2023 最后编辑:Oscar R 更新时间:11/18/2023 访问量:76
如何设置 HTML 元素的 tabindex,使其对应于 Tab 键顺序中的下一个元素?
How can I set the tabindex of an HTML element so that it corresponds to the next element in the tab order?
问:
在下面的代码中,您可以看到一个简化的表工作示例,其中每行都有一个选项按钮,该按钮会触发包含更多按钮的菜单。但是,当您尝试按 Tab 键浏览可聚焦元素时,您会看到尽管打开了菜单,但在进入菜单之前,您仍然需要按 Tab 键浏览所有行。
我想要的结果是,例如,如果您将“选项”按钮集中在第 1 行,然后继续打开它,则下一次按下选项卡应该会将您带入菜单的第一个按钮。然后,当您按 Tab 键浏览菜单的所有按钮时,您应该会回到第 2 行的“选项”按钮。
如何实现此目的?
(此外,我无法删除溢出:从
表中隐藏,这就是为什么我首先在表元素之外使用绝对定位的菜单)
const menu = document.getElementById("menu");
const table = document.getElementById("table");
const option_buttons = Array.from(table.querySelectorAll("button"));
table.addEventListener("click", (e) => {
if (option_buttons.includes(e.target)) {
e.stopPropagation();
menu.style.top = e.target.getBoundingClientRect().top + "px";
menu.style.display = "flex";
document.documentElement.addEventListener("click", (e) => {
if (e.target !== menu && !menu.contains(e.target)) {
menu.style.display = "none";
}
});
}
});
.table {
overflow: hidden;
}
.row {
display: flex;
gap: 2rem;
padding: 0.5rem 0;
border-bottom: 0.0625rem solid #ccc;
}
.menu {
display: none;
position: absolute;
left: 11rem;
padding: 0.5rem;
background: #ccc;
flex-direction: column;
gap: 0.5rem;
}
.menu::before {
content: '';
position: absolute;
top: 0;
left: -0.75rem;
width: 0;
height: 0;
border-top: 0.75rem solid transparent;
border-bottom: 0.75rem solid transparent;
border-right: 0.75rem solid #ccc;
}
button:focus {
background: skyblue;
}
<div class="table" id="table">
<div class="row">
<span>Item 1</span>
<button class="btn_option" type="button">Options</button>
</div>
<div class="row">
<span>Item 2</span>
<button class="btn_option" type="button">Options</button>
</div>
<div class="row">
<span>Item 3</span>
<button class="btn_option" type="button">Options</button>
</div>
<div class="row">
<span>Item 4</span>
<button class="btn_option" type="button">Options</button>
</div>
<div class="row">
<span>Item 5</span>
<button class="btn_option" type="button">Options</button>
</div>
</div>
<div class="menu" id="menu">
<button type="button">View</button>
<button type="button">Edit</button>
<button type="button">Delete</button>
</div>
答:
0赞
Oscar R
11/18/2023
#1
我终于在这个解决方案中实现了我在帖子中概述的一切。我在每行的开头/结尾 + 菜单的开头/结尾使用“焦点管理器”来手动传递焦点。
唯一需要注意的是,它要求您手动将数据属性添加到:
- 表格前后的第一个可聚焦元素
- 所有行中的第一个和最后一个可聚焦元素
- 菜单中的第一个和最后一个可聚焦元素
除此之外,我唯一担心的是使用不可见按钮可能是 SEO/可访问性问题(我不知道)
PS:此解决方案还假设您不需要行本身是可聚焦的。
class Table {
constructor(elm_table, elm_menu, elm_table_prev_focusable, elm_table_next_focusable) {
if (!elm_table || !elm_menu || !elm_table_prev_focusable || !elm_table_next_focusable) return;
this.elm_table = elm_table;
this.elm_menu = elm_menu;
this.elm_table_prev_focusable = elm_table_prev_focusable;
this.elm_table_next_focusable = elm_table_next_focusable;
this.table_option_buttons = Array.from(this.elm_table.querySelectorAll(".btn_option"));
this.row_prev_focus_manager = Object.assign(document.createElement('button'), { type: 'button', id: 'row_prev_focus_manager', className: 'focus_manager' });
this.row_next_focus_manager = Object.assign(document.createElement('button'), { type: 'button', id: 'row_next_focus_manager', className: 'focus_manager' });
this.menu_prev_focus_manager = this.elm_menu.querySelector("#menu_prev_focus_manager");
this.menu_next_focus_manager = this.elm_menu.querySelector("#menu_next_focus_manager");
this.menu_first_focusable_elm = this.elm_menu.querySelector("[data-menu-first-focusable]");
this.menu_last_focusable_elm = this.elm_menu.querySelector("[data-menu-last-focusable]");
this.elm_table.addEventListener("focusin", (e) => {
const parent_row = this.get_parent_row(e.target);
if (parent_row) {
this.set_row_as_active(parent_row);
}
});
this.elm_table.addEventListener("focusout", (e) => {
const old_parent_row = this.get_parent_row(e.target);
const cur_parent_row = this.get_parent_row(e.relatedTarget);
if (old_parent_row && old_parent_row !== cur_parent_row) {
this.set_row_as_inactive(old_parent_row);
}
});
this.row_prev_focus_manager.addEventListener("focus", (e) => {
const target_row = this.get_parent_row(e.target);
const prev_row = target_row.previousElementSibling;
if (this.elm_menu.is_open && this.elm_menu.target_row === prev_row) {
this.menu_last_focusable_elm.focus();
} else {
if (prev_row) {
this.get_row_last_focusable(prev_row).focus();
} else {
this.elm_table_prev_focusable.focus();
}
}
});
this.row_next_focus_manager.addEventListener("focus", (e) => {
const target_row = this.get_parent_row(e.target);
if (this.elm_menu.is_open && this.elm_menu.target_row === target_row) {
this.menu_first_focusable_elm.focus();
} else {
const next_row = target_row.nextElementSibling;
if (next_row) {
this.get_row_first_focusable(next_row).focus();
} else {
this.elm_table_next_focusable.focus();
}
}
});
this.menu_prev_focus_manager.addEventListener("focus", (e) => {
this.get_row_last_focusable(this.elm_menu.target_row).focus();
});
this.menu_next_focus_manager.addEventListener("focus", (e) => {
if (e.relatedTarget === this.elm_table_next_focusable) {
this.menu_last_focusable_elm.focus();
return;
}
const next_row = this.elm_menu.target_row.nextElementSibling;
if (next_row) {
this.get_row_first_focusable(next_row).focus();
} else {
this.elm_table_next_focusable.focus();
}
});
this.elm_table.addEventListener("click", (e) => {
if (this.table_option_buttons.includes(e.target)) {
e.stopPropagation();
this.elm_menu.style.top = e.target.getBoundingClientRect().top + "px";
this.elm_menu.style.display = "flex";
this.elm_menu.is_open = true;
this.elm_menu.target_row = this.get_parent_row(e.target);
document.documentElement.addEventListener("click", (e) => {
if (e.target !== this.elm_menu && !this.elm_menu.contains(e.target)) {
this.elm_menu.style.display = "none";
this.elm_menu.is_open = false;
this.elm_menu.target_row = null;
}
});
}
});
}
get_parent_row(elm) {
return !elm ? null : elm.closest(".row");
}
get_row_first_focusable(row) {
return row.querySelector("[data-row-first-focusable]");
}
get_row_last_focusable(row) {
return row.querySelector("[data-row-last-focusable]");
}
set_row_as_active(row) {
if (!row.is_active) {
row.is_active = true;
row.insertBefore(this.row_prev_focus_manager, row.firstChild);
row.appendChild(this.row_next_focus_manager);
}
}
set_row_as_inactive(row) {
row.is_active = false;
if (row.contains(this.row_prev_focus_manager) && row.contains(this.row_next_focus_manager)) {
row.removeChild(this.row_prev_focus_manager);
row.removeChild(this.row_next_focus_manager);
}
}
}
const my_table = new Table(
document.getElementById("table"),
document.getElementById("menu"),
document.querySelector("[data-table-prev-focusable]"),
document.querySelector("[data-table-next-focusable]"),
);
.table {
overflow: hidden;
}
.table > .row:first-child {
border-top: 0.0625rem solid #ccc;
}
.table > .row {
display: flex;
gap: 2rem;
padding: 0.5rem 0;
border-bottom: 0.0625rem solid #ccc;
}
.menu {
display: none;
position: absolute;
left: 11rem;
padding: 0.5rem;
background: #ccc;
flex-direction: column;
gap: 0.5rem;
}
.menu::before {
content: '';
position: absolute;
top: 0;
left: -0.75rem;
width: 0;
height: 0;
border-top: 0.75rem solid transparent;
border-bottom: 0.75rem solid transparent;
border-right: 0.75rem solid #ccc;
}
.focus_manager {
position: absolute;
opacity: 0;
}
button:focus {
background: skyblue;
}
<button type="button" data-table-prev-focusable>Prev Focusable Element</button>
<br><br><br>
<div class="table" id="table">
<div class="row">
<span>Item 1</span>
<button data-row-first-focusable data-row-last-focusable class="btn_option" type="button">Options</button>
</div>
<div class="row">
<span>Item 2</span>
<button data-row-first-focusable data-row-last-focusable class="btn_option" type="button">Options</button>
</div>
<div class="row">
<span>Item 3</span>
<button data-row-first-focusable data-row-last-focusable class="btn_option" type="button">Options</button>
</div>
<div class="row">
<span>Item 4</span>
<button data-row-first-focusable data-row-last-focusable class="btn_option" type="button">Options</button>
</div>
<div class="row">
<span>Item 5</span>
<button data-row-first-focusable data-row-last-focusable class="btn_option" type="button">Options</button>
</div>
</div>
<div class="menu" id="menu">
<button type="button" id="menu_prev_focus_manager" class="focus_manager"></button>
<button type="button" data-menu-first-focusable>View</button>
<button type="button">Edit</button>
<button type="button" data-menu-last-focusable>Delete</button>
<button type="button" id="menu_next_focus_manager" class="focus_manager"></button>
</div>
<br><br><br>
<button type="button" data-table-next-focusable>Next Focusable Element</button>
评论
.focus()
tabindex
overflow: hidden;
position: relative;