Focus Trap
Purpose
Focus Trap provides shared utility behavior for design-system usage.
Import
import { FocusTrap } from "@tesseract-nexus/tesserix-ui"API reference
import * as React from "react"
export interface FocusTrapProps {
/**
* The children to render
*/
children: React.ReactNode
/**
* Whether the focus trap is active
*/
enabled?: boolean
/**
* Whether to restore focus to the previously focused element on unmount
*/
restoreFocus?: boolean
}
/**
* Component that traps focus within its children
*/
export function FocusTrap({ children, enabled = true, restoreFocus = true }: FocusTrapProps) {
const containerRef = React.useRef<HTMLDivElement>(null)
const previouslyFocusedElement = React.useRef<HTMLElement | null>(null)
React.useEffect(() => {
if (!enabled) return
// Store the currently focused element
previouslyFocusedElement.current = document.activeElement as HTMLElement
const container = containerRef.current
if (!container) return
// Focus first focusable element
const focusableElements = getFocusableElements(container)
if (focusableElements.length > 0) {
focusableElements[0].focus()
}
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key !== "Tab") return
const focusableElements = getFocusableElements(container)
if (focusableElements.length === 0) return
const firstElement = focusableElements[0]
const lastElement = focusableElements[focusableElements.length - 1]
// If shift + tab on first element, focus last
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault()
lastElement.focus()
}
// If tab on last element, focus first
else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault()
firstElement.focus()
}
}
document.addEventListener("keydown", handleKeyDown)
return () => {
document.removeEventListener("keydown", handleKeyDown)
// Restore focus to previously focused element
if (restoreFocus && previouslyFocusedElement.current) {
previouslyFocusedElement.current.focus()
}
}
}, [enabled, restoreFocus])
return <div ref={containerRef}>{children}</div>
}
function getFocusableElements(container: HTMLElement): HTMLElement[] {
const selector =
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])'
return Array.from(container.querySelectorAll(selector)) as HTMLElement[]
}Do / Don’t
Do
// Use shared utility in app architecture for consistencyDon’t
// Avoid duplicating equivalent utility behavior in feature codeToken / Theming Mapping
- Utility UI should inherit system tokens: —primary, —foreground, —muted, —radius, —ring.
- Theming/loading utilities should rely on provider config and CSS variables.
Interaction Test Checklist
- Functional behavior contract verification.
- Integration verification in at least one consuming screen.
- Accessibility verification for focus/visibility semantics where relevant.
- Error/fallback behavior verification.