Skip to content

Commit 8ee8f5d

Browse files
committed
feat(tree-view): support links
1 parent 0bce397 commit 8ee8f5d

4 files changed

Lines changed: 331 additions & 42 deletions

File tree

src/TreeView/TreeView.svelte

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@
111111
* @property {any} text
112112
* @property {any} [icon]
113113
* @property {boolean} [disabled] - Whether the node is disabled
114+
* @property {string} [href] - Optional URL the node links to
115+
* @property {string} [target] - Optional link target (e.g., "_blank")
114116
* @property {TreeNode<Id>[]} [nodes]
115117
* @typedef {object} ShowNodeOptions
116118
* @property {boolean} [expand] - Whether to expand the node and its ancestors (default: true)

src/TreeView/TreeViewNode.svelte

Lines changed: 116 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@
4444
function findParentTreeNode(node) {
4545
if (node == null || !(node instanceof HTMLElement)) return null;
4646
if (node.classList.contains("bx--tree-parent-node")) return node;
47+
if (node.classList.contains("bx--tree-node-link-parent")) {
48+
return node.firstElementChild;
49+
}
4750
if (node.classList.contains("bx--tree")) return null;
4851
if (node.parentNode instanceof HTMLElement) {
4952
return findParentTreeNode(node.parentNode);
@@ -66,6 +69,18 @@
6669
export let text = "";
6770
export let disabled = false;
6871
72+
/**
73+
* Specify the URL the TreeNode links to.
74+
* @type {string | undefined}
75+
*/
76+
export let href = undefined;
77+
78+
/**
79+
* Specify the link target.
80+
* @type {string | undefined}
81+
*/
82+
export let target = undefined;
83+
6984
/**
7085
* Specify the icon to render.
7186
* @type {Icon}
@@ -112,47 +127,106 @@
112127
}
113128
</script>
114129

115-
<!-- svelte-ignore a11y-no-noninteractive-element-to-interactive-role -->
116-
<li
117-
bind:this={ref}
118-
role="treeitem"
119-
{id}
120-
tabindex={disabled ? undefined : -1}
121-
aria-current={id === $activeNodeId || undefined}
122-
aria-selected={disabled ? undefined : selected}
123-
aria-disabled={disabled}
124-
class:bx--tree-node={true}
125-
class:bx--tree-leaf-node={true}
126-
class:bx--tree-node--active={id === $activeNodeId}
127-
class:bx--tree-node--selected={selected}
128-
class:bx--tree-node--disabled={disabled}
129-
class:bx--tree-node--with-icon={icon}
130-
on:click|stopPropagation={() => {
131-
if (disabled) return;
132-
clickNode(node);
133-
}}
134-
on:keydown={(e) => {
135-
if (e.key === "ArrowLeft" || e.key === "ArrowRight" || e.key === "Enter") {
136-
e.stopPropagation();
137-
}
138-
139-
if (e.key === "ArrowLeft") {
140-
const parentNode = findParentTreeNode(ref.parentNode);
141-
if (parentNode) parentNode.focus();
142-
}
143-
144-
if (e.key === "Enter" || e.key === " ") {
145-
e.preventDefault();
130+
{#if href}
131+
<li role="none">
132+
<!-- svelte-ignore a11y-no-noninteractive-element-to-interactive-role a11y-role-has-required-aria-props -->
133+
<a
134+
bind:this={ref}
135+
role="treeitem"
136+
{id}
137+
href={disabled ? undefined : href}
138+
target={disabled ? undefined : target}
139+
rel={target === "_blank" ? "noopener noreferrer" : undefined}
140+
tabindex={disabled ? undefined : -1}
141+
aria-current={id === $activeNodeId ? "page" : undefined}
142+
aria-disabled={disabled}
143+
class:bx--tree-node={true}
144+
class:bx--tree-leaf-node={true}
145+
class:bx--tree-node--active={id === $activeNodeId}
146+
class:bx--tree-node--selected={selected}
147+
class:bx--tree-node--disabled={disabled}
148+
class:bx--tree-node--with-icon={icon}
149+
on:click|stopPropagation={() => {
150+
if (disabled) return;
151+
clickNode(node);
152+
}}
153+
on:keydown={(e) => {
154+
if (
155+
e.key === "ArrowLeft" ||
156+
e.key === "ArrowRight" ||
157+
e.key === "Enter"
158+
) {
159+
e.stopPropagation();
160+
}
161+
162+
if (e.key === "ArrowLeft") {
163+
const parentNode = findParentTreeNode(ref.parentNode?.parentNode);
164+
if (parentNode) parentNode.focus();
165+
}
166+
167+
if (e.key === "Enter" || e.key === " ") {
168+
e.preventDefault();
169+
if (disabled) return;
170+
clickNode(node);
171+
}
172+
}}
173+
on:focus={() => {
174+
focusNode(node);
175+
}}
176+
>
177+
<div bind:this={refLabel} class:bx--tree-node__label={true}>
178+
<svelte:component this={icon} class="bx--tree-node__icon" />
179+
<slot {node}> {text} </slot>
180+
</div>
181+
</a>
182+
</li>
183+
{:else}
184+
<!-- svelte-ignore a11y-no-noninteractive-element-to-interactive-role -->
185+
<li
186+
bind:this={ref}
187+
role="treeitem"
188+
{id}
189+
tabindex={disabled ? undefined : -1}
190+
aria-current={id === $activeNodeId || undefined}
191+
aria-selected={disabled ? undefined : selected}
192+
aria-disabled={disabled}
193+
class:bx--tree-node={true}
194+
class:bx--tree-leaf-node={true}
195+
class:bx--tree-node--active={id === $activeNodeId}
196+
class:bx--tree-node--selected={selected}
197+
class:bx--tree-node--disabled={disabled}
198+
class:bx--tree-node--with-icon={icon}
199+
on:click|stopPropagation={() => {
146200
if (disabled) return;
147201
clickNode(node);
148-
}
149-
}}
150-
on:focus={() => {
151-
focusNode(node);
152-
}}
153-
>
154-
<div bind:this={refLabel} class:bx--tree-node__label={true}>
155-
<svelte:component this={icon} class="bx--tree-node__icon" />
156-
<slot {node}> {text} </slot>
157-
</div>
158-
</li>
202+
}}
203+
on:keydown={(e) => {
204+
if (
205+
e.key === "ArrowLeft" ||
206+
e.key === "ArrowRight" ||
207+
e.key === "Enter"
208+
) {
209+
e.stopPropagation();
210+
}
211+
212+
if (e.key === "ArrowLeft") {
213+
const parentNode = findParentTreeNode(ref.parentNode);
214+
if (parentNode) parentNode.focus();
215+
}
216+
217+
if (e.key === "Enter" || e.key === " ") {
218+
e.preventDefault();
219+
if (disabled) return;
220+
clickNode(node);
221+
}
222+
}}
223+
on:focus={() => {
224+
focusNode(node);
225+
}}
226+
>
227+
<div bind:this={refLabel} class:bx--tree-node__label={true}>
228+
<svelte:component this={icon} class="bx--tree-node__icon" />
229+
<slot {node}> {text} </slot>
230+
</div>
231+
</li>
232+
{/if}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<script lang="ts">
2+
import { TreeView } from "carbon-components-svelte";
3+
import type { ComponentProps } from "svelte";
4+
5+
export let nodes: ComponentProps<TreeView>["nodes"] = [
6+
{ id: "link-1", text: "Link Node", href: "/page-1" },
7+
{ id: "link-2", text: "Another Link", href: "/page-2" },
8+
{ id: "plain-1", text: "Plain Node" },
9+
{
10+
id: "link-disabled",
11+
text: "Disabled Link",
12+
href: "/disabled",
13+
disabled: true,
14+
},
15+
{
16+
id: "link-blank",
17+
text: "Blank Target",
18+
href: "/external",
19+
target: "_blank",
20+
},
21+
{
22+
id: "link-self",
23+
text: "Self Target",
24+
href: "/internal",
25+
target: "_self",
26+
},
27+
];
28+
export let activeId: ComponentProps<TreeView>["activeId"] = "";
29+
export let selectedIds: ComponentProps<TreeView>["selectedIds"] = [];
30+
</script>
31+
32+
<TreeView
33+
{nodes}
34+
bind:activeId
35+
bind:selectedIds
36+
labelText="Link Tree"
37+
on:select={({ detail }) => console.log("select", detail)}
38+
on:focus={({ detail }) => console.log("focus", detail)}
39+
/>

0 commit comments

Comments
 (0)