UNPKG

aura-glass

Version:

A comprehensive glassmorphism design system for React applications with 142+ production-ready components

279 lines (276 loc) 10.8 kB
'use client'; import { jsx, jsxs } from 'react/jsx-runtime'; import { useReducedMotion } from '../../hooks/useReducedMotion.js'; import { cn } from '../../lib/utilsComprehensive.js'; import { AnimatePresence, motion } from 'framer-motion'; import { useState, useRef, useEffect } from 'react'; import { useAccessibility } from './AccessibilityProvider.js'; // Enhanced focus ring component function FocusRing({ element, variant = "default" }) { const prefersReducedMotion = useReducedMotion(); const [position, setPosition] = useState({ x: 0, y: 0, width: 0, height: 0 }); const [isVisible, setIsVisible] = useState(true); useEffect(() => { if (!element) return; const updatePosition = () => { const rect = element.getBoundingClientRect(); setPosition({ x: rect.left + window.scrollX, y: rect.top + window.scrollY, width: rect.width, height: rect.height }); }; updatePosition(); const observer = new ResizeObserver(updatePosition); observer.observe(element); window.addEventListener("scroll", updatePosition); window.addEventListener("resize", updatePosition); return () => { observer.disconnect(); window.removeEventListener("scroll", updatePosition); window.removeEventListener("resize", updatePosition); }; }, [element]); const getVariantStyles = () => { switch (variant) { case "interactive": return { ring: "ring-4 ring-blue-400/60 ring-offset-2 ring-offset-slate-900", glow: "shadow-[0_0_20px_rgba(96,165,250,0.6)]", background: "bg-blue-400/10" }; case "navigation": return { ring: "ring-3 ring-purple-400/60 ring-offset-2 ring-offset-slate-900", glow: "shadow-[0_0_15px_rgba(167,139,250,0.6)]", background: "bg-purple-400/10" }; case "form": return { ring: "ring-3 ring-green-400/60 ring-offset-2 ring-offset-slate-900", glow: "shadow-[0_0_15px_rgba(34,197,94,0.6)]", background: "bg-green-400/10" }; default: return { ring: "ring-3 ring-blue-400/60 ring-offset-2 ring-offset-slate-900", glow: "shadow-[0_0_15px_rgba(96,165,250,0.5)]", background: "bg-blue-400/8" }; } }; const styles = getVariantStyles(); return jsxs(motion.div, { className: cn("glass-position-fixed glass-pointer-events-none glass-z-50 glass-radius-lg", styles.ring, styles.glow, styles.background), style: { left: position.x - 4, top: position.y - 4, width: position.width + 8, height: position.height + 8 }, initial: { opacity: 0, scale: 0.8 }, animate: prefersReducedMotion ? {} : { opacity: isVisible ? 1 : 0, scale: 1 }, exit: { opacity: 0, scale: 0.8 }, transition: { type: "spring", stiffness: 400, damping: 25, opacity: { duration: 0.15 } }, children: [jsx(motion.div, { className: cn("glass-position-absolute glass-inset-0 glass-radius-lg glass-border-2 glass-border-transparent"), style: { background: `linear-gradient(45deg, ${variant === "interactive" ? "var(--glass-color-primary-light)" : variant === "navigation" ? "#A78BFA" : variant === "form" ? "#22C55E" : "var(--glass-color-primary-light)"}, transparent, ${variant === "interactive" ? "var(--glass-color-primary-light)" : variant === "navigation" ? "#A78BFA" : variant === "form" ? "#22C55E" : "var(--glass-color-primary-light)"})`, backgroundSize: "200% 200%" }, animate: prefersReducedMotion ? {} : { backgroundPosition: ["0% 0%", "100% 100%", "0% 0%"] }, transition: prefersReducedMotion ? { duration: 0 } : { duration: 2, repeat: Infinity, ease: "linear" } }), ["top-left", "top-right", "bottom-left", "bottom-right"].map(corner => jsx(motion.div, { className: cn("glass-position-absolute glass-w-2 glass-h-2 glass-radius-full", variant === "interactive" ? "glass-surface-info" : variant === "navigation" ? "glass-surface-accent" : variant === "form" ? "bg-green-400" : "bg-blue-400", corner === "top-left" ? "-top-1 -left-1" : corner === "top-right" ? "-top-1 -right-1" : corner === "bottom-left" ? "-bottom-1 -left-1" : "-bottom-1 -right-1"), animate: prefersReducedMotion ? {} : { scale: [1, 1.5, 1], opacity: [0.6, 1, 0.6] }, transition: prefersReducedMotion ? { duration: 0 } : { duration: 1.5, repeat: Infinity, delay: corner === "top-right" ? 0.2 : corner === "bottom-right" ? 0.4 : corner === "bottom-left" ? 0.6 : 0 } }, corner))] }); } // Main focus indicator system function GlassFocusIndicators() { const { settings } = useAccessibility(); const [focusedElement, setFocusedElement] = useState(null); const [focusVariant, setFocusVariant] = useState("default"); const lastFocusedRef = useRef(null); useEffect(() => { if (!settings.focusIndicators) return; const handleFocus = event => { const target = event.target; if (!target || target === document.body) return; // Determine focus variant based on element type and attributes let variant = "default"; if (target.matches('button, [role="button"], [role="menuitem"], [role="tab"]')) { variant = "interactive"; } else if (target.matches('a, [role="link"], nav a, [role="navigation"] *')) { variant = "navigation"; } else if (target.matches('input, textarea, select, [role="textbox"], [role="combobox"], [role="listbox"]')) { variant = "form"; } setFocusedElement(target); setFocusVariant(variant); lastFocusedRef.current = target; // Announce focus change to screen readers if (settings.screenReaderOptimized && window.announceToScreenReader) { const elementType = target.tagName.toLowerCase(); const elementText = target.textContent || target.getAttribute("aria-label") || target.getAttribute("aria-labelledby") || target.getAttribute("title") || ""; const roleText = target.getAttribute("role") || elementType; window.announceToScreenReader(`Focused on ${roleText}${elementText ? ": " + elementText.slice(0, 50) : ""}`); } }; const handleBlur = event => { // Small delay to prevent flicker when focus moves between elements setTimeout(() => { if (document.activeElement === document.body || !document.activeElement) { setFocusedElement(null); } }, 10); }; // Enhanced keyboard navigation const handleKeyDown = event => { if (!settings.keyboardNavigation) return; const { key, ctrlKey, altKey, shiftKey } = event; // Skip navigation (Alt + S) if (altKey && key === "s") { event.preventDefault(); const mainContent = document.getElementById("main-content") || document.querySelector("main"); if (mainContent) { mainContent.focus(); mainContent.scrollIntoView({ behavior: "smooth", block: "start" }); } } // Focus landmarks (Alt + L) if (altKey && key === "l") { event.preventDefault(); const landmarks = document.querySelectorAll('[role="banner"], [role="navigation"], [role="main"], [role="contentinfo"], header, nav, main, footer'); if (landmarks.length > 0) { const currentIndex = Array.from(landmarks).findIndex(el => el === document.activeElement); const nextIndex = (currentIndex + 1) % landmarks.length; landmarks[nextIndex].focus(); } } // Focus headings (Alt + H) if (altKey && key === "h") { event.preventDefault(); const headings = document.querySelectorAll("h1, h2, h3, h4, h5, h6"); if (headings.length > 0) { const currentIndex = Array.from(headings).findIndex(el => el === document.activeElement); const nextIndex = (currentIndex + 1) % headings.length; headings[nextIndex].focus(); } } // Escape key to blur current element if (key === "Escape" && focusedElement && focusedElement !== document.body) { focusedElement.blur(); document.body.focus(); } }; // Add event listeners document.addEventListener("focusin", handleFocus, true); document.addEventListener("focusout", handleBlur, true); document.addEventListener("keydown", handleKeyDown); // Focus visible elements on page load const autoFocusElement = document.querySelector("[autofocus]"); if (autoFocusElement) { setTimeout(() => autoFocusElement.focus(), 100); } return () => { document.removeEventListener("focusin", handleFocus, true); document.removeEventListener("focusout", handleBlur, true); document.removeEventListener("keydown", handleKeyDown); }; }, [settings.focusIndicators, settings.keyboardNavigation, settings.screenReaderOptimized, focusedElement]); // Add focus trap for modals and overlays useEffect(() => { const handleFocusTrap = () => { const modals = document.querySelectorAll('[role="dialog"], [aria-modal="true"], .modal, .overlay'); modals.forEach(modal => { const focusableElements = modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"]), [role="button"], [role="link"]'); if (focusableElements.length === 0) return; const firstElement = focusableElements[0]; const lastElement = focusableElements[focusableElements.length - 1]; const handleTabKey = event => { if (event.key !== "Tab") return; if (event.shiftKey) { if (document.activeElement === firstElement) { event.preventDefault(); lastElement.focus(); } } else { if (document.activeElement === lastElement) { event.preventDefault(); firstElement.focus(); } } }; modal.addEventListener("keydown", handleTabKey); }); }; if (settings.keyboardNavigation) { handleFocusTrap(); } }, [settings.keyboardNavigation, focusedElement]); if (!settings.focusIndicators || !focusedElement) { return null; } return jsx(AnimatePresence, { children: jsx(FocusRing, { element: focusedElement, variant: focusVariant }) }); } export { GlassFocusIndicators, GlassFocusIndicators as default }; //# sourceMappingURL=GlassFocusIndicators.js.map