Skip to content

Instantly share code, notes, and snippets.

@lebbe
Created January 31, 2023 11:32
Show Gist options
  • Save lebbe/b4e12d3294f6e1006cdabb430419d524 to your computer and use it in GitHub Desktop.
Save lebbe/b4e12d3294f6e1006cdabb430419d524 to your computer and use it in GitHub Desktop.
Find previous focusable sibling
/*
* 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