feat: implement role-based access for settings route and add focus trap hook

- Wrapped the Settings component in RequireRole to enforce access control for admin and user roles.
- Introduced a new custom hook `useFocusTrap` to manage focus within modal dialogs, enhancing accessibility.
- Applied the focus trap in InviteModal, PermissionsModal, and UserDetailModal to prevent focus from leaving the dialog.
- Updated PassthroughLanding to focus on the heading when the component mounts.
This commit is contained in:
GitHub Actions
2026-03-03 03:08:59 +00:00
parent a681d6aa30
commit 3f12ca05a3
7 changed files with 1105 additions and 447 deletions
+47
View File
@@ -0,0 +1,47 @@
import { useEffect, type RefObject } from 'react'
const FOCUSABLE_SELECTOR =
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
export function useFocusTrap(
dialogRef: RefObject<HTMLElement | null>,
isOpen: boolean,
onEscape?: () => void,
) {
useEffect(() => {
if (!isOpen) return
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && onEscape) {
onEscape()
return
}
if (e.key === 'Tab' && dialogRef.current) {
const focusable =
dialogRef.current.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)
if (focusable.length === 0) return
const first = focusable[0]
const last = focusable[focusable.length - 1]
if (e.shiftKey && document.activeElement === first) {
e.preventDefault()
last.focus()
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault()
first.focus()
}
}
}
document.addEventListener('keydown', handleKeyDown)
requestAnimationFrame(() => {
const first = dialogRef.current?.querySelector<HTMLElement>(FOCUSABLE_SELECTOR)
first?.focus()
})
return () => document.removeEventListener('keydown', handleKeyDown)
}, [isOpen, onEscape, dialogRef])
}