Skip to content

Instantly share code, notes, and snippets.

@bmorrall
Last active November 26, 2022 04:16
Show Gist options
  • Save bmorrall/1e6977c3183e59b3d6723d353e1cc49c to your computer and use it in GitHub Desktop.
Save bmorrall/1e6977c3183e59b3d6723d353e1cc49c to your computer and use it in GitHub Desktop.
Prevent a user from tabbing outside of an Stimulus Controller element (Useful for Modals and Dialogs).
// Trap Focus
// https://hiddedevries.nl/en/blog/2017-01-29-using-javascript-to-trap-focus-in-an-element
//
// Usage:
//
// import { Controller } from 'stimulus'
// import { useTrapFocus } from './mixins/use-trap-focus.js
//
// export default class extends Controller {
// static targets = ['dialog']
// open () {
// // prevent the user from tabbing outside of the element
// useTrapFocus(this, this.dialogTarget)
//
// // ...
// }
// }
//
export const useTrapFocus = (composableController, targetElement = null) => {
const controller = composableController
const element = targetElement || controller.element
const focusableEls = element.querySelectorAll('a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])')
const firstFocusableEl = focusableEls[0]
const lastFocusableEl = focusableEls[focusableEls.length - 1]
const KEYCODE_TAB = 9
const keydownHandler = (event) => {
const isTabPressed = (event.key === 'Tab' || event.keyCode === KEYCODE_TAB)
if (!isTabPressed) {
return
}
if (event.shiftKey) /* shift + tab */ {
if (document.activeElement === firstFocusableEl) {
lastFocusableEl.focus()
event.preventDefault()
}
} else /* tab */ {
if (document.activeElement === lastFocusableEl) {
firstFocusableEl.focus()
event.preventDefault()
}
}
}
const controllerDisconnect = controller.disconnect.bind(controller)
const observe = () => {
element.addEventListener('keydown', keydownHandler)
}
const unobserve = () => {
element.removeEventListener('keydown', keydownHandler)
}
Object.assign(controller, {
disconnect() {
unobserve()
controllerDisconnect()
}
})
observe()
return [observe, unobserve]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment