feat(design-system): Liquid Glass redesign portfolio — foundation + primitives + Layout #95

Merged
varutasu merged 2 commits from liquid-glass-foundation-portfolio into main 2026-06-03 21:12:33 -04:00
Showing only changes of commit 72fe6d1be0 - Show all commits

View file

@ -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;