feat(design-system): Liquid Glass redesign portfolio — foundation + primitives + Layout #95
1 changed files with 83 additions and 4 deletions
|
|
@ -1,6 +1,74 @@
|
|||
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]',
|
||||
'button:not([disabled])',
|
||||
'input:not([disabled]):not([type="hidden"])',
|
||||
|
|
@ -10,7 +78,18 @@ const FOCUSABLE = [
|
|||
'[contenteditable="true"]',
|
||||
].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);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -22,7 +101,7 @@ export default function useFocusTrap({ active, containerRef, initialFocusRef, re
|
|||
|
||||
const focusTarget =
|
||||
initialFocusRef?.current ??
|
||||
container.querySelector(FOCUSABLE) ??
|
||||
container.querySelector(NEW_FOCUSABLE) ??
|
||||
container;
|
||||
if (focusTarget && typeof focusTarget.focus === 'function') {
|
||||
if (focusTarget === container && !container.hasAttribute('tabindex')) {
|
||||
|
|
@ -33,7 +112,7 @@ export default function useFocusTrap({ active, containerRef, initialFocusRef, re
|
|||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key !== 'Tab') return;
|
||||
const focusable = Array.from(container.querySelectorAll(FOCUSABLE));
|
||||
const focusable = Array.from(container.querySelectorAll(NEW_FOCUSABLE));
|
||||
if (focusable.length === 0) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
|
|
|
|||
Loading…
Reference in a new issue