I am working on making an overlay modal more accessible. It works essentially like this JSFiddle. When you open the modal, the focus doesn't properly go into the modal, and it continues to focus on other (hidden, background) items in the page.
You can see in my JSFiddle demo that I have already used aria-controls
, aria-owns
, aria-haspopup
and even aria-flowto
.
<button
aria-controls="two"
aria-owns="true"
aria-haspopup="true"
aria-flowto="two"
onclick="toggleTwo();"
>
TOGGLE DIV #2
</button>
However, while using MacOS VoiceOver, none of these do what I intend (though VoiceOver does respect the aria-hidden
that I set on div two
).
I know that I could manipulate the tabindex
, however, values above 0 are bad for accessibility, so my only other option would be to manually find all focusable elements on the page and set them to tabindex=-1
, which is not feasible on this large, complicated site.
Additionally, I've looked into manually intercepting and controlling tab behavior with Javascript, so that the focus is moved into the popup and wraps back to the top upon exiting the bottom, however, this has interfered with accessibility as well.
Focus can be moved with the focus() method. I've updated the jsFiddle with the intended behavior. I tested this on JAWS on Windows and Chrome.
I've added a tabindex="-1"
on the "two" div to allow it to be focusable with the focus method.
I split the toggle function into two functions, this can probably be refactored to fit your needs, but one function sets the aria-hidden attribute to true and moves the focus on the newly opened modal, and the other function does the reverse.
I removed the excessive aria attributes, the first rule of aria is to only use it when necessary. This can cause unexpected behavior if you're just mashing in aria.
To keep focus within the modal, unfortunately one of the best options is to set all other active elements to tabindex="-1"
or aria-hidden="true"
. I've applied an alternative where an event listener is added to the last element in the modal upon tabbing. To be compliant, another listener must be added to the first element to move focus to the last element upon a shift+tab event.
Unfortunately, to my knowledge there isn't a cleaner answer than those above solutions to keeping focus within a modal.
Make the first and the last focusable element of your modal react on kwydown event, resp. on pressing tab and shift+tab. As far as I ahve tested, it works everywhere.
`Basic example:
function createFocusCycle (first, last) {
first.addEventListener('keydown', function(e){
if (e.keyCode===9 && e.shiftKey) {
last.focus();
e.preventDefault();
}});
last.addEventListener('keydown', function(e){
if (e.keyCode===9) {
first.focus();
e.preventDefault();
}});
}
Naturally, you need to know what is the first and the last focusable element of your modal. Normally it shouldn't be too complicated.
Otherwise if you don't know what are the first and last focusable elements of your modal, it's perhaps a sign that you are making a too complex UI.
aria-disabled
vs aria-hidden
First, note that aria-hidden
is not intended to be used when the element is visible on the screen:
Indicates that the element and all of its descendants are not visible or perceivable to any user
The option you should use is aria-disabled
Indicates that the element is perceivable but disabled, so it is not editable or otherwise operable.
- on using
tabindex
Removing a link from the tabindex is a WCAG failure if this link is still perceivable from a screenreader or clickable. It has to be used conjointly with aria-disabled
or better the disabled
attribute.
- Disabling mouse events using
pointer-events
css property
The easiest way to disable mouse events is by using the pointer-events
css property:
pointer-events: none;
- Disabling keyboard focus
The jQuery :focusable
selecter is the easiest thing you could use
$("#div1 :focusable").attr("tabindex", -1);
sample code
$("#div1 :focusable")
.addClass("unfocus")
.attr("tabindex", -1)
.attr("disabled", true);
$("button").on("click", function(){
$(".unfocus").attr("tabindex", 0)
.removeClass("unfocus")
.removeAttr("disabled");
});
.unfocus {
pointer-events: none;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://code.jquery.com/ui/1.12.0/jquery-ui.min.js"></script>
<div id="div1">
<a href="">non clickable link</a>
<div tabindex="0">
non focusable div
</div>
</div>
<div id="div2">
<button>click here to restore other links</button>
</div>
Use role = "dialog" aria-modal="true" on your modal popup