carbon-components-svelte/src/TreeView/TreeViewNode.svelte
Eric Liu 6ed4aaa86e
TreeView (#725)
* feat(tree-view): add TreeView

* fix(tree-view): select initial active node, correct typedefs

* docs(tree-view): update examples

* chore(tree-view): add test for types

* docs(tree-view): rename example

* docs(tree-view): improve docs

* docs(tree-view): refine examples

* docs: fix invalid syntax

* chore: rebuild component index/api

* docs(layout): increase height of sidenav menu [ci skip]
2021-07-05 09:11:15 -07:00

125 lines
3.1 KiB
Svelte

<script context="module">
/**
* Computes the depth of a tree leaf node relative to <ul role="tree" />
* @param {HTMLLIElement} node
* @returns {number} depth
*/
export function computeTreeLeafDepth(node) {
let depth = 0;
if (node == null) return depth;
let parentNode = node.parentNode;
while (parentNode != null && parentNode.getAttribute("role") !== "tree") {
parentNode = parentNode.parentNode;
if (parentNode.tagName === "LI") depth++;
}
return depth;
}
/**
* Finds the nearest parent tree node
* @param {HTMLElement} node
* @returns {null | HTMLElement}
*/
function findParentTreeNode(node) {
if (node.classList.contains("bx--tree-parent-node")) return node;
if (node.classList.contains("bx--tree")) return null;
return findParentTreeNode(node.parentNode);
}
</script>
<script>
/**
* @typedef {string | number} TreeNodeId
*/
export let leaf = false;
/** @type {TreeNodeId} */
export let id = "";
export let text = "";
export let disabled = false;
/**
* Specify the icon from `carbon-icons-svelte` to render
* @type {typeof import("carbon-icons-svelte").CarbonIcon}
*/
export let icon = undefined;
import { afterUpdate, getContext } from "svelte";
let ref = null;
let refLabel = null;
let prevActiveId = undefined;
const {
activeNodeId,
selectedNodeIds,
clickNode,
selectNode,
focusNode,
} = getContext("TreeView");
const offset = () =>
computeTreeLeafDepth(refLabel) + (leaf && icon ? 2 : 2.5);
afterUpdate(() => {
if (id === $activeNodeId && prevActiveId !== $activeNodeId) {
if (!$selectedNodeIds.includes(id)) selectNode(node);
}
prevActiveId = $activeNodeId;
});
$: node = { id, text, expanded: false, leaf };
$: if (refLabel) {
refLabel.style.marginLeft = `-${offset()}rem`;
refLabel.style.paddingLeft = `${offset()}rem`;
}
</script>
<li
bind:this="{ref}"
role="treeitem"
id="{id}"
tabindex="{disabled ? undefined : -1}"
aria-current="{id === $activeNodeId || undefined}"
aria-selected="{disabled ? undefined : $selectedNodeIds.includes(id)}"
aria-disabled="{disabled}"
class:bx--tree-node="{true}"
class:bx--tree-leaf-node="{true}"
class:bx--tree-node--active="{id === $activeNodeId}"
class:bx--tree-node--selected="{$selectedNodeIds.includes(id)}"
class:bx--tree-node--disabled="{disabled}"
class:bx--tree-node--with-icon="{icon}"
on:click|stopPropagation="{() => {
if (disabled) return;
clickNode(node);
}}"
on:keydown="{(e) => {
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'Enter') {
e.stopPropagation();
}
if (e.key === 'ArrowLeft') {
const parentNode = findParentTreeNode(ref.parentNode);
if (parentNode) parentNode.focus();
}
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
if (disabled) return;
clickNode(node);
}
}}"
on:focus="{() => {
focusNode(node);
}}"
>
<div bind:this="{refLabel}" class:bx--tree-node__label="{true}">
<svelte:component this="{icon}" class="bx--tree-node__icon" />
{text}
</div>
</li>