/* * 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(); } }