I'm developing an app with Angular
and Semantic-UI
. The app should be accessible, this means it should be compliant with WCAG 2.0.
To reach this purpose the modals should keep focus within the dialog and prevents users from going outside or move with "tabs" between elements of the page that lays under the modal.
I have found some working examples, like the following:
dialog
HTML 5.1 element: https://demo.agektmr.com/dialog
Here is my try to create an accessible modal with Semantic-UI: https://plnkr.co/edit/HjhkZg
As you can see I used the following attributes:
role="dialog"
aria-labelledby="modal-title"
aria-modal="true"
But they don't solve my issue. Do you know any way to make my modal keeping focus and lose it only when user click on cancel/confirm buttons?
The tabindex attribute specifies the tab order of an element (when the "tab" button is used for navigating).
There is currently no easy way to achieve this. The inert attribute was proposed to try to solve this problem by making any element with the attribute and all of it's children inaccessible. However, adoption has been slow and only recently did it land in Chrome Canary behind a flag.
Another proposed solution is making a native API that would keep track of the modal stack, essentially making everything not currently the top of the stack inert. I'm not sure the status of the proposal, but it doesn't look like it will be implemented any time soon.
Unfortunately without a good solution. One solution that is popular is to create a query selector of all known focusable elements and then trap focus to the modal by adding a keydown event to the last and first elements in the modal. However, with the rise of web components and shadow DOM, this solution can no longer find all focusable elements.
If you always control all the elements within the dialog (and you're not creating a generic dialog library), then probably the easiest way to go is to add an event listener for keydown on the first and last focusable elements, check if tab or shift tab was used, and then focus the first or last element to trap focus.
If you're creating a generic dialog library, the only thing I have found that works reasonably well is to either use the inert polyfill or make everything outside of the modal have a tabindex=-1
.
var nonModalNodes;
function openDialog() {
var modalNodes = Array.from( document.querySelectorAll('dialog *') );
// by only finding elements that do not have tabindex="-1" we ensure we don't
// corrupt the previous state of the element if a modal was already open
nonModalNodes = document.querySelectorAll('body *:not(dialog):not([tabindex="-1"])');
for (var i = 0; i < nonModalNodes.length; i++) {
var node = nonModalNodes[i];
if (!modalNodes.includes(node)) {
// save the previous tabindex state so we can restore it on close
node._prevTabindex = node.getAttribute('tabindex');
node.setAttribute('tabindex', -1);
// tabindex=-1 does not prevent the mouse from focusing the node (which
// would show a focus outline around the element). prevent this by disabling
// outline styles while the modal is open
// @see https://www.sitepoint.com/when-do-elements-take-the-focus/
node.style.outline = 'none';
}
}
}
function closeDialog() {
// close the modal and restore tabindex
if (this.type === 'modal') {
document.body.style.overflow = null;
// restore or remove tabindex from nodes
for (var i = 0; i < nonModalNodes.length; i++) {
var node = nonModalNodes[i];
if (node._prevTabindex) {
node.setAttribute('tabindex', node._prevTabindex);
node._prevTabindex = null;
}
else {
node.removeAttribute('tabindex');
}
node.style.outline = null;
}
}
}
The different "working examples" do not work as expected with a screenreader.
They do not trap the screenreader visual focus inside the modal.
For this to work, you have to :
aria-hidden
attribute on any other nodesdisable keyboard focusable elements inside those trees (links using tabindex=-1
, controls using disabled
, ...)
:focusable
pseudo selector can be useful to find focusable elements.add a transparent layer over the page to disable mouse selection.
pointer-events: none
property when the browser handles it with non SVG elements, not in IEThis focus-trap plugin is excellent at making sure that focus stays trapped inside of dialogue elements.
It sounds like your problem can be broken down into 2 categories:
Add a tabindex of -1 to the main container which is the DOM element that has role="dialog". Set the focus to the container.
I found no other way of doing this except by getting the tabbable elements within the dialog box and listening it on keydown. When I know the element in focus (document.activeElement) is the last one on the list, I make it wrap
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With