aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
295 lines (292 loc) • 10.3 kB
JavaScript
'use client';
import { jsx, jsxs } from 'react/jsx-runtime';
import { GlassButton } from '../button/GlassButton.js';
import { cn } from '../../lib/utilsComprehensive.js';
import { forwardRef, useState, useRef, useEffect } from 'react';
import '../../primitives/GlassCore.js';
import '../../primitives/glass/GlassAdvanced.js';
import { OptimizedGlassCore } from '../../primitives/OptimizedGlassCore.js';
import '../../primitives/glass/OptimizedGlassAdvanced.js';
import '../../primitives/MotionNative.js';
import '../../primitives/motion/MotionFramer.js';
import { useA11yId } from '../../utils/a11y.js';
import { useMotionPreferenceContext } from '../../contexts/MotionPreferenceContext.js';
/**
* GlassAccordion component
* Collapsible sections with smooth animations and glassmorphism styling
*/
const GlassAccordion = /*#__PURE__*/forwardRef(({
// TODO: Integrate ContrastGuard for table cells, list items, badges, card titles, and other text content for WCAG AA compliance
items = [],
variant = "default",
size = "md",
multiple = false,
value,
defaultValue,
onValueChange,
showIcons = true,
expandIcon,
collapseIcon,
animationDuration = 300,
animated = true,
collapsible = true,
respectMotionPreference = true,
"aria-label": ariaLabel,
className,
...props
}, ref) => {
const accordionId = useA11yId("accordion");
const {
prefersReducedMotion
} = useMotionPreferenceContext();
const shouldAnimate = animated && (!respectMotionPreference || !prefersReducedMotion);
// Normalize value to array for consistent handling
const normalizeValue = val => {
if (!val) return [];
return Array.isArray(val) ? val : [val];
};
// Internal state for uncontrolled mode
const [internalValue, setInternalValue] = useState(normalizeValue(defaultValue));
// Determine current open items
const openItems = value !== undefined ? normalizeValue(value) : internalValue;
// Handle value changes
const handleValueChange = newValue => {
if (value === undefined) {
setInternalValue(newValue);
}
const result = multiple ? newValue : newValue?.[0] || "";
onValueChange?.(result);
};
// Toggle item open/closed
const toggleItem = itemId => {
const isOpen = (openItems || []).includes(itemId);
if (multiple) {
const newValue = isOpen ? (openItems || []).filter(id => id !== itemId) : [...openItems, itemId];
handleValueChange(newValue);
} else {
const newValue = isOpen && collapsible ? [] : [itemId];
handleValueChange(newValue);
}
};
// Handle keyboard navigation
const handleKeyDown = (event, itemId) => {
switch (event.key) {
case "Enter":
case " ":
event.preventDefault();
toggleItem(itemId);
break;
case "ArrowDown":
event.preventDefault();
focusNextItem(itemId);
break;
case "ArrowUp":
event.preventDefault();
focusPreviousItem(itemId);
break;
case "Home":
event.preventDefault();
focusFirstItem();
break;
case "End":
event.preventDefault();
focusLastItem();
break;
}
};
// Focus management helpers
const focusNextItem = currentId => {
if (!items || !Array.isArray(items) || (items?.length || 0) === 0) return;
const currentIndex = (items || []).findIndex(item => item?.id === currentId);
if (currentIndex === -1) return;
const nextIndex = (currentIndex + 1) % (items?.length || 0);
const nextButton = document.querySelector(`[data-accordion-trigger="${items[nextIndex].id}"]`);
nextButton?.focus();
};
const focusPreviousItem = currentId => {
if (!items || !Array.isArray(items) || (items?.length || 0) === 0) return;
const currentIndex = (items || []).findIndex(item => item?.id === currentId);
if (currentIndex === -1) return;
const prevIndex = currentIndex === 0 ? (items?.length || 0) - 1 : currentIndex - 1;
const prevButton = document.querySelector(`[data-accordion-trigger="${items[prevIndex].id}"]`);
prevButton?.focus();
};
const focusFirstItem = () => {
const firstButton = document.querySelector(`[data-accordion-trigger="${items[0]?.id}"]`);
firstButton?.focus();
};
const focusLastItem = () => {
const lastButton = document.querySelector(`[data-accordion-trigger="${items[(items?.length || 0) - 1]?.id}"]`);
lastButton?.focus();
};
// Size classes
const sizeClasses = {
sm: {
trigger: "glass-px-3 glass-py-2 glass-text-sm",
content: "glass-px-3 pb-2 glass-text-sm",
icon: "w-4 h-4"
},
md: {
trigger: "glass-px-4 glass-py-3 glass-text-base",
content: "glass-px-4 pb-3 glass-text-base",
icon: "w-5 h-5"
},
lg: {
trigger: "glass-px-6 glass-py-4 glass-text-lg",
content: "glass-px-6 pb-4 glass-text-lg",
icon: "w-6 h-6"
}
};
// Default icons
const defaultExpandIcon = jsx("svg", {
className: cn("transition-transform duration-200", sizeClasses[size].icon),
fill: "none",
stroke: "currentColor",
viewBox: "0 0 24 24",
children: jsx("path", {
strokeLinecap: "round",
strokeLinejoin: "round",
strokeWidth: 2,
d: "M19 9l-7 7-7-7"
})
});
const defaultCollapseIcon = jsx("svg", {
className: cn("transition-transform duration-200 rotate-180", sizeClasses[size].icon),
fill: "none",
stroke: "currentColor",
viewBox: "0 0 24 24",
children: jsx("path", {
strokeLinecap: "round",
strokeLinejoin: "round",
strokeWidth: 2,
d: "M19 9l-7 7-7-7"
})
});
return jsx("div", {
ref: ref,
id: accordionId,
className: cn("glass-accordion w-full", className),
role: "tablist",
"aria-label": ariaLabel || "Accordion",
"aria-multiselectable": multiple,
...props,
children: (items || []).map((item, index) => {
const isOpen = (openItems || []).includes(item?.id);
const isFirst = index === 0;
const isLast = index === (items?.length || 1) - 1;
return jsxs(OptimizedGlassCore, {
elevation: variant === "flush" ? undefined : "level1",
intensity: "medium",
depth: 2,
tint: "neutral",
border: "subtle",
animation: "none",
performanceMode: "medium",
className: cn("overflow-hidden transition-all duration-200", {
"border-0": variant === "flush",
"border border-border/20": variant === "bordered",
"glass-mb-2": variant !== "flush" && !isLast,
"border-t-0": variant === "flush" && !isFirst
}),
children: [jsxs(GlassButton, {
type: "button",
className: cn("w-full flex items-center justify-between text-left font-medium", "transition-colors duration-200", "hover:bg-muted/20 focus:bg-muted/20", "focus:outline-none focus:ring-2 focus:ring-primary/20", sizeClasses[size].trigger, {
"bg-muted/10": isOpen,
"opacity-50 cursor-not-allowed": item?.disabled,
"border-b border-border/10": variant === "flush" && isOpen
}),
onClick: e => !item?.disabled && toggleItem(item?.id),
onKeyDown: e => !item?.disabled && handleKeyDown(e, item?.id),
disabled: item?.disabled,
"aria-expanded": isOpen,
"aria-controls": `accordion-content-${item?.id}`,
"data-accordion-trigger": item?.id,
role: "tab",
children: [jsxs("div", {
className: "glass-flex glass-items-center glass-gap-3",
children: [item?.icon && jsx("span", {
className: "glass-flex-shrink-0 glass-text-secondary",
children: item?.icon
}), jsx("span", {
children: item?.title
})]
}), showIcons && !item?.disabled && jsx("span", {
className: cn("flex-shrink-0 glass-text-secondary transition-transform duration-200", isOpen && "rotate-180"),
children: isOpen ? collapseIcon || defaultCollapseIcon : expandIcon || defaultExpandIcon
})]
}), jsx(AccordionContent, {
id: `accordion-content-${item?.id}`,
isOpen: isOpen,
animated: shouldAnimate,
duration: animationDuration,
className: sizeClasses[size].content,
role: "tabpanel",
"aria-labelledby": `accordion-trigger-${item?.id}`,
children: item?.content
})]
}, item?.id);
})
});
});
GlassAccordion.displayName = "GlassAccordion";
const AccordionContent = /*#__PURE__*/forwardRef(({
id,
isOpen,
animated,
duration,
className,
children,
...props
}, ref) => {
const contentRef = useRef(null);
const [height, setHeight] = useState(isOpen ? "auto" : 0);
useEffect(() => {
if (!animated) {
setHeight(isOpen ? "auto" : 0);
return;
}
const content = contentRef.current;
if (!content) return;
if (isOpen) {
// Opening: measure content height and animate to it
const scrollHeight = content.scrollHeight;
setHeight(scrollHeight);
// After animation completes, set to auto for dynamic content
const timer = setTimeout(() => {
setHeight("auto");
}, duration);
return () => clearTimeout(timer);
} else {
// Closing: set explicit height first, then animate to 0
setHeight(content.scrollHeight);
// Force reflow to ensure height is set before animating to 0
requestAnimationFrame(() => {
setHeight(0);
});
}
}, [isOpen, animated, duration]);
const contentStyle = animated ? {
height: height === "auto" ? "auto" : `${height}px`,
overflow: height === "auto" ? "visible" : "hidden",
transition: `height ${duration}ms cubic-bezier(0.4, 0, 0.2, 1)`
} : {
height: isOpen ? "auto" : 0,
overflow: isOpen ? "visible" : "hidden"
};
return jsx("div", {
ref: contentRef,
id: id,
style: contentStyle,
...props,
children: jsx("div", {
className: cn("transition-opacity duration-200", className, {
"opacity-0": !isOpen && animated,
"opacity-100": isOpen || !animated
}),
children: children
})
});
});
AccordionContent.displayName = "AccordionContent";
export { GlassAccordion };
//# sourceMappingURL=GlassAccordion.js.map