Created
January 31, 2023 11:32
-
-
Save lebbe/b4e12d3294f6e1006cdabb430419d524 to your computer and use it in GitHub Desktop.
Find previous focusable sibling
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* This is a handy tool when it comes to accessibility-proofing widgets on the web. | |
* I use this whenever some element with focus is removed from the screen, and I | |
* elsewise don't know which element should become focused. | |
* | |
* When we remove elements on the screen with focus, it is important that we give | |
* another element focus instead, so that screen readers can inform the user that | |
* something has happened, and so that keyboard navigation functions optimally. | |
* | |
* For more about operating a web-page with the keyboard and focus order, see | |
* WCAG 2.1: https://www.w3.org/TR/WCAG21/#focus-order | |
* | |
* A use-case for this, is for instance if the user chooses to remove a widget on | |
* the screen, then perhaps the previous widget (or an element within it) should | |
* be focused instead. | |
* | |
* Another use-case would be a form, where the user can insert and remove form | |
* fields. When a form field is removed, perhaps the previous field should become | |
* focused? Some might want the _next_ field to be focused instead, in that case, | |
* it should be fairly straightforward to alter this implementation. | |
* | |
*/ | |
/** | |
* This find the previous focusable sibling/element in the DOM, relative to the | |
* given one (E), with the following algorithm: | |
* | |
* For an element E: | |
* | |
* 0. If E is null or undefined, return; | |
* 1. Get the previous sibling element (`E.previousElementSibling`), we call this `PES`. | |
* 1b. If PES is not found, set the elements parent (`E.parentElement`) as `E`, and go to step 0. | |
* 2. Collext `PES`'s non-disabled focusable descendants, we call this `focusableChildren` | |
* 2b. If length of `focusableChildren` is 0, set PES to E and go to step 0. | |
* 3 (_optional_): If one of the focusableChildren is an input, return the last input element in that list. | |
* 4. Return this last element in the list focusableChildren. | |
* | |
* Here implemented as a recursive function: | |
*/ | |
function findPreviousFocusableElement(E, preferInputElement) { | |
// Step 0 | |
if (!E) return; | |
// Step 1 | |
const PES = E.previousElementSibling; | |
// Step 1b | |
if (PES === null) { | |
return findPreviousFocusableElement(E.parentElement); | |
} | |
// Step 2 | |
const focusableChildren = Array.from( | |
PES.querySelectorAll('input, button, [tabindex="0"]') | |
).filter((n) => !n.disabled); | |
// Step 2b | |
if (focusableChildren.length === 0) { | |
return findPreviousFocusableElement(PES); | |
} | |
// Step 3 (optional) | |
if (preferInputElement) { | |
const inputs = focusableChildren.filter((n) => n.nodeName === 'INPUT'); | |
if (inputs.length > 0) { | |
return inputs[inputs.length - 1]; | |
} | |
} | |
// Step 4 | |
return focusableChildren[focusableChildren.length - 1]; | |
} | |
/** | |
* This is how you could use the above function. | |
* The argument given here, would be the element about to be removed from the DOM tree. | |
*/ | |
function givePreviousElementFocus(element) { | |
if (!element) return; // Always start with paranoia check | |
const last = findPreviousFocusableElement(e, true); | |
if (last) { | |
// Remember to also invoke API functions in your current CSS framework/ | |
// design system that is needed to visually show that this element has focus! | |
// One way to make this work in many systems, would be to call `last.click()` | |
// instead, but that could also have some unforeseen consequences. | |
last.focus(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment