UNPKG

@furystack/shades-common-components

Version:

Common UI components for FuryStack Shades

78 lines 4.09 kB
import { createComponent, Shade } from '@furystack/shades'; import { ObservableValue } from '@furystack/utils'; import { cssVariableTheme } from '../../services/css-variable-theme.js'; import { TreeItem } from './tree-item.js'; let nextTreeId = 0; export const Tree = Shade({ customElementName: 'shade-tree', css: { display: 'block', fontFamily: cssVariableTheme.typography.fontFamily, width: '100%', overflow: 'auto', }, render: ({ props, useDisposable, useObservable, useHostProps, useState }) => { const [navSectionId] = useState('navSectionId', String(nextTreeId++)); useDisposable('keydown-handler', () => { const listener = (ev) => { props.treeService.handleKeyDown(ev); if (ev.key === 'Enter' && props.treeService.hasFocus.getValue()) { const focusedItem = props.treeService.focusedItem.getValue(); if (focusedItem && props.onItemActivate) { props.onItemActivate(focusedItem); } } }; window.addEventListener('keydown', listener, true); return { [Symbol.dispose]: () => window.removeEventListener('keydown', listener, true) }; }); if (props.treeService.rootItems.getValue() !== props.rootItems) { props.treeService.rootItems.setValue(props.rootItems); props.treeService.updateFlattenedNodes(); } const treeInstanceId = useDisposable('treeInstanceId', () => ({ value: Math.random().toString(36).slice(2), [Symbol.dispose]() { }, })); useDisposable('clickAway', () => { const listener = (ev) => { const isInside = ev .composedPath() .some((el) => el instanceof HTMLElement && el.dataset.treeInstanceId === treeInstanceId.value); if (!isInside) { props.treeService.hasFocus.setValue(false); } }; window.addEventListener('click', listener, true); return { [Symbol.dispose]: () => window.removeEventListener('click', listener, true) }; }); if (props.onSelectionChange) { const { onSelectionChange } = props; useDisposable('selectionChangeCallback', () => props.treeService.selection.subscribe((newSelection) => { onSelectionChange(newSelection); })); } useHostProps({ 'data-variant': props.variant || undefined, 'data-tree-instance-id': treeInstanceId.value, 'data-nav-section': props.navSection ?? `tree-${navSectionId}`, role: 'tree', 'aria-multiselectable': 'true', onclick: () => props.treeService.hasFocus.setValue(true), onfocusout: (ev) => { const hostEl = ev.currentTarget; if (!ev.relatedTarget || !hostEl.contains(ev.relatedTarget)) { props.treeService.hasFocus.setValue(false); } }, }); const [flattenedNodes] = useObservable('flattenedNodes', props.treeService.flattenedNodes); // eslint-disable-next-line furystack/require-use-observable-for-render -- Used as persistent ref, not reactive state; read and written synchronously in same render cycle const previousItemsRef = useDisposable('previousTreeItems', () => new ObservableValue(new Set())); const previousItems = previousItemsRef.getValue(); const currentItems = new Set(flattenedNodes.map((n) => n.item)); previousItemsRef.setValue(currentItems); return (createComponent(createComponent, null, flattenedNodes.map((nodeInfo) => (createComponent(TreeItem, { item: nodeInfo.item, treeService: props.treeService, nodeInfo: nodeInfo, isNew: !previousItems.has(nodeInfo.item), renderItem: props.renderItem, renderIcon: props.renderIcon, onActivate: props.onItemActivate }))))); }, }); //# sourceMappingURL=tree.js.map