我为需要可访问的弹出窗口创建了以下 JavaScript 代码。
问题在于,在键盘浏览期间,选项卡按钮首先选择登陆页面上弹出窗口后面的所有按钮。我需要它只选择弹出窗口上的按钮和关闭按钮。
我知道焦点捕获弹出窗口是一种可能有效的方法,但我不知道如何将其添加到我的代码中。
https://codepen.io/aryanotstark/pen/KKBjLXY << Here is the code
https://digitalcloud.co.za/kiron/ << You can view the popup in action on this website
<script>
// Check if the popup has already been shown during the current session
if (!sessionStorage.getItem('popupShown')) {
setTimeout(function() {
var popup = document.createElement("div");
popup.style.position = "fixed";
popup.style.top = "0";
popup.style.left = "0";
popup.style.zIndex = "999";
popup.style.backgroundColor = "rgba(0, 0, 0, 0.5)";
popup.style.width = "100vw";
popup.style.height = "100vh";
popup.style.display = "flex";
popup.style.alignItems = "center";
popup.style.justifyContent = "center";
var innerPopup = document.createElement("div");
innerPopup.style.backgroundColor = "black";
innerPopup.style.display = "flex";
innerPopup.style.flexDirection = "column";
innerPopup.style.alignItems = "center";
innerPopup.style.justifyContent = "center";
innerPopup.style.padding = "40px";
innerPopup.style.margin = "35px";
innerPopup.style.borderRadius = "25px";
var img = document.createElement("img");
img.src = "https://digitalcloud.co.za/wp-content/uploads/2023/01/kerridge_pop_up_illustration.png ";
img.alt = "Find out about Kerridge’s core solution ";
img.style.width = "25%";
img.style.cursor = "pointer";
innerPopup.appendChild(img);
var text = document.createElement("p");
text.innerHTML = "Interested to find out more about our core solution?";
text.style.color = "white";
text.style.marginTop = "50px";
text.style.textAlign = "center";
text.style.fontSize = "20px";
innerPopup.appendChild(text);
var button = document.createElement("button");
button.innerHTML = "Book a free demo today";
button.style.backgroundColor = "#E8017B";
button.style.color = "white";
button.style.fontSize = "20px";
button.style.padding = "15px";
button.style.borderRadius = "50px"
button.style.border = "none"
button.onclick = function() {
location.href = "https://digitalcloud.co.za/ ";
}
innerPopup.appendChild(button);
var secondText = document.createElement("p");
secondText.innerHTML = "Stay up to date with the latest Kerridge product updates and existing announcements";
secondText.style.color = "white";
secondText.style.marginTop = "50px";
secondText.style.textAlign = "center";
secondText.style.fontSize = "20px";
innerPopup.appendChild(secondText);
var secondButton = document.createElement("button");
secondButton.innerHTML = "Sign up to our Newsletter";
secondButton.style.color = "#E8017B";
secondButton.style.background = "transparent";
secondButton.style.border = "none";
secondButton.style.fontSize = "20px";
secondButton.onclick = function() {
location.href = "https://digitalcloud.co.za/ ";
}
innerPopup.appendChild(secondButton);
var closeBtn = document.createElement("div");
closeBtn.style.position = "absolute";
closeBtn.style.top = "50px";
closeBtn.style.right = "50px";
closeBtn.style.cursor = "pointer";
closeBtn.style.width = "60px";
closeBtn.style.height = "60px";
closeBtn.style.borderRadius = "50%";
closeBtn.style.backgroundColor = "#E8017B";
closeBtn.style.display = "flex";
closeBtn.style.alignItems = "center";
closeBtn.style.justifyContent = "center";
closeBtn.setAttribute("tabindex", "0");
closeBtn.setAttribute("role", "button");
closeBtn.setAttribute("aria-label", "Close");
closeBtn.addEventListener("click", function() {
// Add logic to close something here
popup.remove();
});
closeBtn.addEventListener("keydown", function(event) {
if (event.key === "Enter" || event.key === " ") {
// Add logic to close something here
popup.remove();
}
});
var closeX = document.createElement("div");
closeX.innerHTML = "X";
closeX.style.color = "white";
closeX.style.fontWeight = "bold";
closeBtn.appendChild(closeX);
closeBtn.onclick = function() {
popup.remove();
}
innerPopup.appendChild(closeBtn);
popup.appendChild(innerPopup);
document.body.appendChild(popup);
// Set a value in sessionStorage indicating that the popup has been shown
sessionStorage.setItem('popupShown', 'true');
}, 90000 );
}
</script>
为了使问题更完整,对于 modal 对话框,您需要两件事:
当然还有更多内容,但这是与问题相关的部分。
通过 Tab 导航只是屏幕阅读器的一种导航方式。 Android 和 iOS 上的屏幕阅读器 TalkBack 和 VoiceOver 在实际焦点和阅读焦点之间没有区别。这就是后者的含义:避免读取对话框之外的内容。
在很多解决方案中,这做得很糟糕,并且可以移出移动平台上的模式对话框,例如在 Bootstrap 5 中。
<dialog>
元素来解决这些问题。
显然,这是最好的方法,因为这意味着您拥有浏览器供应商开发的所有动力来支持您的对话框。
如果您不想依赖此本机元素并实现自定义模式对话框,则本机元素会根据您的问题执行两件事。
由
<dialog>
方法调用的元素将具有隐式showModal()
[…]aria-modal="true"
[…] 除了
及其内容之外的所有内容都应使用<dialog>
属性呈现惰性。inert
换句话说,这将引导我们
<main inert>
…
<p><a href="#">Can you focus me?</a></p>
</main>
<div role="dialog" aria-modal="true" aria-labelledby="d-title">
<h2 id="d-title">Dialog title</h2>
…
<p><button>Can you focus me?</button></p>
</div>
不幸的是,浏览器支持还不是很好。
aria-modal
该属性应该对辅助技术隐藏所有其他内容(上例中的
<main>
)。对于不支持此功能的浏览器,aria-hidden
可以应用于其他内容:
<main aria-hidden="true">
…
</main>
<div role="dialog" …>
这不会影响注意力,但会影响通过辅助技术阅读。
有两种策略。
focusin
这是Bootstrap的模态和APG的模态对话框示例中使用的方法。
一种策略是将侦听器绑定到对话框外部内容的focusin
事件,并将焦点放回到对话框中的第一个或最后一个交互元素上,具体取决于最后一个焦点。 您不能使用
focusout
事件,因为用户触发的
focus
的焦点顺序将在
focusout
之后触发,并且后者不可取消。
document.getElementsByTagName('main')[0].addEventListener('focusin', () => document.querySelector('[role=dialog] button').focus());
<main>
…
<p><a href="#">Can you focus me?</a></p>
</main>
<div role="dialog" aria-modal="true" aria-labelledby="d-title">
<h2 id="d-title">Dialog title</h2>
…
<p><button autofocus>Can you focus me?</button></p>
</div>
使其他一切都无法聚焦
<main style="pointer-events: none">
…
<p><a href="#" tabindex="-1">Can you focus me?</a></p>
</main>
<div role="dialog" aria-modal="true" aria-labelledby="d-title">
<h2 id="d-title">Dialog title</h2>
…
<p><button autofocus>Can you focus me?</button></p>
</div>
<div id="div1" tabindex="-1"> content you'd like to prevent tab</div>
[PREV HANDLE]
DIALOG -----
| |
| INPUT A |
| INPUT B |
| CHECKBOX |
| BUTTON |
------------
[NEXT HANDLE]
伪代码如下:
<div tabindex="0" class="hidden" />
<div role="dialog" aria-modal="true">
<input />
<button />
</div>
<div tabindex="0" class="hidden" id="nextHandle" />
const trapFocus = (e) => {
const isDialogBlured = !dialogElement.contains(e.target);
if (isDialogBlured) {
document.activeElement === nextHandle
? focusFirstDescendant(dialogElement) // Tab clicked - focus first descendant
: focusLastDescendant(dialogElement); // Shift + Tab clicked - focus last descendant
}
}
document.addEventListener("focus", trapFocus, true);
focusFirstDescendant(dialogElement);
示例片段,基于 https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/dialog/
const wrapper = document.getElementById("wrapper");
const dialog = document.getElementById("dialog");
const triggerButton = document.getElementById("trigger");
const nextHandle = document.getElementById("nextHandle");
let preventFocusTrap = false;
const showDialog = () => {
document.addEventListener("focus", trapFocus, true);
wrapper.classList.remove("hidden");
focusFirstDescendant(dialog);
};
const closeDialog = () => {
document.removeEventListener("focus", trapFocus, true);
wrapper.classList.add("hidden");
triggerButton.focus();
}
const attemptFocus = (element) => {
if (!isFocusable(element)) {
return false;
}
preventFocusTrap = true; // We're about to manually focus element inside dialog. Tell the event handler not to take action
element.focus();
preventFocusTrap = false;
return document.activeElement === element;
};
const focusFirstDescendant = (element) => {
for (let i = 0; i < element.childNodes.length; i++) {
const child = element.childNodes[i];
if (attemptFocus(child) || focusFirstDescendant(child)) {
return true; // escape recursive function
}
}
};
const focusLastDescendant = (element) => {
for (let i = element.childNodes.length - 1; i >= 0; i--) {
const child = element.childNodes[i];
if (attemptFocus(child) || focusLastDescendant(child)) {
return true; // escape recursive function
}
}
};
const trapFocus = (e) => {
if (preventFocusTrap) {
return;
}
const isDialogBlured = !dialog.contains(e.target);
if (isDialogBlured) {
document.activeElement === nextHandle
? focusFirstDescendant(dialog) // Tab clicked - focus first descendant
: focusLastDescendant(dialog); // Shift + Tab clicked - focus last descendant
}
};
const isFocusable = (element) => {
if (element.tabIndex < 0) {
return false;
}
if (element.disabled) {
return false;
}
switch (element.nodeName) {
case "A":
return !!element.href && element.rel !== "ignore";
case "INPUT":
return element.type !== "hidden";
case "BUTTON":
case "SELECT":
case "TEXTAREA":
return true;
default:
return false;
}
};
.hidden {
display: none;
}
/* http://a11yproject.com/posts/how-to-hide-content/ */
.invisible {
position: absolute;
width: 0;
height: 0;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
/* Visual styles, insignificant */
#dialog {
border: 2px solid #14acca;
background-color: #eef8ff;
border-radius: 10px;
margin: 10px 0;
padding: 0 15px;
}
a, button, label {
display: block;
margin: 10px 0;
}
<button onclick="showDialog()" id="trigger">
Display dialog and trap focus
</button>
<div id="wrapper" class="hidden">
<div tabindex="0" class="invisible">Prev handle</div>
<div role="dialog" aria-modal="true" id="dialog">
<h3>Hi! I am a dialog. Hit Tab or Shift+Tab few times to test focus trap</h3>
<label>User: <input type="text" /></label>
<label>Password: <input type="text" /></label>
<label><input type="checkbox" /> Remember me</label>
<button onclick="closeDialog()">Close dialog and untrap focus</button>
</div>
<div tabindex="0" class="invisible" id="nextHandle">Next handle</div>
</div>
<a href="#">Link after dialog</a>
<button>Button after dialog</button>