@furystack/shades-common-components
Version:
Common UI components for FuryStack Shades
132 lines • 6.52 kB
JavaScript
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