Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tree-view): add showNode accessor #1844

Merged
merged 1 commit into from
Nov 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 14 additions & 13 deletions COMPONENT_INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -4691,19 +4691,20 @@ export interface TreeNode {

### Props

| Prop name | Required | Kind | Reactive | Type | Default value | Description |
| :------------ | :------- | :-------------------- | :------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
| expandedIds | No | <code>let</code> | Yes | <code>ReadonlyArray<TreeNodeId></code> | <code>[]</code> | Set the node ids to be expanded |
| selectedIds | No | <code>let</code> | Yes | <code>ReadonlyArray<TreeNodeId></code> | <code>[]</code> | Set the node ids to be selected |
| activeId | No | <code>let</code> | Yes | <code>TreeNodeId</code> | <code>""</code> | Set the current active node id<br />Only one node can be active |
| children | No | <code>let</code> | No | <code>Array<TreeNode></code> | <code>[]</code> | Provide an array of children nodes to render |
| size | No | <code>let</code> | No | <code>"default" &#124; "compact"</code> | <code>"default"</code> | Specify the TreeView size |
| labelText | No | <code>let</code> | No | <code>string</code> | <code>""</code> | Specify the label text |
| hideLabel | No | <code>let</code> | No | <code>boolean</code> | <code>false</code> | Set to `true` to visually hide the label text |
| expandAll | No | <code>function</code> | No | <code>() => void</code> | <code>() => { expandedIds = [...nodeIds]; }</code> | Programmatically expand all nodes |
| collapseAll | No | <code>function</code> | No | <code>() => void</code> | <code>() => { expandedIds = []; }</code> | Programmatically collapse all nodes |
| expandNodes | No | <code>function</code> | No | <code>(filterId?: (node: TreeNode) => boolean) => void</code> | <code>() => { expandedIds = nodes .filter( (node) => filterNode(node) &#124;&#124; node.children?.some((child) => filterNode(child) && child.children) ) .map((node) => node.id); }</code> | Programmatically expand a subset of nodes.<br />Expands all nodes if no argument is provided |
| collapseNodes | No | <code>function</code> | No | <code>(filterId?: (node: TreeNode) => boolean) => void</code> | <code>() => { expandedIds = nodes .filter((node) => expandedIds.includes(node.id) && !filterNode(node)) .map((node) => node.id); }</code> | Programmatically collapse a subset of nodes.<br />Collapses all nodes if no argument is provided |
| Prop name | Required | Kind | Reactive | Type | Default value | Description |
| :------------ | :------- | :-------------------- | :------- | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| expandedIds | No | <code>let</code> | Yes | <code>ReadonlyArray<TreeNodeId></code> | <code>[]</code> | Set the node ids to be expanded |
| selectedIds | No | <code>let</code> | Yes | <code>ReadonlyArray<TreeNodeId></code> | <code>[]</code> | Set the node ids to be selected |
| activeId | No | <code>let</code> | Yes | <code>TreeNodeId</code> | <code>""</code> | Set the current active node id<br />Only one node can be active |
| children | No | <code>let</code> | No | <code>Array<TreeNode></code> | <code>[]</code> | Provide an array of children nodes to render |
| size | No | <code>let</code> | No | <code>"default" &#124; "compact"</code> | <code>"default"</code> | Specify the TreeView size |
| labelText | No | <code>let</code> | No | <code>string</code> | <code>""</code> | Specify the label text |
| hideLabel | No | <code>let</code> | No | <code>boolean</code> | <code>false</code> | Set to `true` to visually hide the label text |
| expandAll | No | <code>function</code> | No | <code>() => void</code> | <code>() => { expandedIds = [...nodeIds]; }</code> | Programmatically expand all nodes |
| collapseAll | No | <code>function</code> | No | <code>() => void</code> | <code>() => { expandedIds = []; }</code> | Programmatically collapse all nodes |
| expandNodes | No | <code>function</code> | No | <code>(filterId?: (node: TreeNode) => boolean) => void</code> | <code>() => { expandedIds = nodes .filter( (node) => filterNode(node) &#124;&#124; node.children?.some((child) => filterNode(child) && child.children) ) .map((node) => node.id); }</code> | Programmatically expand a subset of nodes.<br />Expands all nodes if no argument is provided |
| collapseNodes | No | <code>function</code> | No | <code>(filterId?: (node: TreeNode) => boolean) => void</code> | <code>() => { expandedIds = nodes .filter((node) => expandedIds.includes(node.id) && !filterNode(node)) .map((node) => node.id); }</code> | Programmatically collapse a subset of nodes.<br />Collapses all nodes if no argument is provided |
| showNode | No | <code>function</code> | No | <code>(id: TreeNodeId) => void</code> | <code>() => { for (const child of children) { const nodes = findNodeById(child, id); if (nodes) { const ids = nodes.map((node) => node.id); const nodeIds = new Set(ids); expandNodes((node) => nodeIds.has(node.id)); const lastId = ids[ids.length - 1]; activeId = lastId; selectedIds = [lastId]; tick().then(() => { ref?.querySelector(\`[id="${lastId}"]\`)?.focus(); }); // Break out of the loop if the node is found. break; } } }</code> | Programmatically show a node by `id`.<br />The matching node will be expanded, selected, and focused |

### Slots

Expand Down
12 changes: 12 additions & 0 deletions docs/src/COMPONENT_API.json
Original file line number Diff line number Diff line change
Expand Up @@ -14664,6 +14664,18 @@
"isRequired": false,
"constant": false,
"reactive": false
},
{
"name": "showNode",
"kind": "function",
"description": "Programmatically show a node by `id`.\nThe matching node will be expanded, selected, and focused",
"type": "(id: TreeNodeId) => void",
"value": "() => { for (const child of children) { const nodes = findNodeById(child, id); if (nodes) { const ids = nodes.map((node) => node.id); const nodeIds = new Set(ids); expandNodes((node) => nodeIds.has(node.id)); const lastId = ids[ids.length - 1]; activeId = lastId; selectedIds = [lastId]; tick().then(() => { ref?.querySelector(`[id=\"${lastId}\"]`)?.focus(); }); // Break out of the loop if the node is found. break; } } }",
"isFunction": true,
"isFunctionDeclaration": true,
"isRequired": false,
"constant": false,
"reactive": false
}
],
"moduleExports": [],
Expand Down
10 changes: 9 additions & 1 deletion docs/src/pages/components/TreeView.svx
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,12 @@ Use the `TreeView.collapseNodes` method to collapse a subset of nodes.

If no argument is provided, all nodes will be collapsed.

<FileSource src="/framed/TreeView/TreeViewCollapseNodes" />
<FileSource src="/framed/TreeView/TreeViewCollapseNodes" />

## Show a specific node

Use the `TreeView.showNode` method to show a specific node.

If a matching node is found, it will be expanded, selected, and focused.

<FileSource src="/framed/TreeView/TreeViewShowNode" />
49 changes: 49 additions & 0 deletions docs/src/pages/framed/TreeView/TreeViewShowNode.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<script>
import { Button, ButtonSet, TreeView } from "carbon-components-svelte";

const nodeSpark = { id: 3, text: "Apache Spark" };
const nodeBlockchain = { id: 8, text: "IBM Blockchain Platform" };

let treeview = null;
let children = [
{ id: 0, text: "AI / Machine learning" },
{
id: 1,
text: "Analytics",
children: [
{
id: 2,
text: "IBM Analytics Engine",
children: [nodeSpark, { 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" }],
},
];
</script>

<ButtonSet style="margin-bottom: var(--cds-spacing-05)">
{#each [nodeSpark, nodeBlockchain] as { id, text }}
<Button
on:click="{() => {
treeview?.showNode(id);
}}"
>
Show "{text}"
</Button>
{/each}
<Button kind="tertiary" on:click="{treeview.collapseAll}">Collapse all</Button
>
</ButtonSet>

<TreeView
bind:this="{treeview}"
labelText="Cloud Products"
children="{children}"
/>
60 changes: 59 additions & 1 deletion src/TreeView/TreeView.svelte
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
<script context="module">
/**
* Depth-first search to find a node by id; returns an array
* of nodes from the initial node to the matching leaf.
* @param {TreeNode} node
* @param {TreeNodeId} id
* @returns {null | TreeNode[]}
*/
function findNodeById(node, id) {
if (node === null) return null;
if (node.id === id) return [node];
if (!Array.isArray(node.children)) {
return null;
}

for (const child of node.children) {
const nodes = findNodeById(child, id);

if (Array.isArray(nodes)) {
nodes.unshift(node);
return nodes;
}
}

return null;
}
</script>

<script>
/**
* @typedef {string | number} TreeNodeId
Expand Down Expand Up @@ -87,7 +115,37 @@
.map((node) => node.id);
}

import { createEventDispatcher, setContext, onMount } from "svelte";
/**
* Programmatically show a node by `id`.
* The matching node will be expanded, selected, and focused
* @type {(id: TreeNodeId) => void}
*/
export function showNode(id) {
for (const child of children) {
const nodes = findNodeById(child, id);

if (nodes) {
const ids = nodes.map((node) => node.id);
const nodeIds = new Set(ids);

expandNodes((node) => nodeIds.has(node.id));

const lastId = ids[ids.length - 1];

activeId = lastId;
selectedIds = [lastId];

tick().then(() => {
ref?.querySelector(`[id="${lastId}"]`)?.focus();
});

// Break out of the loop if the node is found.
break;
}
}
}

import { createEventDispatcher, setContext, onMount, tick } from "svelte";
import { writable } from "svelte/store";
import TreeViewNodeList from "./TreeViewNodeList.svelte";

Expand Down
1 change: 1 addition & 0 deletions tests/TreeView.test.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
treeview.collapseNodes((node) => {
return node.disabled;
});
treeview.showNode(1);
}
</script>

Expand Down
6 changes: 6 additions & 0 deletions types/TreeView/TreeView.svelte.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,10 @@ export default class TreeView extends SvelteComponentTyped<
* Collapses all nodes if no argument is provided
*/
collapseNodes: (filterId?: (node: TreeNode) => boolean) => void;

/**
* Programmatically show a node by `id`.
* The matching node will be expanded, selected, and focused
*/
showNode: (id: TreeNodeId) => void;
}
Loading