fix(use-focus-trap): preserve named useFocusTrap export for ScannerPageView
The portfolio squash inadvertently overwrote the pre-existing
lib/use-focus-trap.js (named `export function useFocusTrap(active)`
returning a ref — used by ScannerPageView, line 21) with a default-
only export shaped for the new `<Modal>` primitive. Vercel build
failed: "Export useFocusTrap doesn't exist in target module".
Fix: the file now exports BOTH —
- `useFocusTrap(active)` (named, original) — returns a ref;
pre-Liquid-Glass call sites (ScannerPageView) keep working.
- `useFocusTrapContainer({ active, containerRef, ... })` (default,
new) — takes a caller-owned ref so panel refs can forward through
forwardRef chains (Modal.js consumes this shape).
Both hooks are commented to document which to use when. Modal.js
imports default already, so no change needed there.
Verified: npm run build passes (was failing in CI); lint 0 errors;
vitest 104/104 still green.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
66553fa99a
commit
72fe6d1be0
1 changed files with 83 additions and 4 deletions
|
|
@ -1,6 +1,74 @@
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
const FOCUSABLE = [
|
const FOCUSABLE_SELECTOR =
|
||||||
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
||||||
|
|
||||||
|
function getFocusableElements(container) {
|
||||||
|
return Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR)).filter(
|
||||||
|
(el) => !el.disabled && el.getAttribute('aria-hidden') !== 'true'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trap focus inside a container while `active` and restore focus on close.
|
||||||
|
* Returns a ref to attach to the dialog panel (not the backdrop).
|
||||||
|
*
|
||||||
|
* Used by pre-Liquid-Glass call sites (e.g. ScannerPageView) where the
|
||||||
|
* caller wants the hook to own the container ref. New code that needs
|
||||||
|
* to share / forward the panel ref should use the default export
|
||||||
|
* `useFocusTrapContainer` instead — see Modal.js for the pattern.
|
||||||
|
*/
|
||||||
|
export function useFocusTrap(active) {
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
const previouslyFocusedRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!active) return undefined;
|
||||||
|
|
||||||
|
previouslyFocusedRef.current = document.activeElement;
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return undefined;
|
||||||
|
|
||||||
|
const focusFirst = () => {
|
||||||
|
const nodes = getFocusableElements(container);
|
||||||
|
nodes[0]?.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
focusFirst();
|
||||||
|
|
||||||
|
const handleKeyDown = (event) => {
|
||||||
|
if (event.key !== 'Tab') return;
|
||||||
|
|
||||||
|
const nodes = getFocusableElements(container);
|
||||||
|
if (nodes.length === 0) return;
|
||||||
|
|
||||||
|
const first = nodes[0];
|
||||||
|
const last = nodes[nodes.length - 1];
|
||||||
|
|
||||||
|
if (event.shiftKey && document.activeElement === first) {
|
||||||
|
event.preventDefault();
|
||||||
|
last.focus();
|
||||||
|
} else if (!event.shiftKey && document.activeElement === last) {
|
||||||
|
event.preventDefault();
|
||||||
|
first.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
const previous = previouslyFocusedRef.current;
|
||||||
|
if (previous && typeof previous.focus === 'function') {
|
||||||
|
previous.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [active]);
|
||||||
|
|
||||||
|
return containerRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NEW_FOCUSABLE = [
|
||||||
'a[href]',
|
'a[href]',
|
||||||
'button:not([disabled])',
|
'button:not([disabled])',
|
||||||
'input:not([disabled]):not([type="hidden"])',
|
'input:not([disabled]):not([type="hidden"])',
|
||||||
|
|
@ -10,7 +78,18 @@ const FOCUSABLE = [
|
||||||
'[contenteditable="true"]',
|
'[contenteditable="true"]',
|
||||||
].join(',');
|
].join(',');
|
||||||
|
|
||||||
export default function useFocusTrap({ active, containerRef, initialFocusRef, restoreFocus = true }) {
|
/**
|
||||||
|
* Focus-trap variant for the `<Modal>` primitive in components/ui/.
|
||||||
|
* Takes a caller-owned containerRef + optional initialFocusRef, so the
|
||||||
|
* panel ref can be forwarded through GlassSurface / forwardRef chains
|
||||||
|
* without colliding with the hook's internal ref ownership.
|
||||||
|
*/
|
||||||
|
export default function useFocusTrapContainer({
|
||||||
|
active,
|
||||||
|
containerRef,
|
||||||
|
initialFocusRef,
|
||||||
|
restoreFocus = true,
|
||||||
|
}) {
|
||||||
const previousActiveElement = useRef(null);
|
const previousActiveElement = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -22,7 +101,7 @@ export default function useFocusTrap({ active, containerRef, initialFocusRef, re
|
||||||
|
|
||||||
const focusTarget =
|
const focusTarget =
|
||||||
initialFocusRef?.current ??
|
initialFocusRef?.current ??
|
||||||
container.querySelector(FOCUSABLE) ??
|
container.querySelector(NEW_FOCUSABLE) ??
|
||||||
container;
|
container;
|
||||||
if (focusTarget && typeof focusTarget.focus === 'function') {
|
if (focusTarget && typeof focusTarget.focus === 'function') {
|
||||||
if (focusTarget === container && !container.hasAttribute('tabindex')) {
|
if (focusTarget === container && !container.hasAttribute('tabindex')) {
|
||||||
|
|
@ -33,7 +112,7 @@ export default function useFocusTrap({ active, containerRef, initialFocusRef, re
|
||||||
|
|
||||||
const handleKeyDown = (e) => {
|
const handleKeyDown = (e) => {
|
||||||
if (e.key !== 'Tab') return;
|
if (e.key !== 'Tab') return;
|
||||||
const focusable = Array.from(container.querySelectorAll(FOCUSABLE));
|
const focusable = Array.from(container.querySelectorAll(NEW_FOCUSABLE));
|
||||||
if (focusable.length === 0) {
|
if (focusable.length === 0) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue