Skip to content

Instantly share code, notes, and snippets.

@predragnikolic
Last active March 14, 2022 09:44
Show Gist options
  • Save predragnikolic/f50a7c31822fcb8bf64744316563adf9 to your computer and use it in GitHub Desktop.
Save predragnikolic/f50a7c31822fcb8bf64744316563adf9 to your computer and use it in GitHub Desktop.
Trap focus. Useful for trapping focus within a modal, or an open dropdown...
// inspired by
// https://hiddedevries.nl/en/blog/2017-01-29-using-javascript-to-trap-focus-in-an-element
import { ReactNode, useEffect, useRef } from "react"
type FocusableHTMLElement = HTMLButtonElement
type Props = {
children: ReactNode
}
export function FocusTrap({ children }: Props) {
const focusTrapElRef = useRef<HTMLDivElement>(null)
useEffect(() => {
// currentlyFocusedEl is used to restore focus when the focus trap element is removed
const currentlyFocusedEl = document.activeElement as FocusableHTMLElement | null
const element = focusTrapElRef.current
if (!element) return
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] as FocusableHTMLElement | null
firstFocusableEl?.focus()
const onKeyDown = (e: KeyboardEvent) => {
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] as FocusableHTMLElement | null
const lastFocusableEl = focusableEls[focusableEls.length - 1] as FocusableHTMLElement | null
const KEYCODE_TAB = 9
const isTabPressed = (e.key === "Tab" || e.keyCode === KEYCODE_TAB)
if (!isTabPressed) return
if ( e.shiftKey ) /* shift + tab */ {
if (document.activeElement === firstFocusableEl) {
lastFocusableEl?.focus()
e.preventDefault()
}
} else /* tab */ {
if (document.activeElement === lastFocusableEl) {
firstFocusableEl?.focus()
e.preventDefault()
}
}
}
element.addEventListener("keydown", onKeyDown)
return () => {
element.removeEventListener("keydown", onKeyDown)
currentlyFocusedEl?.focus()
}
}, [children])
return <div ref={focusTrapElRef}>{children}</div>
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment