简体   繁体   中英

How to prevent screen reader focus (not referring to keyboard focus) from leaving predefined area (e.g., modal)

I've been trying to figure out how to contain the screen reader focus within a certain area. When I say screen reader focus, I don't mean the default browser focus that one can move with tabbing/shift-tabbing. I predominantly implement accessibility while using Voiceover on Mac, and when you turn that on, a new focus box appears on the page and reads out the information that it is 'highlighting'.

At that point if you were to tab, both the browser and the screenreader focus move concurrently. Aside from tabbing to different focusable elements, you can also hold cmd + opt and keypress left and right to move the screen reader focus from element to element, regardless if one can tab to it. That's the focus that I'm trying to contain.

I've tried preventing cmd, opt, and arrow key key presses when the focus is on the last element that I want focusable, but the browser doesn't seem to recognize the screen reader focus. And I believe that the keyboard disabling wouldn't work with the screen reader anyways, as it seems to work independently of the browser.

I've also tried dynamically adding tabindex: -1 and aria-hidden: true to all other elements on the page when a modal appears. This works when you turn on Voiceover after the fact; the screen reader focus does in fact get trapped. However if the screen reader is on first, which likely will be the case in most user instances, the screen reader doesn't respect the dynamic change. It's like the screen reader takes a 'snapshot' of the accessibility state as the page loads, and it doesn't respect new changes to the DOM.

Anyone have any ideas?

You can't prevent key shortcuts of the screen reader from being used. They have priority over everything else. They aren't even caught by a keydown/up/press handler within your script. Fortunately for us as screen reader users, this isn't an acceptable way to go.

As you also have observed, the browse cursor is effectively completely independant from the system focus. The accessibility tree determines what is reachable when using the screen reader's browse cursor.

To temporarily restrict the elements seen by the browse cursor, you must use the aria-modal attribute. Put it on the root element that should be reachable. Everything inside will stay reachable. Everything else outside will no longer be reachable as long as the attribute stays on the element.

Don't play with aria-hidden to produce the same effect. Some screen readers have issues with nested elements having an aria-hidden attribute. For example, if an outer element has aria-hidden=true and an inner element has aria-hidden=false, Jaws won't show the inner element.

Restricting the browse cursor with aria-modal, as well as hidding elements with aria-hidden by the way, doesn't automatically imply that they can't be focused with the regular system focus (Tab/Shift+Tab). You will therefore usually double the aria-modal restriction with a focus trap to prevent the system focus from going to a place where it isn't expected. If you don't do it, you may create troubles for screen reader users (what should the screen reader do if the focus is currently on an element hidden from the accessibility tree?). This is a recurrent oversight.

The safest to make a focus trap is to catch tab on the last allowed element and shift+tab on the first, and resp. bring the focus back to the first or last allowed element. It's much easier than setting all focusable elements to tabindex=-1 and then back to tabindex=0, and as far as I have tested, it works almost everywhere.

Here is a method I use to manage focus for modals which solves the issue you are having.

The item variable is the referring button that was pressed before opening the modal (so we can return focus there after closing the modal).

The className variable is the class name of the modal so you can target different modals.

kluio.helpers is just an array of functions I use across the site so can be omitted.

kluio.globalVars is an array of global variables so could be substituted for returning the results from the function.

I have added comments to each part to explain what it does.

The setFocus function is called when the modal is opened passing in the element that was pressed to activate it and the modal's className (works for our use case better, you should use an ID instead).

var kluio = {};
kluio.helpers = {};
kluio.globalVars = {};

kluio.helpers.setFocus = function (item, className) {

    className = className || "content"; //defaults to class 'content' in case of error (content being the <main> element.
    kluio.globalVars.beforeOpen = item; //we store the button that was pressed before the modal opened in a global variable so we can return focus to it on modal close.

    var focusableItems = ['a[href]', 'area[href]', 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', 'button:not([disabled])', '[tabindex="0"]']; //a list of items that should be focusable.
    var findItems = [];
    for (i = 0, len = focusableItems.length; i < len; i++) {
        findItems.push('.' + className + " " + focusableItems[i]); //add every focusable item to an array.
    }

    var findString = findItems.join(", ");
    kluio.globalVars.canFocus = Array.prototype.slice.call($('body').find(findString)); //please note we use a custom replacement for jQuery, pretty sure .find() behaves identically but just check it yourself.
    if (kluio.globalVars.canFocus.length > 0) {
        setTimeout(function () { //set timeout not needed most of the time, we have a modal that is off-screen and slides in, setting focus too early results in the page jumping so we added a delay.
            kluio.globalVars.canFocus[0].focus(); //set the focus to the first focusable element within the modal
            kluio.globalVars.lastItem = kluio.globalVars.canFocus[kluio.globalVars.canFocus.length - 1]; //we also store the last focusable item within the modal so we can keep focus within the modal. 
        }, 600);
    }
}

We then intercept the keydown event with the following function to manage focus.

document.onkeydown = function (evt) {
    evt = evt || window.event;
    if (evt.keyCode == 27) {
        closeAllModals(); //a function that will close any open modal with the Escape key
    }
    if (kluio.globalVars.modalOpen && evt.keyCode == 9) { //global variable to check any modal is open and key is the tab key
        if (evt.shiftKey) { //also pressing shift key
            if (document.activeElement == kluio.globalVars.canFocus[0]) { //the current element is the same as the first focusable element
                evt.preventDefault();
                kluio.globalVars.lastItem.focus(); //we focus the last focusable element as we are reverse tabbing through the items.
            }
        } else {
            if (document.activeElement == kluio.globalVars.lastItem) { //when tabbing forward we look for the last tabbable element 
                evt.preventDefault();
                kluio.globalVars.canFocus[0].focus(); //move the focus to the first tabbable element.
            }
        }
    }
};

Finally in your version of the closeAllModals function you need to return focus to the referring element.

if (kluio.globalVars.beforeOpen) {
    kluio.globalVars.beforeOpen.focus();
}

It works well for our use case and although it could do with a tidy and we have a few strange practices (global variables....we have good reasons I promise.) it will hopefully be of use to you.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM