I have created the below JavaScript code for a popup that needs to be accessible.
The problem is that during keyboard-browsing the tab button selects all the buttons behind the popup on the landing page first. I need it to only select the buttons on the popup and the close button.
I understand that focus trapping the popup is a method that might work but I don't know how to add that to my code.
<< Here is the code
/ << 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 = ".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 = "/ ";
}
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 = "/ ";
}
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>
I have created the below JavaScript code for a popup that needs to be accessible.
The problem is that during keyboard-browsing the tab button selects all the buttons behind the popup on the landing page first. I need it to only select the buttons on the popup and the close button.
I understand that focus trapping the popup is a method that might work but I don't know how to add that to my code.
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>
Share
Improve this question
asked Feb 10, 2023 at 12:09
Arya PremArya Prem
111 silver badge3 bronze badges
1
-
Use
dialog
element and open dialog asshowModal()
– Anil kumar Commented Feb 10, 2023 at 12:21
3 Answers
Reset to default 5To render the question more plete, for a modal dialog, you need two things:
- Trap focus inside the modal dialog
- Hide all other contents from assistive technology
Of course there is more to it, but this is the part relevant to the question.
Navigation via Tab is only one means of navigation with screen readers. TalkBack and VoiceOver, the screen readers on Android and iOS, make no difference between real focus and reading focus. That is what the latter is about: Avoiding reading access to contents outside the dialog.
In a lot of solutions this is badly done, and one can move outside a modal dialog on mobile platforms, for example in Bootstrap 5.
Native, standards methods
The <dialog>
element was standardised to solve these issues.
Obviously, this is the best way to go, since it means you have all the momentum of browser vendors’ development in favour of your dialog.
According to Scott O'hara two weeks ago, you should Use it
If you don’t want to rely on this native element, and implement a custom modal dialog, there is two things the native one does, concerning your question.
<dialog>
elements invoked by theshowModal()
method will have an implicitaria-modal="true"
[…]
[…] everything other than the
<dialog>
and its contents should be rendered inert using theinert
attribute.
In other words this would guide us to
<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>
Unfortunately, also here, browser support is not great yet.
Alternative for aria-modal
That attribute is supposed to hide everything else (the <main>
in above example) from assistive technology. For browsers that don’t support this, aria-hidden
can be applied to the other contents:
<main aria-hidden="true">
…
</main>
<div role="dialog" …>
This has no effect on focus, but on reading through assistive technology.
Trap focus
There are two strategies.
Intercept focusin
This is the mothed used in Bootstrap’s Modal and the APG’s Modal Dialog Example.
One strategy is to bind a listener to the focusin
event of contents outside the dialog, and put focus back on the first or last interactive element in the dialog, depending on which one was last focused.
You cannot use the focusout
event because the Focus order of user triggered focus
will be fired after focusout
, and the latter is not cancelable.
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>
Make everything else not focusable
You can try to render all other interactive elements not focusable or clickable.
This is obviously more fragile, especially if the interactivity is not exposed through use of native HTML elements or ARIA roles, so it bees hard to identify these.
<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>
One cool idea is to attach hidden, tab-able elements before and after your dialog. Once those receive focus, pass it to the first or last element inside dialog:
[PREV HANDLE]
DIALOG -----
| |
| INPUT A |
| INPUT B |
| CHECKBOX |
| BUTTON |
------------
[NEXT HANDLE]
Pseudo code would be as follows:
<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);
Example snippet, basing on https://www.w3/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./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>
you could dynamically add With <div id="div1" tabindex="-1"> content you'd like to prevent tab</div>
with js logic