Prevent Focus From Entering an Element
Trapping focus inside of a modal is a pretty common accessibility consideration, and there are many reusable modules that make it much easier to implement in almost any framework. I've used focus-trap-react a few times, and I found it to be very helpful with adding that functionality to React components.
Preventing focus from entering an element is also something to think about if you are hiding content visually, or have elements that expand depending on the user flow.
Problems We're Preventing
There's a "keep reading" section on a popular crowdfunding website, and despite the article being collapsed until hitting the "Continue reading" button, the content is still accessible via keyboard and the content visually scrolls as you move through the collapsed sections.
If we're implementing an expandable section to make it easier for users to scan the content on the page, we probably want keyboard and screen reader users to have a similar experience.
After activating the button to expand the content, we would also want to set the focus above the content so a user does not have to move back through all the previously collapsed elements to get to where they perceived themselves to be.
Simple "Lockout" React Component
To prevent a user from reaching focusable elements inside our component, we'll need to grab those focusable elements and set their tabindex
to "-1"
. After the content has been expanded, or we want to allow the user to access the focusable elements again, we'll set the tabindex back to "0"
.
import React, { useRef, useEffect } from 'react';
/*
Selector to grab focusable elements.
@see https://stackoverflow.com/questions/7668525/is-there-a-jquery-selector-to-get-all-elements-that-can-get-focus
*/
let FOCUSABLE_ELEMENTS_SELECTOR =
'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]';
function Lockout({ isLocked, children }) {
let containerRef = useRef(null);
// Listen for prop change to update tab indexes.
useEffect(() => {
if (containerRef.current) {
let focusableElements = containerRef.current.querySelectorAll(
FOCUSABLE_ELEMENTS_SELECTOR
);
focusableElements.forEach((el) => {
el.tabIndex = isLocked ? '-1' : '0';
});
}
}, [isLocked]);
return <div ref={containerRef}>{children}</div>;
}
The selector used in the Lockout
component works well, but we would need to update it if any new focusable elements were added to the HTML standard spec. We could grab all child elements, but the overhead is probably not worth it unless we really need it for a special use case.