Last active
April 22, 2021 14:35
-
-
Save tmarshall/36a2bb91179edb56c2bd718d501c75e5 to your computer and use it in GitHub Desktop.
Tab trap hook modal (or other) react components.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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