Skip to content

Instantly share code, notes, and snippets.

@tmarshall
Last active April 22, 2021 14:35
Show Gist options
  • Save tmarshall/36a2bb91179edb56c2bd718d501c75e5 to your computer and use it in GitHub Desktop.
Save tmarshall/36a2bb91179edb56c2bd718d501c75e5 to your computer and use it in GitHub Desktop.
Tab trap hook modal (or other) react components.
/*
This hook will trap tabbing within a component.
A common usecase is a Modal, in which you want tabbing to keep focus within the modal.
This assumes that any custom `tabindex` prop is either `0` (should be tabbable, no order preference)
or `-1` (not tabbable, should skip). Anything like `tabIndex={2}` will not get ordered correctly.
It will skip over disabled inputs.
---
```jsx
// not including actual modal styles/logic
const Modal = ({ children }) => {
const containerRef = useRef(null)
useTabTrap(ref) // needs ref to component
return (
<div ref={containerRef}>
{children}
</div>
)
}
```
*/
import { useLayoutEffect } from 'react'
const focusableSelector = 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, [tabindex]:not([tabindex="-1"]), [contenteditable]'
export default function useTabTrap(contentRef) {
useLayoutEffect(() => {
const listener = (event) => {
if (event.key !== 'Tab') {
return
}
// forcing focus to the first focusable element if any of these are true:
// - no current focus on page
// - focused element is outside of the container
// - focused element is last in the container
let forceFocus = false
let firstFocusableElement = null
if (!document.activeElement) {
forceFocus = true
}
if (!forceFocus && !contentRef.current.contains(document.activeElement)) {
forceFocus = true
}
if (!forceFocus) {
let focusableInModal = contentRef.current.querySelectorAll(focusableSelector)
if (event.shiftKey) {
focusableInModal = Array.prototype.slice.call(focusableInModal)
focusableInModal.reverse()
}
if (document.activeElement === focusableInModal[focusableInModal.length - 1]) {
firstFocusableElement = focusableInModal[0]
forceFocus = true
}
}
if (forceFocus) {
event.preventDefault()
if (!firstFocusableElement) {
if (event.shiftKey) {
firstFocusableElement = contentRef.current.querySelectorAll(focusableSelector)
firstFocusableElement = Array.prototype.slice.call(firstFocusableElement)
firstFocusableElement.reverse()
firstFocusableElement = firstFocusableElement[0]
} else {
firstFocusableElement = contentRef.current.querySelector(focusableSelector)
}
}
firstFocusableElement.focus()
}
}
document.addEventListener('keydown', listener)
return () => {
document.removeEventListener('keydown', listener)
}
}, [])
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment