carbon-components-svelte/src/TreeView/TreeViewNodeList.svelte
Eric Liu 6bf72d4602
fix(types): loosen icon prop type to any (#2095)
Fixes https://github.com/carbon-design-system/carbon-icons-svelte/issues/207

`carbon-icons-svelte@13` and `carbon-pictograms-svelte@13` now  
only support TypeScript for Svelte 4/5.

The new `Component` type is incompatible with the `icon` prop in  
`carbon-components-svelte`, causing a type error with Svelte 5, as  
`typeof SvelteComponent` doesn't match the new `Component` type.

Since `Component` isn't available in Svelte 3/4, this PR changes  
the `icon` prop type to `any` for compatibility across Svelte 3, 4, and 5.
2025-02-02 19:49:53 -08:00

179 lines
4.8 KiB
Svelte

<script>
/**
* @typedef {string | number} TreeNodeId
* @typedef {{ id: TreeNodeId; text: string; disabled?: boolean; expanded?: boolean; }} TreeNode
* @slot {{ node: { id: TreeNodeId; text: string; expanded: boolean, leaf: boolean; disabled: boolean; selected: boolean; } }}
*/
/** @type {Array<TreeNode & { nodes?: TreeNode[] }>} */
export let nodes = [];
export let root = false;
/** @type {string | number} */
export let id = "";
export let text = "";
export let disabled = false;
/**
* Specify the icon to render
* @type {any}
*/
export let icon = undefined;
import { afterUpdate, getContext } from "svelte";
import CaretDown from "../icons/CaretDown.svelte";
import TreeViewNode, { computeTreeLeafDepth } from "./TreeViewNode.svelte";
let ref = null;
let refLabel = null;
let prevActiveId = undefined;
const {
activeNodeId,
selectedNodeIds,
expandedNodeIds,
clickNode,
selectNode,
expandNode,
focusNode,
toggleNode,
} = getContext("TreeView");
const offset = () => {
const depth = computeTreeLeafDepth(refLabel);
if (parent) return depth + 1;
if (icon) return depth + 2;
return depth + 2.5;
};
afterUpdate(() => {
if (id === $activeNodeId && prevActiveId !== $activeNodeId) {
if (!$selectedNodeIds.includes(id)) selectNode(node);
}
prevActiveId = $activeNodeId;
});
$: parent = Array.isArray(nodes);
$: node = { id, text, expanded, leaf: !parent };
$: if (refLabel) {
refLabel.style.marginLeft = `-${offset()}rem`;
refLabel.style.paddingLeft = `${offset()}rem`;
}
$: expanded = $expandedNodeIds.includes(id);
</script>
{#if root}
{#each nodes as child (child.id)}
{#if Array.isArray(child.nodes)}
<svelte:self {...child} let:node>
<slot {node} />
</svelte:self>
{:else}
<TreeViewNode leaf {...child} let:node>
<slot {node} />
</TreeViewNode>
{/if}
{/each}
{:else}
{@const selected = $selectedNodeIds.includes(id)}
<!-- svelte-ignore a11y-no-noninteractive-element-to-interactive-role -->
<li
bind:this={ref}
role="treeitem"
{id}
tabindex={disabled ? undefined : -1}
aria-current={id === $activeNodeId || undefined}
aria-selected={disabled ? undefined : selected}
aria-disabled={disabled}
class:bx--tree-node={true}
class:bx--tree-parent-node={true}
class:bx--tree-node--active={id === $activeNodeId}
class:bx--tree-node--selected={selected}
class:bx--tree-node--disabled={disabled}
class:bx--tree-node--with-icon={icon}
aria-expanded={expanded}
on:click|stopPropagation={() => {
if (disabled) return;
clickNode(node);
}}
on:keydown={(e) => {
if (
e.key === "ArrowLeft" ||
e.key === "ArrowRight" ||
e.key === "Enter"
) {
e.stopPropagation();
}
if (parent && e.key === "ArrowLeft") {
expanded = false;
expandNode(node, false);
toggleNode(node);
}
if (parent && e.key === "ArrowRight") {
if (expanded) {
ref.lastChild.firstElementChild?.focus();
} else {
expanded = true;
expandNode(node, true);
toggleNode(node);
}
}
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
if (disabled) return;
expanded = !expanded;
toggleNode(node);
clickNode(node);
expandNode(node, expanded);
ref.focus();
}
}}
on:focus={() => {
focusNode(node);
}}
>
<div class:bx--tree-node__label={true} bind:this={refLabel}>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span
class:bx--tree-parent-node__toggle={true}
{disabled}
on:click={() => {
if (disabled) return;
expanded = !expanded;
expandNode(node, expanded);
toggleNode(node);
}}
>
<CaretDown
class="bx--tree-parent-node__toggle-icon {expanded &&
'bx--tree-parent-node__toggle-icon--expanded'}"
/>
</span>
<span class:bx--tree-node__label__details={true}>
<svelte:component this={icon} class="bx--tree-node__icon" />
<slot node={{ ...node, selected, disabled }} />
</span>
</div>
{#if expanded}
<ul role="group" class:bx--tree-node__children={true}>
{#each nodes as child (child.id)}
{#if Array.isArray(child.nodes)}
<svelte:self {...child} let:node>
<slot {node} />
</svelte:self>
{:else}
<TreeViewNode leaf {...child} let:node>
<slot {node}>{node.text}</slot>
</TreeViewNode>
{/if}
{/each}
</ul>
{/if}
</li>
{/if}