aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
282 lines (279 loc) • 8.63 kB
JavaScript
'use client';
import { useRef, useState, useCallback, useEffect } from 'react';
const DEFAULT_OPTIONS = {
enabled: true,
color: 'var(--glass-color-primary, 0.5)',
width: 2,
blur: 4,
spread: 2,
duration: 200,
offset: 2,
position: 'relative',
customStyles: {},
keyboardNavigation: true,
autoFocus: false,
focusTrap: false,
restoreFocus: false
};
function useGlassFocus(options = {}) {
const elementRef = useRef(null);
const previousFocusedElementRef = useRef(null);
const focusOriginRef = useRef(null);
const [focusState, setFocusState] = useState({
isFocused: false,
isKeyboardFocused: false,
focusOrigin: null,
focusRingVisible: false,
previousFocusedElement: null
});
const finalOptions = {
...DEFAULT_OPTIONS,
...options
};
// Base inline styles only (no pseudo-selectors here)
const baseStyle = useCallback(() => {
if (!finalOptions.enabled) return {};
return {
outline: 'none',
position: 'relative'
// pseudo-element styles are applied by CSS-in-JS in the wrapper component
};
}, [finalOptions.enabled]);
// Handle focus events
const handleFocus = useCallback(event => {
const target = event.target;
// Determine focus origin
if (event.relatedTarget) {
focusOriginRef.current = 'keyboard';
} else if (focusOriginRef.current === null) {
focusOriginRef.current = 'programmatic';
}
setFocusState(prev => ({
...prev,
isFocused: true,
isKeyboardFocused: focusOriginRef.current === 'keyboard',
focusOrigin: focusOriginRef.current,
focusRingVisible: finalOptions.enabled && focusOriginRef.current === 'keyboard'
}));
// Apply focus styles
if (target && finalOptions.enabled) {
target.classList.add('glass-focus-visible');
if (focusOriginRef.current === 'keyboard') {
target.classList.add('glass-focus-keyboard');
}
}
}, [finalOptions.enabled]);
const handleBlur = useCallback(event => {
const target = event.target;
setFocusState(prev => ({
...prev,
isFocused: false,
isKeyboardFocused: false,
focusOrigin: null,
focusRingVisible: false
}));
// Remove focus styles
if (target) {
target.classList.remove('glass-focus-visible', 'glass-focus-keyboard');
}
}, []);
// Handle mouse down to detect mouse focus
const handleMouseDown = useCallback(() => {
focusOriginRef.current = 'mouse';
}, []);
// Handle key down to detect keyboard focus
const handleKeyDown = useCallback(event => {
if (['Tab', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Enter', ' '].includes(event.key)) {
focusOriginRef.current = 'keyboard';
}
}, []);
// Programmatic focus methods
const focus = useCallback(() => {
if (elementRef.current && finalOptions.enabled) {
focusOriginRef.current = 'programmatic';
elementRef.current.focus();
}
}, [finalOptions.enabled]);
const blur = useCallback(() => {
if (elementRef.current) {
elementRef.current.blur();
}
}, []);
// Focus trap implementation
const setupFocusTrap = useCallback(() => {
if (!finalOptions.focusTrap || !elementRef.current) return;
const element = elementRef.current;
const focusableElements = element.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
const handleKeyDown = event => {
if (event.key === 'Tab') {
if (event.shiftKey) {
if (document.activeElement === firstElement) {
event.preventDefault();
lastElement?.focus();
}
} else {
if (document.activeElement === lastElement) {
event.preventDefault();
firstElement?.focus();
}
}
}
};
element.addEventListener('keydown', handleKeyDown);
return () => {
element.removeEventListener('keydown', handleKeyDown);
};
}, [finalOptions.focusTrap]);
// Setup event listeners
useEffect(() => {
const element = elementRef.current;
if (!element) return;
// Store previous focused element for restoration
if (finalOptions.restoreFocus) {
previousFocusedElementRef.current = document.activeElement;
}
// Add event listeners
element.addEventListener('focus', handleFocus);
element.addEventListener('blur', handleBlur);
element.addEventListener('mousedown', handleMouseDown);
if (finalOptions.keyboardNavigation) {
element.addEventListener('keydown', handleKeyDown);
}
// Setup focus trap
const cleanupFocusTrap = setupFocusTrap();
// Auto-focus if enabled
if (finalOptions.autoFocus) {
setTimeout(() => focus(), 0);
}
return () => {
element.removeEventListener('focus', handleFocus);
element.removeEventListener('blur', handleBlur);
element.removeEventListener('mousedown', handleMouseDown);
if (finalOptions.keyboardNavigation) {
element.removeEventListener('keydown', handleKeyDown);
}
cleanupFocusTrap?.();
// Restore focus if enabled
if (finalOptions.restoreFocus && previousFocusedElementRef.current) {
setTimeout(() => {
previousFocusedElementRef.current?.focus?.();
}, 0);
}
};
}, [handleFocus, handleBlur, handleMouseDown, handleKeyDown, setupFocusTrap, finalOptions.autoFocus, finalOptions.keyboardNavigation, finalOptions.restoreFocus]);
return {
ref: elementRef,
focusState,
baseStyle: baseStyle(),
focus,
blur,
options: finalOptions
};
}
// Hook for managing focus within a group of elements
function useGlassFocusGroup(options = {
elements: []
}) {
const {
elements,
loop = true,
...focusOptions
} = options;
const [currentFocusIndex, setCurrentFocusIndex] = useState(-1);
const moveFocus = useCallback(direction => {
if (elements.length === 0) return;
let newIndex;
switch (direction) {
case 'first':
newIndex = 0;
break;
case 'last':
newIndex = elements.length - 1;
break;
case 'next':
newIndex = loop ? (currentFocusIndex + 1) % elements.length : Math.min(currentFocusIndex + 1, elements.length - 1);
break;
case 'previous':
newIndex = loop ? (currentFocusIndex - 1 + elements.length) % elements.length : Math.max(currentFocusIndex - 1, 0);
break;
}
setCurrentFocusIndex(newIndex);
elements[newIndex]?.focus();
}, [elements, currentFocusIndex, loop]);
const handleKeyDown = useCallback(event => {
switch (event.key) {
case 'ArrowRight':
case 'ArrowDown':
event.preventDefault();
moveFocus('next');
break;
case 'ArrowLeft':
case 'ArrowUp':
event.preventDefault();
moveFocus('previous');
break;
case 'Home':
event.preventDefault();
moveFocus('first');
break;
case 'End':
event.preventDefault();
moveFocus('last');
break;
}
}, [moveFocus]);
useEffect(() => {
const handleGlobalKeyDown = event => {
if (elements.includes(event.target)) {
handleKeyDown(event);
}
};
document.addEventListener('keydown', handleGlobalKeyDown);
return () => document.removeEventListener('keydown', handleGlobalKeyDown);
}, [elements, handleKeyDown]);
return {
currentFocusIndex,
moveFocus,
focusOptions
};
}
// Hook for skip links (accessibility)
function useSkipLinks(links) {
const [visible, setVisible] = useState(false);
useEffect(() => {
const handleKeyDown = event => {
if (event.key === 'Tab' && !visible) {
setVisible(true);
}
};
const handleClick = () => {
setVisible(false);
};
document.addEventListener('keydown', handleKeyDown);
document.addEventListener('click', handleClick);
return () => {
document.removeEventListener('keydown', handleKeyDown);
document.removeEventListener('click', handleClick);
};
}, [visible]);
const skipTo = useCallback(targetId => {
const target = document.getElementById(targetId);
if (target) {
target.focus();
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
setVisible(false);
}, []);
return {
visible,
links,
skipTo
};
}
export { useGlassFocus, useGlassFocusGroup, useSkipLinks };
//# sourceMappingURL=useGlassFocus.js.map