UNPKG

@furystack/shades-common-components

Version:

Common UI components for FuryStack Shades

132 lines 6.52 kB
import { createComponent, Shade } from '@furystack/shades'; import { buildTransition, cssVariableTheme } from '../../services/css-variable-theme.js'; import { Icon } from '../icons/icon.js'; import { chevronRight } from '../icons/icon-definitions.js'; const INDENT_PX = 20; const EXPAND_ICON_WIDTH = 20; export const TreeItem = Shade({ customElementName: 'shade-tree-item', css: { display: 'flex', fontFamily: cssVariableTheme.typography.fontFamily, alignItems: 'center', cursor: 'default', userSelect: 'none', padding: `${cssVariableTheme.spacing.xs} ${cssVariableTheme.spacing.sm}`, gap: '6px', transition: buildTransition(['opacity', cssVariableTheme.transitions.duration.fast, 'ease-out'], ['transform', cssVariableTheme.transitions.duration.fast, 'ease-out'], ['background-color', cssVariableTheme.transitions.duration.fast, 'ease'], ['box-shadow', cssVariableTheme.transitions.duration.fast, cssVariableTheme.transitions.easing.easeInOut]), borderLeft: '3px solid transparent', '&[data-animate-in]': { opacity: '0', transform: 'translateY(-6px)', }, '&:not([data-selected]):hover': { backgroundColor: cssVariableTheme.action.hoverBackground, }, '&[data-selected]': { backgroundColor: cssVariableTheme.action.selectedBackground, borderLeft: `3px solid ${cssVariableTheme.palette.primary.main}`, }, '&[data-focused]': { boxShadow: `0 0 0 2px ${cssVariableTheme.palette.primary.main} inset`, }, }, render: ({ props, useObservable, useHostProps, useRef, useState }) => { const { item, treeService, nodeInfo, isNew, renderItem, renderIcon, onActivate } = props; const { level, hasChildren, isExpanded } = nodeInfo; const [selection] = useObservable('selection', treeService.selection); const [focusedItem] = useObservable('focusedItem', treeService.focusedItem); const [isAnimatingIn, setIsAnimatingIn] = useState('isAnimatingIn', isNew); if (isNew) { requestAnimationFrame(() => { requestAnimationFrame(() => setIsAnimatingIn(false)); }); } const isFocused = focusedItem === item; const isSelected = selection.includes(item); useHostProps({ tabIndex: isFocused ? 0 : -1, 'data-spatial-nav-target': '', role: 'treeitem', 'aria-level': (level + 1).toString(), 'aria-selected': isSelected.toString(), ...(hasChildren ? { 'aria-expanded': isExpanded.toString() } : {}), onfocus: () => { if (treeService.focusedItem.getValue() !== item) { treeService.focusedItem.setValue(item); } if (!treeService.hasFocus.getValue()) { treeService.hasFocus.setValue(true); } }, onclick: (ev) => { treeService.handleItemClick(item, ev); }, ondblclick: () => { treeService.handleItemDoubleClick(item); if (!hasChildren) { onActivate?.(item); } }, ...(isAnimatingIn ? { 'data-animate-in': '' } : {}), ...(isSelected ? { 'data-selected': '' } : {}), ...(isFocused ? { 'data-focused': '' } : {}), }); const wrapperRef = useRef('wrapper'); if (isFocused) { queueMicrotask(() => { const el = wrapperRef.current; if (!el) return; const hostEl = el.closest('shade-tree-item'); if (!hostEl) return; if (document.activeElement !== hostEl) { hostEl.focus({ preventScroll: true }); } const scrollContainer = el.closest('shade-tree'); if (scrollContainer) { const containerRect = scrollContainer.getBoundingClientRect(); const itemRect = hostEl.getBoundingClientRect(); const itemTopInContainer = itemRect.top - containerRect.top; const itemBottomInContainer = itemRect.bottom - containerRect.top; if (itemTopInContainer < 0) { scrollContainer.scrollTo({ top: scrollContainer.scrollTop + itemTopInContainer, behavior: 'instant', }); } else if (itemBottomInContainer > scrollContainer.clientHeight) { scrollContainer.scrollTo({ top: scrollContainer.scrollTop + (itemBottomInContainer - scrollContainer.clientHeight), behavior: 'instant', }); } } }); } const state = { isFocused, isSelected, level, hasChildren, isExpanded }; const handleExpandClick = (ev) => { ev.stopPropagation(); treeService.toggleExpanded(item); }; return (createComponent("span", { ref: wrapperRef, style: { display: 'contents' } }, createComponent("span", { style: { width: `${level * INDENT_PX}px`, flexShrink: '0' } }), createComponent("span", { className: "tree-item-expand", style: { width: `${EXPAND_ICON_WIDTH}px`, flexShrink: '0', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: hasChildren ? 'pointer' : 'default', }, onclick: hasChildren ? handleExpandClick : undefined }, hasChildren ? (createComponent("span", { style: { display: 'inline-flex', transition: 'transform 0.2s ease', transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)', } }, createComponent(Icon, { icon: chevronRight, size: 14 }))) : ('')), renderIcon && createComponent("span", { className: "tree-item-icon" }, renderIcon(item, isExpanded)), createComponent("span", { className: "tree-item-content", style: { flex: '1' } }, renderItem(item, state)))); }, }); //# sourceMappingURL=tree-item.js.map