Skip to content

KonnorRogers/focus-hunter

Repository files navigation

Bundle Size Badge

Purpose

Focus trapping made easy for things like Dialogs.

Why?

Because focus trapping sucks. But its a necessary evil.

Demo

https://konnorrogers.github.io/focus-hunter

Prior Art

  • Focus Trap was attempted to be used, but was quite big (~5kb) and didn't handle multiple levels of shadow DOM. It is however a big inspiration for this library.

  • This solution has been largely extracted from Shoelace

Differences from Focus Trap

Focus Hunter doesn't aim to do everything. It tries its best to keep a small minimal API and get out of your way. This is reflected in bundle size.

focus-hunter is ~1.5kb minified + gzipped. focus-trap is ~5.5kb minified + gzipped.

Installation

npm install focus-hunter

Adding a trap

// Create a trap
const trap = new Trap({ rootElement: document.querySelector("my-trap") })

// Start the trap
trap.start()

// Stop the trap
trap.stop()

All Options

const trap = new Trap({
  rootElement,
  preventScroll, // Passed to `element.focus({ preventScroll })` for programmatically focused elements
})

Multiple Traps

Focus Trap is allowed to have multiple traps. It keeps track of the stacks using window.focusHunter.trapStack which is implemented via a Set.

There is also a stack of rootElements at window.focusHunter.rootElementStack

There 2 stacks are checked when you call trap.start() to ensure the rootElement isn't already being trapped and that the trap isn't already active.

window.focusHunter.trapStack // => Set
window.focusHunter.rootElementStack // => Set

A note on iframes

While the focus trap can get to an <iframe> it cannot find elements within a cross origin iframe so they are excluded from the focus trap.

Differences from Shoelace

This library is largely me experimenting with generators. Beyond internal implementation details, here are some differences:

- // Elements with aria-disabled are not tabbable
- if (el.hasAttribute('aria-disabled') && el.getAttribute('aria-disabled') !== 'false') {
-   return false;
- }

The above was removed from exports/tabbable.js because aria-disabled elements are tabbable.

+  // Anchor tags with no hrefs arent focusable.
+  // This is focusable: <a href="">Stuff</a>
+  // This is not: <a>Stuff</a>
+  if ("a" === tag && el.getAttribute("href") == null) return false

While not a big deal, anchor elements without an href attribute were getting tripped up. So we added a check to make sure it has an href.

+iframe, object, embed

The additional elements were found here: https://github.com/gdkraus/accessible-modal-dialog/blob/d2a9c13de65028cda917279246346a277509fda0/modal-window.js#L38

Structure

exports/ is publicly available files internal/ is...well...internal.

exports and internal shouldn't write their own .d.ts that are co-located.

types/ is where you place your handwritten .d.ts files.