aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
279 lines (276 loc) • 10.8 kB
JavaScript
'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