Skip to content

Instantly share code, notes, and snippets.

@lebbe
Created January 31, 2023 11:32
Show Gist options
  • Select an option

  • Save lebbe/b4e12d3294f6e1006cdabb430419d524 to your computer and use it in GitHub Desktop.

Select an option

Save lebbe/b4e12d3294f6e1006cdabb430419d524 to your computer and use it in GitHub Desktop.

Revisions

  1. lebbe created this gist Jan 31, 2023.
    92 changes: 92 additions & 0 deletions findPreviouslyFocusableElement.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,92 @@
    /*
    * 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();
    }
    }