UNPKG

aura-glass

Version:

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

300 lines (297 loc) 9.21 kB
'use client'; import { jsx, jsxs } from 'react/jsx-runtime'; import React, { forwardRef, useRef, useContext, useMemo, useState, useLayoutEffect } from 'react'; import { cn } from '../../lib/utilsComprehensive.js'; import { useReducedMotion } from '../../hooks/useReducedMotion.js'; import { useAnimationContext } from '../../contexts/AnimationContext.js'; import { SpringPresets } from '../../animations/physics/springPhysics.js'; import { TreeViewContext } from './TreeView.js'; import styles from './TreeItem.module.css.js'; // Default icons const DefaultExpandIcon = () => jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: jsx("path", { d: "M8.59 16.59L13.17 12L8.59 7.41L10 6L16 12L10 18L8.59 16.59Z", fill: "currentColor" }) }); const DefaultCollapseIcon = () => jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: jsx("path", { d: "M7.41 8.59L12 13.17L16.59 8.59L18 10L12 16L6 10L7.41 8.59Z", fill: "currentColor" }) }); const DefaultEndIcon = () => jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: jsx("circle", { cx: "12", cy: "12", r: "4", fill: "currentColor" }) }); /** * TreeItem Component Implementation */ function TreeItemComponent(props, ref) { const { nodeId, label, children, className, style, glass: propGlass, color, icon, expandIcon, collapseIcon, endIcon, disabled = false, animationConfig, disableAnimation, motionSensitivity, ...rest } = props; const prefersReducedMotion = useReducedMotion(); const { defaultSpring } = useAnimationContext(); const finalDisableAnimation = disableAnimation ?? prefersReducedMotion; // Refs const contentRef = useRef(null); const childrenRef = useRef(null); // Get TreeView context const treeContext = useContext(TreeViewContext); if (!treeContext) { throw new Error('TreeItem must be used within a TreeView'); } const { expanded, selected, focused, size, disabled: contextDisabled, glass: contextGlass, selectNode, toggleNode, focusNode } = treeContext; // Merge props with context const finalGlass = propGlass !== undefined ? propGlass : contextGlass; const finalDisabled = disabled || contextDisabled; // Check if the node has children const hasChildren = React.Children.count(children) > 0; // Check if the node is expanded const isExpanded = hasChildren && expanded.includes(nodeId); // Check if the node is selected const isSelected = selected.includes(nodeId); // Check if the node is focused const isFocused = focused.includes(nodeId); // Animation Context and Config Calculation useMemo(() => { const baseConfig = SpringPresets.default; let contextConfig = {}; if (typeof defaultSpring === 'string' && defaultSpring in SpringPresets) { contextConfig = SpringPresets[defaultSpring]; } else if (typeof defaultSpring === 'object') { contextConfig = defaultSpring ?? {}; } let propConfig = {}; const propSource = animationConfig; if (typeof propSource === 'string' && propSource in SpringPresets) { propConfig = SpringPresets[propSource]; } else if (typeof propSource === 'object') { propConfig = propSource ?? {}; } // Return merged config return { ...baseConfig, ...contextConfig, ...propConfig }; }, [defaultSpring, animationConfig]); // State for measuring height const [measuredHeight, setMeasuredHeight] = useState('auto'); // UseLayoutEffect to measure height before animation useLayoutEffect(() => { let rafId = null; if (isExpanded && childrenRef.current) { // Use requestAnimationFrame to defer measurement slightly rafId = requestAnimationFrame(() => { if (childrenRef.current) { // Check ref again inside RAF const height = childrenRef.current.scrollHeight; setMeasuredHeight(height); } }); } else { // Set to 0 when collapsed or no children setMeasuredHeight(0); } // Cleanup RAF on effect change or unmount return () => { if (rafId !== null) { cancelAnimationFrame(rafId); } }; }, [isExpanded, children]); // Rerun when expansion state or children change // Define spring targets using useMemo const animationTargets = useMemo(() => ({ // Use measuredHeight, ensuring it's a number for the spring height: isExpanded && typeof measuredHeight === 'number' ? measuredHeight : 0, opacity: isExpanded ? 1 : 0 }), [isExpanded, measuredHeight]); // Simple animation style calculation const animatedStyle = finalDisableAnimation ? { height: animationTargets.height, opacity: animationTargets.opacity, transform: isExpanded ? 'perspective(800px) rotateX(0deg)' : 'perspective(800px) rotateX(-5deg)', transformOrigin: 'top' } : { height: animationTargets.height, opacity: animationTargets.opacity, transform: isExpanded ? 'perspective(800px) rotateX(0deg)' : 'perspective(800px) rotateX(-5deg)', transformOrigin: 'top', transition: 'height 0.3s ease, opacity 0.3s ease, transform 0.28s ease' }; // Handle click event const handleClick = event => { event.stopPropagation(); // Select the node if (!finalDisabled && selectNode) { selectNode(nodeId); } }; // Handle expand/collapse const handleToggle = event => { event.stopPropagation(); // Toggle the node if (!finalDisabled && hasChildren && toggleNode) { toggleNode(nodeId); } }; // Handle keyboard events const handleKeyDown = event => { if (finalDisabled) return; switch (event.key) { case ' ': case 'Enter': event.preventDefault(); if (selectNode) { selectNode(nodeId); } break; case 'ArrowRight': event.preventDefault(); if (hasChildren && !isExpanded && toggleNode) { toggleNode(nodeId); } break; case 'ArrowLeft': event.preventDefault(); if (hasChildren && isExpanded && toggleNode) { toggleNode(nodeId); } break; } }; // Handle focus const handleFocus = event => { if (!finalDisabled && focusNode) { focusNode(nodeId); } }; // Determine which icon to show const renderToggleIcon = () => { if (!hasChildren) { return endIcon || jsx(DefaultEndIcon, {}); } if (isExpanded) { return collapseIcon || jsx(DefaultCollapseIcon, {}); } return expandIcon || jsx(DefaultExpandIcon, {}); }; const sizeClass = size === 'small' && styles.sizeSmall || size === 'large' && styles.sizeLarge || styles.sizeMedium; const contentClassName = cn(styles.content, sizeClass, finalGlass && styles.glass, finalDisabled && styles.disabled, isSelected && styles.selected, isFocused && styles.focused, 'glass-focus glass-touch-target glass-contrast-guard'); const toggleIconClass = cn(styles.icon, hasChildren && styles.toggleIcon, hasChildren && 'glass-focus glass-touch-target'); const iconClass = cn(styles.icon); const labelClass = styles.label; const rootClass = cn(styles.root, className); const itemStyleVars = color ? { '--tree-view-color': color } : undefined; return jsxs("li", { ref: ref, className: rootClass, style: style, role: "treeitem", "aria-expanded": hasChildren ? isExpanded : undefined, "aria-selected": isSelected, ...rest, children: [jsxs("div", { ref: contentRef, onClick: handleClick, onDoubleClick: handleToggle, onKeyDown: handleKeyDown, onFocus: handleFocus, tabIndex: finalDisabled ? -1 : 0, "aria-disabled": finalDisabled, className: contentClassName, style: itemStyleVars, children: [jsx("div", { onClick: hasChildren ? handleToggle : undefined, className: toggleIconClass, role: hasChildren ? 'button' : undefined, "aria-hidden": !hasChildren, children: renderToggleIcon() }), icon && jsx("div", { className: iconClass, children: icon }), jsx("div", { className: labelClass, children: label })] }), hasChildren && jsx("ul", { ref: childrenRef, role: "group", className: styles.children, style: { ...animatedStyle, willChange: 'height, opacity, transform' }, children: children }, `children-${nodeId}`)] }); } /** * TreeItem Component * * A item component for the TreeView. */ const TreeItem = /*#__PURE__*/forwardRef(TreeItemComponent); /** * GlassTreeItem Component * * Glass variant of the TreeItem component. */ const GlassTreeItem = /*#__PURE__*/forwardRef((props, ref) => jsx(TreeItem, { ...props, glass: true, ref: ref })); GlassTreeItem.displayName = 'GlassTreeItem'; export { GlassTreeItem, TreeItem, TreeItem as default }; //# sourceMappingURL=TreeItem.js.map