* 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]
This commit is contained in:
Eric Liu 2021-07-05 09:11:15 -07:00 committed by GitHub
commit 6ed4aaa86e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 1176 additions and 4 deletions

View file

@ -1,6 +1,6 @@
# Component Index
> 169 components exported from carbon-components-svelte@0.38.2.
> 170 components exported from carbon-components-svelte@0.38.2.
## Components
@ -171,6 +171,7 @@
- [`TooltipDefinition`](#tooltipdefinition)
- [`TooltipFooter`](#tooltipfooter)
- [`TooltipIcon`](#tooltipicon)
- [`TreeView`](#treeview)
- [`Truncate`](#truncate)
- [`UnorderedList`](#unorderedlist)
@ -4649,6 +4650,48 @@ None.
| mouseleave | forwarded | -- |
| focus | forwarded | -- |
## `TreeView`
### Types
```ts
export type TreeNodeId = string | number;
export interface TreeNode {
id: TreeNodeId;
text: string;
icon?: typeof import("carbon-icons-svelte").CarbonIcon;
disabled?: boolean;
expanded?: boolean;
}
```
### Props
| Prop name | Kind | Reactive | Type | Default value | Description |
| :---------- | :--------------- | :------- | :------------------------------------------------------- | ---------------------- | --------------------------------------------------------------- |
| selectedIds | <code>let</code> | Yes | <code>TreeNodeId[]</code> | <code>[]</code> | Set the node ids to be selected |
| activeId | <code>let</code> | Yes | <code>TreeNodeId</code> | <code>""</code> | Set the current active node id<br />Only one node can be active |
| children | <code>let</code> | No | <code>Array<TreeNode & { children?: TreeNode[] }></code> | <code>[]</code> | Provide an array of children nodes to render |
| size | <code>let</code> | No | <code>"default" &#124; "compact"</code> | <code>"default"</code> | Specify the TreeView size |
| labelText | <code>let</code> | No | <code>string</code> | <code>""</code> | Specify the label text |
| hideLabel | <code>let</code> | No | <code>boolean</code> | <code>false</code> | Set to `true` to visually hide the label text |
### Slots
| Slot name | Default | Props | Fallback |
| :-------- | :------ | :---- | :----------------------- |
| labelText | No | -- | <code>{labelText}</code> |
### Events
| Event name | Type | Detail |
| :--------- | :--------- | :------------------------------------------------------------ |
| select | dispatched | <code>TreeNode & { expanded: boolean; leaf: boolean; }</code> |
| toggle | dispatched | <code>TreeNode & { expanded: boolean; leaf: boolean; }</code> |
| focus | dispatched | <code>TreeNode & { expanded: boolean; leaf: boolean; }</code> |
| keydown | forwarded | -- |
## `Truncate`
### Props

View file

@ -76,5 +76,8 @@ $css--plex: true;
@import "carbon-components/scss/globals/scss/_css--body";
@import "carbon-components/scss/globals/grid/grid";
// Import experimental TreeView styles not included in global styles
@import "carbon-components/src/components/treeview/treeview";
// Import all component styles
@import "carbon-components/scss/globals/scss/styles";

View file

@ -27,6 +27,9 @@ $carbon--theme: $carbon--theme--g10;
@import "carbon-components/scss/globals/scss/_css--body";
@import "carbon-components/scss/globals/grid/grid";
// Import experimental TreeView styles not included in global styles
@import "carbon-components/src/components/treeview/treeview";
// Import all component styles
@import "carbon-components/scss/globals/scss/styles";

View file

@ -27,5 +27,8 @@ $carbon--theme: $carbon--theme--g100;
@import "carbon-components/scss/globals/scss/_css--body";
@import "carbon-components/scss/globals/grid/grid";
// Import experimental TreeView styles not included in global styles
@import "carbon-components/src/components/treeview/treeview";
// Import all component styles
@import "carbon-components/scss/globals/scss/styles";

View file

@ -27,6 +27,9 @@ $carbon--theme: $carbon--theme--g80;
@import "carbon-components/scss/globals/scss/_css--body";
@import "carbon-components/scss/globals/grid/grid";
// Import experimental TreeView styles not included in global styles
@import "carbon-components/src/components/treeview/treeview";
// Import all component styles
@import "carbon-components/scss/globals/scss/styles";

View file

@ -27,5 +27,8 @@ $carbon--theme: $carbon--theme--g90;
@import "carbon-components/scss/globals/scss/_css--body";
@import "carbon-components/scss/globals/grid/grid";
// Import experimental TreeView styles not included in global styles
@import "carbon-components/src/components/treeview/treeview";
// Import all component styles
@import "carbon-components/scss/globals/scss/styles";

View file

@ -27,5 +27,8 @@ $carbon--theme: $carbon--theme--white;
@import "carbon-components/scss/globals/scss/_css--body";
@import "carbon-components/scss/globals/grid/grid";
// Import experimental TreeView styles not included in global styles
@import "carbon-components/src/components/treeview/treeview";
// Import all component styles
@import "carbon-components/scss/globals/scss/styles";

View file

@ -84,6 +84,8 @@
@import "carbon-components/scss/globals/scss/_css--body";
@import "carbon-components/scss/globals/grid/grid";
@import "carbon-components/src/components/treeview/treeview";
// Import all component styles
@import "carbon-components/scss/globals/scss/styles";
</style>

View file

@ -1,5 +1,5 @@
{
"total": 169,
"total": 170,
"components": [
{
"moduleName": "Accordion",
@ -11834,6 +11834,111 @@
"typedefs": [],
"rest_props": { "type": "Element", "name": "button" }
},
{
"moduleName": "TreeView",
"filePath": "src/TreeView/TreeView.svelte",
"props": [
{
"name": "children",
"kind": "let",
"description": "Provide an array of children nodes to render",
"type": "Array<TreeNode & { children?: TreeNode[] }>",
"value": "[]",
"isFunction": false,
"constant": false,
"reactive": false
},
{
"name": "activeId",
"kind": "let",
"description": "Set the current active node id\nOnly one node can be active",
"type": "TreeNodeId",
"value": "\"\"",
"isFunction": false,
"constant": false,
"reactive": true
},
{
"name": "selectedIds",
"kind": "let",
"description": "Set the node ids to be selected",
"type": "TreeNodeId[]",
"value": "[]",
"isFunction": false,
"constant": false,
"reactive": true
},
{
"name": "size",
"kind": "let",
"description": "Specify the TreeView size",
"type": "\"default\" | \"compact\"",
"value": "\"default\"",
"isFunction": false,
"constant": false,
"reactive": false
},
{
"name": "labelText",
"kind": "let",
"description": "Specify the label text",
"type": "string",
"value": "\"\"",
"isFunction": false,
"constant": false,
"reactive": false
},
{
"name": "hideLabel",
"kind": "let",
"description": "Set to `true` to visually hide the label text",
"type": "boolean",
"value": "false",
"isFunction": false,
"constant": false,
"reactive": false
}
],
"slots": [
{
"name": "labelText",
"default": false,
"fallback": "{labelText}",
"slot_props": "{}"
}
],
"events": [
{
"type": "dispatched",
"name": "select",
"detail": "TreeNode & { expanded: boolean; leaf: boolean; }"
},
{
"type": "dispatched",
"name": "toggle",
"detail": "TreeNode & { expanded: boolean; leaf: boolean; }"
},
{
"type": "dispatched",
"name": "focus",
"detail": "TreeNode & { expanded: boolean; leaf: boolean; }"
},
{ "type": "forwarded", "name": "keydown", "element": "ul" }
],
"typedefs": [
{
"type": "string | number",
"name": "TreeNodeId",
"ts": "type TreeNodeId = string | number"
},
{
"type": "{ id: TreeNodeId; text: string; icon?: typeof import(\"carbon-icons-svelte\").CarbonIcon; disabled?: boolean; expanded?: boolean; }",
"name": "TreeNode",
"ts": "interface TreeNode { id: TreeNodeId; text: string; icon?: typeof import(\"carbon-icons-svelte\").CarbonIcon; disabled?: boolean; expanded?: boolean; }"
}
],
"rest_props": { "type": "Element", "name": "ul" }
},
{
"moduleName": "Truncate",
"filePath": "src/Truncate/Truncate.svelte",

View file

@ -20,7 +20,7 @@
import Footer from "../components/Footer.svelte";
const deprecated = ["ToggleSmall", "Icon"];
const new_components = ["ProgressBar", "RecursiveList"];
const new_components = ["ProgressBar", "RecursiveList", "TreeView"];
let isOpen = false;
let isSideNavOpen = true;
@ -264,6 +264,6 @@
}
.bx--side-nav__submenu[aria-expanded="true"] + .bx--side-nav__menu {
max-height: 124rem;
max-height: 132rem;
}
</style>

View file

@ -0,0 +1,48 @@
<script>
import { InlineNotification } from "carbon-components-svelte";
import Preview from "../../components/Preview.svelte";
</script>
### Default
The `children` prop accepts an array of child nodes. Each node should contain `id` and `text` properties.
Optional properties include `disabled`, `expanded`, `icon`, and `children`.
A parent node contains `children` while a leaf node does not.
<InlineNotification svx-ignore title="Note:" kind="info" hideCloseButton>
<div class="body-short-01">Every node must have a unique id.</div>
</InlineNotification>
<FileSource src="/framed/TreeView/TreeView" />
### Initial active node
The active node can be set through `activeId`.
<FileSource src="/framed/TreeView/TreeViewActive" />
### Compact size
Set `size` to `"compact"` to use the compact variant.
<FileSource src="/framed/TreeView/TreeViewCompact" />
### With icons
To render a node with an icon, define an `icon` property with a Carbon Svelte icon as its value.
<FileSource src="/framed/TreeView/TreeViewIcons" />
### Initial expanded nodes
Set `expanded` to `true` on nodes that should be expanded by default.
<FileSource src="/framed/TreeView/TreeViewExpanded" />
### Initial multiple selected nodes
Initial multiple selected nodes can be set using `selectedIds`.
<FileSource src="/framed/TreeView/TreeViewMultiselect" />

View file

@ -0,0 +1,65 @@
<script>
import { TreeView } from "carbon-components-svelte";
let activeId = "";
let selectedIds = [];
let children = [
{ id: 0, text: "AI / Machine learning" },
{
id: 1,
text: "Analytics",
children: [
{
id: 2,
text: "IBM Analytics Engine",
children: [
{ id: 3, text: "Apache Spark" },
{ id: 4, text: "Hadoop" },
],
},
{ id: 5, text: "IBM Cloud SQL Query" },
{ id: 6, text: "IBM Db2 Warehouse on Cloud" },
],
},
{
id: 7,
text: "Blockchain",
children: [{ id: 8, text: "IBM Blockchain Platform" }],
},
{
id: 9,
text: "Databases",
children: [
{ id: 10, text: "IBM Cloud Databases for Elasticsearch" },
{ id: 11, text: "IBM Cloud Databases for Enterprise DB" },
{ id: 12, text: "IBM Cloud Databases for MongoDB" },
{ id: 13, text: "IBM Cloud Databases for PostgreSQL" },
],
},
{
id: 14,
text: "Integration",
disabled: true,
children: [{ id: 15, text: "IBM API Connect", disabled: true }],
},
];
</script>
<TreeView
labelText="Cloud Products"
children="{children}"
bind:activeId
bind:selectedIds
on:select="{({ detail }) => console.log('select', detail)}"
on:toggle="{({ detail }) => console.log('toggle', detail)}"
on:focus="{({ detail }) => console.log('focus', detail)}"
/>
<div>Active node id: {activeId}</div>
<div>Selected ids: {JSON.stringify(selectedIds)}</div>
<style>
div {
margin-top: var(--cds-spacing-05);
}
</style>

View file

@ -0,0 +1,65 @@
<script>
import { TreeView } from "carbon-components-svelte";
let activeId = 0;
let selectedIds = [];
let children = [
{ id: 0, text: "AI / Machine learning" },
{
id: 1,
text: "Analytics",
children: [
{
id: 2,
text: "IBM Analytics Engine",
children: [
{ id: 3, text: "Apache Spark" },
{ id: 4, text: "Hadoop" },
],
},
{ id: 5, text: "IBM Cloud SQL Query" },
{ id: 6, text: "IBM Db2 Warehouse on Cloud" },
],
},
{
id: 7,
text: "Blockchain",
children: [{ id: 8, text: "IBM Blockchain Platform" }],
},
{
id: 9,
text: "Databases",
children: [
{ id: 10, text: "IBM Cloud Databases for Elasticsearch" },
{ id: 11, text: "IBM Cloud Databases for Enterprise DB" },
{ id: 12, text: "IBM Cloud Databases for MongoDB" },
{ id: 13, text: "IBM Cloud Databases for PostgreSQL" },
],
},
{
id: 14,
text: "Integration",
disabled: true,
children: [{ id: 15, text: "IBM API Connect", disabled: true }],
},
];
</script>
<TreeView
labelText="Cloud Products"
children="{children}"
bind:activeId
bind:selectedIds
on:select="{({ detail }) => console.log('select', detail)}"
on:toggle="{({ detail }) => console.log('toggle', detail)}"
on:focus="{({ detail }) => console.log('focus', detail)}"
/>
<div>Active node id: {activeId}</div>
<div>Selected ids: {JSON.stringify(selectedIds)}</div>
<style>
div {
margin-top: var(--cds-spacing-05);
}
</style>

View file

@ -0,0 +1,66 @@
<script>
import { TreeView } from "carbon-components-svelte";
let activeId = 0;
let selectedIds = [];
let children = [
{ id: 0, text: "AI / Machine learning" },
{
id: 1,
text: "Analytics",
children: [
{
id: 2,
text: "IBM Analytics Engine",
children: [
{ id: 3, text: "Apache Spark" },
{ id: 4, text: "Hadoop" },
],
},
{ id: 5, text: "IBM Cloud SQL Query" },
{ id: 6, text: "IBM Db2 Warehouse on Cloud" },
],
},
{
id: 7,
text: "Blockchain",
children: [{ id: 8, text: "IBM Blockchain Platform" }],
},
{
id: 9,
text: "Databases",
children: [
{ id: 10, text: "IBM Cloud Databases for Elasticsearch" },
{ id: 11, text: "IBM Cloud Databases for Enterprise DB" },
{ id: 12, text: "IBM Cloud Databases for MongoDB" },
{ id: 13, text: "IBM Cloud Databases for PostgreSQL" },
],
},
{
id: 14,
text: "Integration",
disabled: true,
children: [{ id: 15, text: "IBM API Connect", disabled: true }],
},
];
</script>
<TreeView
size="compact"
labelText="Cloud Products"
children="{children}"
bind:activeId
bind:selectedIds
on:select="{({ detail }) => console.log('select', detail)}"
on:toggle="{({ detail }) => console.log('toggle', detail)}"
on:focus="{({ detail }) => console.log('focus', detail)}"
/>
<div>Active node id: {activeId}</div>
<div>Selected ids: {JSON.stringify(selectedIds)}</div>
<style>
div {
margin-top: var(--cds-spacing-05);
}
</style>

View file

@ -0,0 +1,67 @@
<script>
import { TreeView } from "carbon-components-svelte";
let activeId = 1;
let selectedIds = [];
let children = [
{ id: 0, text: "AI / Machine learning" },
{
id: 1,
text: "Analytics",
expanded: true,
children: [
{
id: 2,
text: "IBM Analytics Engine",
expanded: true,
children: [
{ id: 3, text: "Apache Spark" },
{ id: 4, text: "Hadoop" },
],
},
{ id: 5, text: "IBM Cloud SQL Query" },
{ id: 6, text: "IBM Db2 Warehouse on Cloud" },
],
},
{
id: 7,
text: "Blockchain",
children: [{ id: 8, text: "IBM Blockchain Platform" }],
},
{
id: 9,
text: "Databases",
children: [
{ id: 10, text: "IBM Cloud Databases for Elasticsearch" },
{ id: 11, text: "IBM Cloud Databases for Enterprise DB" },
{ id: 12, text: "IBM Cloud Databases for MongoDB" },
{ id: 13, text: "IBM Cloud Databases for PostgreSQL" },
],
},
{
id: 14,
text: "Integration",
disabled: true,
children: [{ id: 15, text: "IBM API Connect", disabled: true }],
},
];
</script>
<TreeView
labelText="Cloud Products"
children="{children}"
bind:activeId
bind:selectedIds
on:select="{({ detail }) => console.log('select', detail)}"
on:toggle="{({ detail }) => console.log('toggle', detail)}"
on:focus="{({ detail }) => console.log('focus', detail)}"
/>
<div>Active node id: {activeId}</div>
<div>Selected ids: {JSON.stringify(selectedIds)}</div>
<style>
div {
margin-top: var(--cds-spacing-05);
}
</style>

View file

@ -0,0 +1,98 @@
<script>
import { TreeView } from "carbon-components-svelte";
import WatsonMachineLearning16 from "carbon-icons-svelte/lib/WatsonMachineLearning16";
import Analytics16 from "carbon-icons-svelte/lib/Analytics16";
import Blockchain16 from "carbon-icons-svelte/lib/Blockchain16";
import DataBase16 from "carbon-icons-svelte/lib/DataBase16";
import SignalStrength16 from "carbon-icons-svelte/lib/SignalStrength16";
let activeId = 1;
let selectedIds = [];
let children = [
{ id: 0, text: "AI / Machine learning", icon: WatsonMachineLearning16 },
{
id: 1,
text: "Analytics",
icon: Analytics16,
expanded: true,
children: [
{
id: 2,
text: "IBM Analytics Engine",
icon: Analytics16,
expanded: true,
children: [
{ id: 3, text: "Apache Spark", icon: Analytics16 },
{ id: 4, text: "Hadoop", icon: Analytics16 },
],
},
{ id: 5, text: "IBM Cloud SQL Query", icon: Analytics16 },
{ id: 6, text: "IBM Db2 Warehouse on Cloud", icon: Analytics16 },
],
},
{
id: 7,
text: "Blockchain",
icon: Blockchain16,
children: [
{ id: 8, text: "IBM Blockchain Platform", icon: Blockchain16 },
],
},
{
id: 9,
text: "Databases",
icon: DataBase16,
children: [
{
id: 10,
text: "IBM Cloud Databases for Elasticsearch",
icon: DataBase16,
},
{
id: 11,
text: "IBM Cloud Databases for Enterprise DB",
icon: DataBase16,
},
{ id: 12, text: "IBM Cloud Databases for MongoDB", icon: DataBase16 },
{
id: 13,
text: "IBM Cloud Databases for PostgreSQL",
icon: DataBase16,
},
],
},
{
id: 14,
text: "Integration",
icon: SignalStrength16,
disabled: true,
children: [
{
id: 15,
text: "IBM API Connect",
icon: SignalStrength16,
disabled: true,
},
],
},
];
</script>
<TreeView
labelText="Cloud Products"
children="{children}"
bind:activeId
bind:selectedIds
on:select="{({ detail }) => console.log('select', detail)}"
on:toggle="{({ detail }) => console.log('toggle', detail)}"
on:focus="{({ detail }) => console.log('focus', detail)}"
/>
<div>Active node id: {activeId}</div>
<div>Selected ids: {JSON.stringify(selectedIds)}</div>
<style>
div {
margin-top: var(--cds-spacing-05);
}
</style>

View file

@ -0,0 +1,65 @@
<script>
import { TreeView } from "carbon-components-svelte";
let activeId = 0;
let selectedIds = [0, 7, 9];
let children = [
{ id: 0, text: "AI / Machine learning" },
{
id: 1,
text: "Analytics",
children: [
{
id: 2,
text: "IBM Analytics Engine",
children: [
{ id: 3, text: "Apache Spark" },
{ id: 4, text: "Hadoop" },
],
},
{ id: 5, text: "IBM Cloud SQL Query" },
{ id: 6, text: "IBM Db2 Warehouse on Cloud" },
],
},
{
id: 7,
text: "Blockchain",
children: [{ id: 8, text: "IBM Blockchain Platform" }],
},
{
id: 9,
text: "Databases",
children: [
{ id: 10, text: "IBM Cloud Databases for Elasticsearch" },
{ id: 11, text: "IBM Cloud Databases for Enterprise DB" },
{ id: 12, text: "IBM Cloud Databases for MongoDB" },
{ id: 13, text: "IBM Cloud Databases for PostgreSQL" },
],
},
{
id: 14,
text: "Integration",
disabled: true,
children: [{ id: 15, text: "IBM API Connect", disabled: true }],
},
];
</script>
<TreeView
labelText="Cloud Products"
children="{children}"
bind:activeId
bind:selectedIds
on:select="{({ detail }) => console.log('select', detail)}"
on:toggle="{({ detail }) => console.log('toggle', detail)}"
on:focus="{({ detail }) => console.log('focus', detail)}"
/>
<div>Active node id: {activeId}</div>
<div>Selected ids: {JSON.stringify(selectedIds)}</div>
<style>
div {
margin-top: var(--cds-spacing-05);
}
</style>

View file

@ -0,0 +1,120 @@
<script>
// TODO: add function to programmatically expand/collapse parent nodes
/**
* @typedef {string | number} TreeNodeId
* @typedef {{ id: TreeNodeId; text: string; icon?: typeof import("carbon-icons-svelte").CarbonIcon; disabled?: boolean; expanded?: boolean; }} TreeNode
* @event {TreeNode & { expanded: boolean; leaf: boolean; }} select
* @event {TreeNode & { expanded: boolean; leaf: boolean; }} toggle
* @event {TreeNode & { expanded: boolean; leaf: boolean; }} focus
*/
/**
* Provide an array of children nodes to render
* @type {Array<TreeNode & { children?: TreeNode[] }>}
*/
export let children = [];
/**
* Set the current active node id
* Only one node can be active
* @type {TreeNodeId}
*/
export let activeId = "";
/**
* Set the node ids to be selected
* @type {TreeNodeId[]}
*/
export let selectedIds = [];
/**
* Specify the TreeView size
* @type {"default" | "compact"}
*/
export let size = "default";
/** Specify the label text */
export let labelText = "";
/** Set to `true` to visually hide the label text */
export let hideLabel = false;
import { createEventDispatcher, setContext } from "svelte";
import { writable } from "svelte/store";
import TreeViewNodeList from "./TreeViewNodeList.svelte";
const dispatch = createEventDispatcher();
const labelId = `label-${Math.random().toString(36)}`;
const activeNodeId = writable(activeId);
const selectedNodeIds = writable(selectedIds);
let ref = null;
let treeWalker = null;
setContext("TreeView", {
activeNodeId,
selectedNodeIds,
clickNode: (node) => {
activeId = node.id;
selectedIds = [node.id];
dispatch("select", node);
},
selectNode: (node) => {
selectedIds = [node.id];
},
focusNode: (node) => dispatch("focus", node),
toggleNode: (node) => dispatch("toggle", node),
});
function handleKeyDown(e) {
if (e.key === "ArrowUp" || e.key === "ArrowDown") e.preventDefault();
treeWalker.currentNode = e.target;
let node = null;
if (e.key === "ArrowUp") node = treeWalker.previousNode();
if (e.key === "ArrowDown") node = treeWalker.nextNode();
if (node && node !== e.target) {
node.tabIndex = "0";
node.focus();
}
}
$: activeNodeId.set(activeId);
$: selectedNodeIds.set(selectedIds);
$: if (ref) {
treeWalker = document.createTreeWalker(ref, NodeFilter.SHOW_ELEMENT, {
acceptNode: (node) => {
if (node.classList.contains("bx--tree-node--disabled"))
return NodeFilter.FILTER_REJECT;
if (node.matches("li.bx--tree-node")) return NodeFilter.FILTER_ACCEPT;
return NodeFilter.FILTER_SKIP;
},
});
}
</script>
{#if !hideLabel}
<!-- svelte-ignore a11y-label-has-associated-control -->
<label id="{labelId}" class:bx--label="{true}">
<slot name="labelText">{labelText}</slot>
</label>
{/if}
<ul
{...$$restProps}
role="tree"
bind:this="{ref}"
class:bx--tree="{true}"
class:bx--tree--default="{size === 'default'}"
class:bx--tree--compact="{size === 'compact'}"
aria-label="{hideLabel ? labelText : undefined}"
aria-labelledby="{!hideLabel ? labelId : undefined}"
aria-multiselectable="{selectedIds.length > 1 || undefined}"
on:keydown
on:keydown|stopPropagation="{handleKeyDown}"
>
<TreeViewNodeList root children="{children}" />
</ul>

View file

@ -0,0 +1,125 @@
<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>

View file

@ -0,0 +1,160 @@
<script>
/**
* @typedef {string | number} TreeNodeId
* @typedef {{ id: TreeNodeId; text: string; disabled?: boolean; expanded?: boolean; }} TreeNode
*/
/** @type {Array<TreeNode & { children?: TreeNode[] }>} */
export let children = [];
export let expanded = false;
export let root = false;
/** @type {string | number} */
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";
import CaretDown16 from "carbon-icons-svelte/lib/CaretDown16/CaretDown16.svelte";
import TreeViewNode, { computeTreeLeafDepth } from "./TreeViewNode.svelte";
let ref = null;
let refLabel = null;
let prevActiveId = undefined;
const {
activeNodeId,
selectedNodeIds,
clickNode,
selectNode,
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(children);
$: node = { id, text, expanded, leaf: !parent };
$: if (refLabel) {
refLabel.style.marginLeft = `-${offset()}rem`;
refLabel.style.paddingLeft = `${offset()}rem`;
}
</script>
{#if root}
{#each children as child (child.id)}
{#if Array.isArray(child.children)}
<svelte:self {...child} />
{:else}
<TreeViewNode leaf {...child} />
{/if}
{/each}
{:else}
<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-parent-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}"
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;
toggleNode(node);
}
if (parent && e.key === 'ArrowRight') {
if (expanded) {
ref.lastChild.firstChild.focus();
} else {
expanded = true;
toggleNode(node);
}
}
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
if (disabled) return;
expanded = !expanded;
toggleNode(node);
clickNode(node);
ref.focus();
}
}}"
on:focus="{() => {
focusNode(node);
}}"
>
<div class:bx--tree-node__label="{true}" bind:this="{refLabel}">
<span
class:bx--tree-parent-node__toggle="{true}"
disabled="{disabled}"
on:click="{() => {
if (disabled) return;
expanded = !expanded;
toggleNode(node);
}}"
>
<CaretDown16
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" />
{text}
</span>
</div>
{#if expanded}
<ul role="group" class:bx--tree-node__children="{true}">
{#each children as child (child.id)}
{#if Array.isArray(child.children)}
<svelte:self {...child} />
{:else}
<TreeViewNode leaf {...child} />
{/if}
{/each}
</ul>
{/if}
</li>
{/if}

1
src/TreeView/index.js Normal file
View file

@ -0,0 +1 @@
export { default as TreeView } from "./TreeView.svelte";

View file

@ -126,6 +126,7 @@ export { ToggleSmall, ToggleSmallSkeleton } from "./ToggleSmall";
export { Tooltip, TooltipFooter } from "./Tooltip";
export { TooltipDefinition } from "./TooltipDefinition";
export { TooltipIcon } from "./TooltipIcon";
export { TreeView } from "./TreeView";
export { Truncate } from "./Truncate";
export {
Header,

View file

@ -0,0 +1,59 @@
<script lang="ts">
import { TreeView } from "../types";
import type { TreeNodeId } from "../types/TreeView/TreeView";
import Analytics16 from "carbon-icons-svelte/lib/Analytics16";
let activeId: TreeNodeId = "";
let selectedIds = [];
let children = [
{ id: 0, text: "AI / Machine learning", icon: Analytics16 },
{
id: 1,
text: "Analytics",
children: [
{
id: 2,
text: "IBM Analytics Engine",
children: [
{ id: 3, text: "Apache Spark" },
{ id: 4, text: "Hadoop" },
],
},
{ id: 5, text: "IBM Cloud SQL Query" },
{ id: 6, text: "IBM Db2 Warehouse on Cloud" },
],
},
{
id: 7,
text: "Blockchain",
children: [{ id: 8, text: "IBM Blockchain Platform" }],
},
{
id: 9,
text: "Databases",
children: [
{ id: 10, text: "IBM Cloud Databases for Elasticsearch" },
{ id: 11, text: "IBM Cloud Databases for Enterprise DB" },
{ id: 12, text: "IBM Cloud Databases for MongoDB" },
{ id: 13, text: "IBM Cloud Databases for PostgreSQL" },
],
},
{
id: 14,
text: "Integration",
disabled: true,
children: [{ id: 15, text: "IBM API Connect", disabled: true }],
},
];
</script>
<TreeView
size="compact"
labelText="Cloud Products"
children="{children}"
bind:activeId
bind:selectedIds
on:select="{({ detail }) => console.log('select', detail)}"
on:toggle="{({ detail }) => console.log('toggle', detail)}"
on:focus="{({ detail }) => console.log('focus', detail)}"
/>

63
types/TreeView/TreeView.d.ts vendored Normal file
View file

@ -0,0 +1,63 @@
/// <reference types="svelte" />
import { SvelteComponentTyped } from "svelte";
export type TreeNodeId = string | number;
export interface TreeNode {
id: TreeNodeId;
text: string;
icon?: typeof import("carbon-icons-svelte").CarbonIcon;
disabled?: boolean;
expanded?: boolean;
}
export interface TreeViewProps
extends svelte.JSX.HTMLAttributes<HTMLElementTagNameMap["ul"]> {
/**
* Provide an array of children nodes to render
* @default []
*/
children?: Array<TreeNode & { children?: TreeNode[] }>;
/**
* Set the current active node id
* Only one node can be active
* @default ""
*/
activeId?: TreeNodeId;
/**
* Set the node ids to be selected
* @default []
*/
selectedIds?: TreeNodeId[];
/**
* Specify the TreeView size
* @default "default"
*/
size?: "default" | "compact";
/**
* Specify the label text
* @default ""
*/
labelText?: string;
/**
* Set to `true` to visually hide the label text
* @default false
*/
hideLabel?: boolean;
}
export default class TreeView extends SvelteComponentTyped<
TreeViewProps,
{
select: CustomEvent<TreeNode & { expanded: boolean; leaf: boolean }>;
toggle: CustomEvent<TreeNode & { expanded: boolean; leaf: boolean }>;
focus: CustomEvent<TreeNode & { expanded: boolean; leaf: boolean }>;
keydown: WindowEventMap["keydown"];
},
{ labelText: {} }
> {}

1
types/index.d.ts vendored
View file

@ -144,6 +144,7 @@ export { default as Tooltip } from "./Tooltip/Tooltip";
export { default as TooltipFooter } from "./Tooltip/TooltipFooter";
export { default as TooltipDefinition } from "./TooltipDefinition/TooltipDefinition";
export { default as TooltipIcon } from "./TooltipIcon/TooltipIcon";
export { default as TreeView } from "./TreeView/TreeView";
export { default as Truncate } from "./Truncate/Truncate";
export { default as Header } from "./UIShell/GlobalHeader/Header";
export { default as HeaderAction } from "./UIShell/GlobalHeader/HeaderAction";