Skip to content

Commit

Permalink
refactor: treeview component
Browse files Browse the repository at this point in the history
  • Loading branch information
segunadebayo committed Oct 28, 2024
1 parent 01b048b commit a2af4ad
Show file tree
Hide file tree
Showing 34 changed files with 1,839 additions and 1,357 deletions.
8 changes: 8 additions & 0 deletions .changeset/empty-shirts-approve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@zag-js/collection": minor
"@zag-js/dom-query": minor
"@zag-js/tree-view": minor
"@zag-js/utils": minor
---

Refactor treeview to use the new tree collection for better rendering and logic management.
8 changes: 4 additions & 4 deletions e2e/models/tree-view.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ export class TreeViewModel extends Model {
return this.page.getByRole("treeitem", { name })
}

private branch(name: string) {
return this.page.locator(`[role=treeitem][data-branch="${name}"]`)
private branch(value: string) {
return this.page.locator(`[data-part=branch][data-value="${value}"]`)
}

private branchTrigger(name: string) {
return this.page.locator(`[role=button][data-branch="${name}"]`)
private branchTrigger(value: string) {
return this.page.locator(`[data-part=branch-control][data-value="${value}"]`)
}

private button(name: string) {
Expand Down
6 changes: 0 additions & 6 deletions e2e/tree-view.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,6 @@ test.describe("tree view / basic", () => {
await I.seeItemIsTabbable("panda.config.ts")
})

test("Interaction outside should reset focused node", async ({ page }) => {
await I.focusItem("panda.config.ts")
await page.click("text=My Documents")
await I.seeBranchIsTabbable("node_modules")
})

test("expand/collapse all button", async () => {
await I.clickButton("Expand all")
await I.seeBranchIsExpanded(["node_modules", "src", "node_modules/@types"])
Expand Down
145 changes: 99 additions & 46 deletions examples/next-ts/pages/tree-view.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,100 @@
import { normalizeProps, useMachine } from "@zag-js/react"
import { treeviewControls } from "@zag-js/shared"
import * as tree from "@zag-js/tree-view"
import { FileIcon, FolderIcon, ChevronRightIcon } from "lucide-react"
import { useId } from "react"
import { StateVisualizer } from "../components/state-visualizer"
import { Toolbar } from "../components/toolbar"
import { useControls } from "../hooks/use-controls"

interface Node {
id: string
name: string
children?: Node[]
}

const collection = tree.collection<Node>({
nodeToValue: (node) => node.id,
nodeToString: (node) => node.name,
rootNode: {
id: "ROOT",
name: "",
children: [
{
id: "node_modules",
name: "node_modules",
children: [
{ id: "node_modules/zag-js", name: "zag-js" },
{ id: "node_modules/pandacss", name: "panda" },
{
id: "node_modules/@types",
name: "@types",
children: [
{ id: "node_modules/@types/react", name: "react" },
{ id: "node_modules/@types/react-dom", name: "react-dom" },
],
},
],
},
{
id: "src",
name: "src",
children: [
{ id: "src/app.tsx", name: "app.tsx" },
{ id: "src/index.ts", name: "index.ts" },
],
},
{ id: "panda.config", name: "panda.config.ts" },
{ id: "package.json", name: "package.json" },
{ id: "renovate.json", name: "renovate.json" },
{ id: "readme.md", name: "README.md" },
],
},
})

interface TreeNodeProps {
node: Node
indexPath: number[]
api: tree.Api
}

const TreeNode = (props: TreeNodeProps): JSX.Element => {
const { node, indexPath, api } = props

const nodeProps = { indexPath, node }
const nodeState = api.getNodeState(nodeProps)

if (nodeState.isBranch) {
return (
<div {...api.getBranchProps(nodeProps)}>
<div {...api.getBranchControlProps(nodeProps)}>
<FolderIcon />
<span {...api.getBranchTextProps(nodeProps)}>{node.name}</span>
<span {...api.getBranchIndicatorProps(nodeProps)}>
<ChevronRightIcon />
</span>
</div>
<div {...api.getBranchContentProps(nodeProps)}>
<div {...api.getBranchIndentGuideProps(nodeProps)} />
{node.children?.map((childNode, index) => (
<TreeNode key={childNode.id} node={childNode} indexPath={[...indexPath, index]} api={api} />
))}
</div>
</div>
)
}

return (
<div {...api.getItemProps(nodeProps)}>
<FileIcon /> {node.name}
</div>
)
}

export default function Page() {
const controls = useControls(treeviewControls)

const [state, send] = useMachine(tree.machine({ id: useId() }), {
const [state, send] = useMachine(tree.machine({ id: useId(), collection }), {
context: controls.context,
})

Expand All @@ -20,58 +105,26 @@ export default function Page() {
<main className="tree-view">
<div {...api.getRootProps()}>
<h3 {...api.getLabelProps()}>My Documents</h3>
<div>
<div style={{ display: "flex", gap: "10px" }}>
<button onClick={() => api.collapse()}>Collapse All</button>
<button onClick={() => api.expand()}>Expand All</button>
<span> - </span>
<button onClick={() => api.select()}>Select All</button>
<button onClick={() => api.deselect()}>Deselect All</button>
{controls.context.selectionMode === "multiple" && (
<>
<button onClick={() => api.select()}>Select All</button>
<button onClick={() => api.deselect()}>Deselect All</button>
</>
)}
</div>
<div {...api.getTreeProps()}>
{collection.rootNode.children?.map((node, index) => (
<TreeNode key={node.id} node={node} indexPath={[index]} api={api} />
))}
</div>

<ul {...api.getTreeProps()}>
<li {...api.getBranchProps({ value: "node_modules", depth: 1 })}>
<div {...api.getBranchControlProps({ value: "node_modules", depth: 1 })}>
<span {...api.getBranchTextProps({ value: "node_modules", depth: 1 })}> 📂 node_modules</span>
</div>

<ul {...api.getBranchContentProps({ value: "node_modules", depth: 1 })}>
<li {...api.getItemProps({ value: "node_modules/zag-js", depth: 2 })}>📄 zag-js</li>
<li {...api.getItemProps({ value: "node_modules/pandacss", depth: 2 })}>📄 panda</li>

<li {...api.getBranchProps({ value: "node_modules/@types", depth: 2 })}>
<div {...api.getBranchControlProps({ value: "node_modules/@types", depth: 2 })}>
<span {...api.getBranchTextProps({ value: "node_modules/@types", depth: 2 })}> 📂 @types</span>
</div>

<ul {...api.getBranchContentProps({ value: "node_modules/@types", depth: 2 })}>
<li {...api.getItemProps({ value: "node_modules/@types/react", depth: 3 })}>📄 react</li>
<li {...api.getItemProps({ value: "node_modules/@types/react-dom", depth: 3 })}>📄 react-dom</li>
</ul>
</li>
</ul>
</li>

<li {...api.getBranchProps({ value: "src", depth: 1 })}>
<div {...api.getBranchControlProps({ value: "src", depth: 1 })}>
<span {...api.getBranchTextProps({ value: "src", depth: 1 })}> 📂 src</span>
</div>

<ul {...api.getBranchContentProps({ value: "src", depth: 1 })}>
<li {...api.getItemProps({ value: "src/app.tsx", depth: 2 })}>📄 app.tsx</li>
<li {...api.getItemProps({ value: "src/index.ts", depth: 2 })}>📄 index.ts</li>
</ul>
</li>

<li {...api.getItemProps({ value: "panda.config", depth: 1 })}>📄 panda.config.ts</li>
<li {...api.getItemProps({ value: "package.json", depth: 1 })}>📄 package.json</li>
<li {...api.getItemProps({ value: "renovate.json", depth: 1 })}>📄 renovate.json</li>
<li {...api.getItemProps({ value: "readme.md", depth: 1 })}>📄 README.md</li>
</ul>
</div>
</main>

<Toolbar controls={controls.ui}>
<StateVisualizer state={state} />
<StateVisualizer state={state} omit={["collection"]} />
</Toolbar>
</>
)
Expand Down
52 changes: 52 additions & 0 deletions examples/nuxt-ts/components/TreeNode.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<script setup lang="ts">
import { FileIcon, FolderIcon, ChevronRightIcon } from "lucide-vue-next"
import * as tree from "@zag-js/tree-view"
interface Node {
id: string
name: string
children?: Node[]
}
interface Props {
node: Node
indexPath: number[]
api: tree.Api
}
const props = defineProps<Props>()
const nodeProps = computed(() => ({
indexPath: props.indexPath,
node: props.node,
}))
const nodeState = computed(() => props.api.getNodeState(nodeProps.value))
</script>

<template>
<template v-if="nodeState.isBranch">
<div v-bind="api.getBranchProps(nodeProps)">
<div v-bind="api.getBranchControlProps(nodeProps)">
<FolderIcon />
<span v-bind="api.getBranchTextProps(nodeProps)">{{ node.name }}</span>
<span v-bind="api.getBranchIndicatorProps(nodeProps)">
<ChevronRightIcon />
</span>
</div>
<div v-bind="api.getBranchContentProps(nodeProps)">
<div v-bind="api.getBranchIndentGuideProps(nodeProps)" />
<TreeNode
v-for="(childNode, index) in node.children"
:key="childNode.id"
:node="childNode"
:index-path="[...indexPath, index]"
:api="api"
/>
</div>
</div>
</template>
<template v-else>
<div v-bind="api.getItemProps(nodeProps)"><FileIcon /> {{ node.name }}</div>
</template>
</template>
107 changes: 61 additions & 46 deletions examples/nuxt-ts/pages/tree-view.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,52 @@ import { normalizeProps, useMachine } from "@zag-js/vue"
const controls = useControls(treeviewControls)
const [state, send] = useMachine(tree.machine({ id: "1" }), {
interface Node {
id: string
name: string
children?: Node[]
}
const collection = tree.collection<Node>({
nodeToValue: (node) => node.id,
nodeToString: (node) => node.name,
rootNode: {
id: "ROOT",
name: "",
children: [
{
id: "node_modules",
name: "node_modules",
children: [
{ id: "node_modules/zag-js", name: "zag-js" },
{ id: "node_modules/pandacss", name: "panda" },
{
id: "node_modules/@types",
name: "@types",
children: [
{ id: "node_modules/@types/react", name: "react" },
{ id: "node_modules/@types/react-dom", name: "react-dom" },
],
},
],
},
{
id: "src",
name: "src",
children: [
{ id: "src/app.tsx", name: "app.tsx" },
{ id: "src/index.ts", name: "index.ts" },
],
},
{ id: "panda.config", name: "panda.config.ts" },
{ id: "package.json", name: "package.json" },
{ id: "renovate.json", name: "renovate.json" },
{ id: "readme.md", name: "README.md" },
],
},
})
const [state, send] = useMachine(tree.machine({ id: "1", collection }), {
context: controls.context,
})
Expand All @@ -16,58 +61,28 @@ const api = computed(() => tree.connect(state.value, send, normalizeProps))
<main class="tree-view">
<div v-bind="api.getRootProps()">
<h3 v-bind="api.getLabelProps()">My Documents</h3>
<div>
<div style="display: flex; gap: 10px">
<button @click="api.collapse()">Collapse All</button>
<button @click="api.expand()">Expand All</button>
<span> - </span>
<button @click="api.select()">Select All</button>
<button @click="api.deselect()">Deselect All</button>
<template v-if="controls.context.value.selectionMode === 'multiple'">
<button @click="api.select()">Select All</button>
<button @click="api.deselect()">Deselect All</button>
</template>
</div>
<div v-bind="api.getTreeProps()">
<TreeNode
v-for="(node, index) in api.collection.rootNode.children"
:key="node.id"
:node="node"
:index-path="[index]"
:api="api"
/>
</div>

<ul v-bind="api.getTreeProps()">
<li v-bind="api.getBranchProps({ value: 'node_modules', depth: 1 })">
<div v-bind="api.getBranchControlProps({ value: 'node_modules', depth: 1 })">
<span v-bind="api.getBranchTextProps({ value: 'node_modules', depth: 1 })"> 📂 node_modules</span>
</div>

<ul v-bind="api.getBranchContentProps({ value: 'node_modules', depth: 1 })">
<li v-bind="api.getItemProps({ value: 'node_modules/zag-js', depth: 2 })">📄 zag-js</li>
<li v-bind="api.getItemProps({ value: 'node_modules/pandacss', depth: 2 })">📄 panda</li>

<li v-bind="api.getBranchProps({ value: 'node_modules/@types', depth: 2 })">
<div v-bind="api.getBranchControlProps({ value: 'node_modules/@types', depth: 2 })">
<span v-bind="api.getBranchTextProps({ value: 'node_modules/@types', depth: 2 })"> 📂 @types</span>
</div>

<ul v-bind="api.getBranchContentProps({ value: 'node_modules/@types', depth: 2 })">
<li v-bind="api.getItemProps({ value: 'node_modules/@types/react', depth: 3 })">📄 react</li>
<li v-bind="api.getItemProps({ value: 'node_modules/@types/react-dom', depth: 3 })">📄 react-dom</li>
</ul>
</li>
</ul>
</li>

<li v-bind="api.getBranchProps({ value: 'src', depth: 1 })">
<div v-bind="api.getBranchControlProps({ value: 'src', depth: 1 })">
<span v-bind="api.getBranchTextProps({ value: 'src', depth: 1 })"> 📂 src</span>
</div>

<ul v-bind="api.getBranchContentProps({ value: 'src', depth: 1 })">
<li v-bind="api.getItemProps({ value: 'src/app.tsx', depth: 2 })">📄 app.tsx</li>
<li v-bind="api.getItemProps({ value: 'src/index.ts', depth: 2 })">📄 index.ts</li>
</ul>
</li>

<li v-bind="api.getItemProps({ value: 'panda.config', depth: 1 })">📄 panda.config.ts</li>
<li v-bind="api.getItemProps({ value: 'package.json', depth: 1 })">📄 package.json</li>
<li v-bind="api.getItemProps({ value: 'renovate.json', depth: 1 })">📄 renovate.json</li>
<li v-bind="api.getItemProps({ value: 'readme.md', depth: 1 })">📄 README.md</li>
</ul>
</div>
</main>

<Toolbar>
<StateVisualizer :state="state" />
<StateVisualizer :state="state" :omit="['collection']" />
<template #controls>
<Controls :control="controls" />
</template>
Expand Down
Loading

0 comments on commit a2af4ad

Please sign in to comment.