添加焦点以弹出/单击时模式以点击选项卡/辅助功能 - JavaScript



我有一个弹出窗口/叠加层,在"单击"元素时出现。由于弹出窗口后面有大量 HTML 内容,因此弹出窗口中的按钮/输入元素自然没有焦点/选项卡索引行为。出于可访问性的原因,我希望当这个弹出窗口显示模态内的元素具有焦点/选项卡索引优先级而不是其背后的主要内容时。

在下面的简单演示中 - 单击"单击我"按钮后,当您使用 Tab 键时,浏览器仍会通过叠加层后面的输入元素进行 Tab 键。

任何关于如何在显示时为覆盖提供选项卡行为的建议将不胜感激。

在模态上创建focus事件似乎不起作用?

代码笔:https://codepen.io/anna_paul/pen/eYywZBz

编辑

我几乎可以让乔治·查普曼的 Codepen 答案工作,但是当您按住回车键时,它会在覆盖层出现和未出现之间来回闪烁,而且它似乎在 Safari 中不起作用?

let clickMe = document.querySelector('#click-me'),
modal = document.querySelector('.modal'),
closeButton = document.querySelector('.close')
clickMe.addEventListener('click', () => {
modal.style.display = 'flex';
// modal.focus();
})
closeButton.addEventListener('click', () => {
modal.style.display = 'none';
})
body {
margin: 0;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
input, button {
margin: 1rem;
padding: .5rem;
}
.click-me {
display: block;
}
.modal {
display: none;
flex-direction: column;
width: 100%;
height: 100%;   
justify-content: center;
align-items: center;
background: grey;
position: absolute;
}
form {
display: flex;
}
<button id="click-me">Click Me</button>
<form action="">
<input type="text" placeholder="An Input">
<input type="text" placeholder="An Input">
<input type="text" placeholder="An Input">
<input type="text" placeholder="An Input">
</form>
<div class="modal">
<button class="close">Close x</button>
<button>More Buttons</button>
<button>More Buttons</button>
</div>

在使模式对话框可访问时,需要考虑一些事项,而不仅仅是将焦点设置为模式或在模式中恢复 Tab 键顺序。您还必须考虑,如果屏幕阅读器未使用aria-hidden="true"对屏幕阅读器隐藏基础页面元素,则屏幕阅读器仍然可以感知这些元素,然后您还需要在模式关闭并还原基础页面时取消隐藏这些元素。

所以,总结一下,你需要做的是:

  1. 将焦点设置为模式中出现的第一个可聚焦元素。
  2. 确保基础页面元素对屏幕阅读器隐藏。
  3. 确保在模态内限制 Tab 键顺序。
  4. 确保实现预期的键盘行为,例如,按 Esc 将关闭或关闭模式对话框。
  5. 确保在模式关闭时还原基础页面元素。
  6. 确保在打开模式对话框之前具有焦点的元素已恢复焦点。

您还需要确保模式对话框具有 ARIArole="dialog"属性,以便屏幕阅读器将宣布焦点已移动到对话框,理想情况下,您应该使用aria-labelledby和/或aria-describedby属性为模式提供可访问的名称和/或说明。

这是一个相当多的列表,但通常建议用于可访问的模式对话框。请参阅 WAI-ARIA 模式对话框示例。

我已经为您的模态编写了一个解决方案,部分基于 Hidde de Vries 的原始代码,用于限制模态对话框中的 Tab 键顺序。

trapFocusInModal函数创建所有可聚焦元素的节点列表,并为Tab键和Shift+Tab键添加密钥侦听器,以确保焦点不会超出模态中的可聚焦元素。密钥侦听器还绑定到Esc键以关闭模式。

openModal函数显示模式对话框,隐藏基础页面元素,在打开模态之前上次保持焦点的元素上放置类名,并将焦点设置为模态中的第一个可聚焦元素。

closeModal函数关闭模式,取消隐藏基础页面,并还原在打开模式之前上次保持焦点的元素的焦点。

domIsReady函数等待 DOM 准备就绪,然后将Enter键和鼠标单击事件绑定到openModalcloseModal函数。

代码笔:https://codepen.io/gnchapman/pen/JjMQyoP

const KEYCODE_TAB = 9;
const KEYCODE_ESCAPE = 27;
const KEYCODE_ENTER = 13;
// Function to open modal if closed
openModal = function (el) {
// Find the modal, check that it's currently hidden
var modal = document.getElementById("modal");
if (modal.style.display === "") {

// Place class on element that triggered event
// so we know where to restore focus when the modal is closed
el.classList.add("last-focus");
// Hide the background page with ARIA
var all = document.querySelectorAll("button#click-me,input");
for (var i = 0; i < all.length; i++) {
all[i].setAttribute("aria-hidden", "true");
}

// Add the classes and attributes to make the modal visible
modal.style.display = "flex";
modal.setAttribute("aria-modal", "true");
modal.querySelector("button").focus();
}
};
// Function to close modal if open
closeModal = function () {
// Find the modal, check that it's not hidden
var modal = document.getElementById("modal");
if (modal.style.display === "flex") {
modal.style.display = "";
modal.setAttribute("aria-modal", "false")
// Restore the background page by removing ARIA
var all = document.querySelectorAll("button#click-me,input");
for (var i = 0; i < all.length; i++) {
all[i].removeAttribute("aria-hidden");
}

// Restore focus to the last element that had it
if (document.querySelector(".last-focus")) {
var target = document.querySelector(".last-focus");
target.classList.remove("last-focus");
target.focus();
}
}
};
// Function to trap focus inside the modal dialog
// Credit to Hidde de Vries for providing the original code on his website:
// https://hidde.blog/using-javascript-to-trap-focus-in-an-element/
trapFocusInModal = function (el) {
// Gather all focusable elements in a list
var query = "a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type='email']:not([disabled]), input[type='text']:not([disabled]), input[type='radio']:not([disabled]), input[type='checkbox']:not([disabled]), select:not([disabled]), [tabindex='0']"
var focusableEls = el.querySelectorAll(query);
var firstFocusableEl = focusableEls[0];
var lastFocusableEl = focusableEls[focusableEls.length - 1];
// Add the key listener to the modal container to listen for Tab, Enter and Escape
el.addEventListener('keydown', function(e) {
var isTabPressed = (e.key === "Tab" || e.keyCode === KEYCODE_TAB);
var isEscPressed = (e.key === "Escape" || e.keyCode === KEYCODE_ESCAPE);

// Define behaviour for Tab or Shift+Tab
if (isTabPressed) {
// Shift+Tab
if (e.shiftKey) {
if (document.activeElement === firstFocusableEl) {
lastFocusableEl.focus();
e.preventDefault();
}
}

// Tab
else {
if (document.activeElement === lastFocusableEl) {
firstFocusableEl.focus();
e.preventDefault();
}
}
}

// Define behaviour for Escape
if (isEscPressed) {
el.querySelector("button.close").click();
}
});
};
// Cross-browser 'DOM is ready' function
// https://www.competa.com/blog/cross-browser-document-ready-with-vanilla-javascript/
var domIsReady = (function(domIsReady) {
var isBrowserIeOrNot = function() {
return (!document.attachEvent || typeof document.attachEvent === "undefined" ? 'not-ie' : 'ie');
}
domIsReady = function(callback) {
if(callback && typeof callback === 'function'){
if(isBrowserIeOrNot() !== 'ie') {
document.addEventListener("DOMContentLoaded", function() {
return callback();
});
} else {
document.attachEvent("onreadystatechange", function() {
if(document.readyState === "complete") {
return callback();
}
});
}
} else {
console.error('The callback is not a function!');
}
}
return domIsReady;
})(domIsReady || {});

(function(document, window, domIsReady, undefined) {
// Check if DOM is ready
domIsReady(function() {
// Write something to the console
console.log("DOM ready...");

// Attach event listener on button elements to open modal
if (document.getElementById("click-me")) {

// Add click listener
document.getElementById("click-me").addEventListener("click", function(event) {
// If the clicked element doesn't have the right selector, bail
if (!event.target.matches('#click-me')) return;
event.preventDefault();
// Run the openModal() function
openModal(event.target);
}, false);
// Add key listener
document.getElementById("click-me").addEventListener('keydown', function(event) {
if (event.code === "Enter" || event.keyCode === KEYCODE_ENTER) {
// If the clicked element doesn't have the right selector, bail
if (!event.target.matches('#click-me')) return;
event.preventDefault();
// Run the openModal() function
openModal(event.target);
}
});
}
// Attach event listener on button elements to close modal
if (document.querySelector("button.close")) {

// Add click listener
document.querySelector("button.close").addEventListener("click", function(event) {
// If the clicked element doesn't have the right selector, bail
if (!event.target.matches('button.close')) return;
event.preventDefault();
// Run the closeModal() function
closeModal(event.target);
}, false);
// Add key listener
document.querySelector("button.close").addEventListener('keydown', function(event) {
if (event.code === "Enter" || event.keyCode === KEYCODE_ENTER) {
// If the clicked element doesn't have the right selector, bail
if (!event.target.matches('button.close')) return;
event.preventDefault();
// Run the closeModal() function
closeModal(event.target);
}
});
}
// Trap tab order within modal
if (document.getElementById("modal")) {
var modal = document.getElementById("modal");
trapFocusInModal(modal);
}

});
})(document, window, domIsReady);
<button id="click-me">Click Me</button>
<form action="">
<input placeholder="An Input" type="text"> <input placeholder="An Input" type="text"> <input placeholder="An Input" type="text"> <input placeholder="An Input" type="text">
</form>
<div class="modal" id="modal" role="dialog">
<button class="close">Close x</button> <button>More Buttons</button> <button>More Buttons</button>
</div>
body {
margin: 0;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
input, button {
margin: 1rem;
padding: .5rem;
}
.click-me {
display: block;
}
.modal {
display: none;
flex-direction: column;
width: 100%;
height: 100%;   
justify-content: center;
align-items: center;
background: grey;
position: absolute;
}
form {
display: flex;
}

您必须在出现后立即将焦点添加到弹出窗口,当您同时执行此操作时closeButton.focus()它不起作用这就是我使用setTimeout(() => closeButton.focus(), 1)的原因,这将在1毫秒后添加它焦点。

起初,焦点在按钮上是不可见的,当按下箭头键时它会变得可见,所以我让它可见样式:

.close:focus {
border: 2px solid black;
border-radius: 5px;
}

整个代码:

let clickMe = document.querySelector("#click-me"),
modal = document.querySelector(".modal"),
closeButton = document.querySelector(".close");
clickMe.addEventListener("click", () => {
setTimeout(() => closeButton.focus(), 1);
modal.style.display = "flex";
});
closeButton.addEventListener("click", () => {
modal.style.display = "none";
});
body {
margin: 0;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
input,
button {
margin: 1rem;
padding: 0.5rem;
}
.click-me {
display: block;
}
.modal {
display: none;
flex-direction: column;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
background: gray;
position: absolute;
}
form {
display: flex;
}
.close:focus {
border: 2px solid black;
border-radius: 5px;
}
<button id="click-me">Click Me</button>
<form action="">
<input type="text" placeholder="An Input" />
<input type="text" placeholder="An Input" />
<input type="text" placeholder="An Input" />
<input type="text" placeholder="An Input" />
</form>
<div class="modal">
<button class="close">Close x</button>
<button>More Buttons</button>
<button>More Buttons</button>
</div>

更新:焦点仅在模态内跳转:

let clickMe = document.querySelector("#click-me"),
modal = document.querySelector(".modal"),
closeButton = document.querySelector(".close");
lastButton = document.querySelector(".lastButton");
clickMe.addEventListener("click", () => {
setTimeout(() => closeButton.focus(), 1);
modal.style.display = "flex";
});
closeButton.addEventListener("click", () => {
modal.style.display = "none";
});
modal.addEventListener("keydown", function (event) {
var code = event.keyCode || event.which;
if (code === 9) {
if (lastButton == document.activeElement) {
event.preventDefault();
closeButton.focus();
}
}
});
body {
margin: 0;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
input,
button {
margin: 1rem;
padding: 0.5rem;
}
.click-me {
display: block;
}
.modal {
display: none;
flex-direction: column;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
background: gray;
position: absolute;
}
form {
display: flex;
}
.close:focus {
border: 2px solid black;
border-radius: 5px;
}
<button id="click-me">Click Me</button>
<form action="">
<input type="text" placeholder="An Input" />
<input type="text" placeholder="An Input" />
<input type="text" placeholder="An Input" />
<input type="text" placeholder="An Input" />
</form>
<div class="modal">
<button class="close">Close x</button>
<button>More Buttons</button>
<button class="lastButton">More Buttons</button>
</div>

我尝试最简单的解决方案呈现给您。

所以我的解决方案是这样的:

1.在模态中找到所有可聚焦的元素。

let modelElement = document.getElementById("modal");
let focusableElements = modelElement.querySelectorAll('a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]');

2.听网页上的焦点变化。

3.在焦点侦听器方法中,检查如果模态为打开且焦点元素列表中不存在焦点元素,则可聚焦元素列表的第一个元素必须是焦点。

document.addEventListener('focus', (event) => { 
if(modal.style.display == 'flex' && !Array.from(focusableElements).includes(event.target))
Array.from(focusableElements)[0].focus();
}, true);

最终代码:

let clickMe = document.querySelector('#click-me'),
modal = document.querySelector('.modal'),
closeButton = document.querySelector('.close')
console.log(clickMe)

clickMe.addEventListener('click', () =>{
modal.style.display = 'flex';
// modal.focus();
})
closeButton.addEventListener('click', () =>{
modal.style.display = 'none';
})
let modelElement = document.getElementById("modal");
let focusableElements = modelElement.querySelectorAll('a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]');
document.addEventListener('focus', (event) => { 
if(modal.style.display == 'flex' && !Array.from(focusableElements).includes(event.target))
Array.from(focusableElements)[0].focus();
}, true);
body {
margin: 0;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
input, button {
margin: 1rem;
padding: .5rem;
}
.click-me {
display: block;
}
.modal {
display: none;
flex-direction: column;
width: 100%;
height: 100%;   
justify-content: center;
align-items: center;
background: grey;
position: absolute;
}
form {
display: flex;
}
<button id="click-me">Click Me</button>
<form action="">
<input type="text" placeholder="An Input">
<input type="text" placeholder="An Input">
<input type="text" placeholder="An Input">
<input type="text" placeholder="An Input">
</form>
<div id="modal" class="modal">
<button class="close">Close x</button>
<button>More Buttons</button>
<button>More Buttons</button>
</div>

将焦点移到模态中

要将焦点放入模态中,您必须将焦点放在模态内的可聚焦元素上,这就是为什么这样做modal.focus();不会导致焦点像您希望的那样移动到模态中,因为模态本身不是一个可聚焦的元素。相反,您可能希望执行诸如$(modal).find("button").first().focus();之类的操作。

User2495207向您展示了另一种方法,但setTimeout容易出现错误且不必要。理想情况下,我们也不想规定它应该专注于特定按钮,就像在 Tab 键顺序中找到的第一个按钮一样。

然而,这只能解决最初将焦点移动到模态中的问题。它不会将焦点困在模态中,因此当您按 Tab 键跳过最后一个按钮时,它会将焦点移动到模态后面的元素。

模态中的陷印焦点

这里的想法是,您要检查下一个可聚焦元素是否在模态内,如果不是,则意味着您在模态中的最后一个元素上,并且需要将焦点包装到模态中的第一个元素。您还应该反转此逻辑,如果第一个按钮聚焦并且有人按下shift+tab它将包装到模态中的最后一个元素,但我将演示第一种情况:

let clickMe = document.querySelector('#click-me'),
modal = document.querySelector('.modal'),
closeButton = document.querySelector('.close')
clickMe.addEventListener('click', () =>{
modal.style.display = 'flex';
$(modal).find("button").first().focus();
trapFocus(modal);
});
function trapFocus(modal) {
$(modal).find("button").last().on('blur', (e) => {
// found something outside the modal
if (!$(modal).find($(e.relatedTarget)).length > 0) {
e.preventDefault();
$(modal).find("button").first().focus();
}
});
}
closeButton.addEventListener('click', () =>{
modal.style.display = 'none';
});

RelatedTarget是一个很棒的工具,可让您拦截focus事件以确定焦点的去向。因此,在上面的代码中,我们正在检查即将聚焦的元素(又名relatedTarget,是否在模态内,如果不是,那么我们将焦点强制聚焦到我们希望它去的地方。

关于辅助功能的最后一点说明

您还希望确保在Escapekeydown时使模态关闭。在这一点上,e.keyCode已被弃用,我们都应该使用e.key.

如果您需要支持IE,首先,我很抱歉。其次,它需要e.keyCode才能正常运行,因此需要与您的e.key检查结合使用,例如e.key === "Escape" && e.keyCode === "27".但是,我确实建议,也许只是创建一个接受事件作为参数的函数,并将这些检查保留在该函数中,以便当IE最终支持e.key时,您可以在一个位置清理所有代码。

最新更新