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