mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
[Design System] Chore: add tree-view documentation (#26821)
* chore: init docs * remove old stories * update source panels
This commit is contained in:
1 parent
33abcaa873
commit
73ff6c1085
15 files changed
+662
-487
No files matched your search
@@ -1699,6 +1699,50 @@ export const Index: Record<string, any> = {
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"tree-view-demo": {
|
||||
name: "tree-view-demo",
|
||||
type: "components:example",
|
||||
registryDependencies: undefined,
|
||||
component: React.lazy(() => import("@/registry/default/example/tree-view-demo")),
|
||||
source: "",
|
||||
files: ["registry/default/example/tree-view-demo.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"tree-view-edit": {
|
||||
name: "tree-view-edit",
|
||||
type: "components:example",
|
||||
registryDependencies: undefined,
|
||||
component: React.lazy(() => import("@/registry/default/example/tree-view-edit")),
|
||||
source: "",
|
||||
files: ["registry/default/example/tree-view-edit.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"tree-view-directories": {
|
||||
name: "tree-view-directories",
|
||||
type: "components:example",
|
||||
registryDependencies: undefined,
|
||||
component: React.lazy(() => import("@/registry/default/example/tree-view-directories")),
|
||||
source: "",
|
||||
files: ["registry/default/example/tree-view-directories.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"tree-view-multi-select": {
|
||||
name: "tree-view-multi-select",
|
||||
type: "components:example",
|
||||
registryDependencies: undefined,
|
||||
component: React.lazy(() => import("@/registry/default/example/tree-view-multi-select")),
|
||||
source: "",
|
||||
files: ["registry/default/example/tree-view-multi-select.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"color-usage-surface-www-and-docs": {
|
||||
name: "color-usage-surface-www-and-docs",
|
||||
type: "docs:example",
|
||||
|
||||
@@ -9,12 +9,6 @@ import Image from 'next/image'
|
||||
|
||||
const SourcePanel = forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement> & { doc: Doc }>(
|
||||
({ doc, children, ...props }, ref) => {
|
||||
const label = {
|
||||
radix: 'Radix UI',
|
||||
vaul: 'Vaul',
|
||||
shadcn: 'ui.shadcn',
|
||||
}
|
||||
|
||||
const ShadcnPanel = () => {
|
||||
if (doc.source?.shadcn) {
|
||||
return (
|
||||
@@ -79,9 +73,9 @@ const SourcePanel = forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement> &
|
||||
alt="Vaul"
|
||||
className="h-6 w-6 rounded-full"
|
||||
/>
|
||||
<span className="hidden font-bold sm:inline-block">vaul</span>
|
||||
<span className="hidden font-bold text-xs sm:inline-block">vaul</span>
|
||||
</div>
|
||||
<span className="text-foreground-light text-sm">
|
||||
<span className="text-foreground-light text-xs">
|
||||
This component is based on vaul by emilkowalski
|
||||
</span>
|
||||
</div>
|
||||
@@ -89,6 +83,74 @@ const SourcePanel = forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement> &
|
||||
}
|
||||
}
|
||||
|
||||
const ReactAccesibleTreeViewPanel = () => {
|
||||
if (doc.source?.reactAccessibleTreeview) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'bg-surface-75/50 border flex items-center p-3 px-5 gap-6 first:rounded-t-md last:rounded-b-md justify-between',
|
||||
props.className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Image
|
||||
width={24}
|
||||
height={24}
|
||||
src="https://avatars.githubusercontent.com/u/14020024?v=4"
|
||||
alt="dgreene1"
|
||||
className="h-6 w-6 rounded-full"
|
||||
/>
|
||||
<span className="hidden text-xs font-bold sm:inline-block">
|
||||
react-accessible-treeview
|
||||
</span>
|
||||
<span className="text-foreground-light text-xs">
|
||||
Component based on react-accessible-treeview by dgreene1
|
||||
</span>
|
||||
</div>
|
||||
{doc.links ? (
|
||||
<div className="flex items-center gap-2 justify-end">
|
||||
{doc.links?.doc && (
|
||||
<Button
|
||||
type="outline"
|
||||
className="rounded-full"
|
||||
icon={<ExternalLink className="text-foreground-muted" strokeWidth={1} />}
|
||||
>
|
||||
<Link
|
||||
href={doc.links.doc}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
|
||||
// className={cn(buttonVariants({ variant: 'default' }), 'gap-1')}
|
||||
>
|
||||
Docs
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
{doc.links?.api && (
|
||||
<Button
|
||||
type="outline"
|
||||
className="rounded-full"
|
||||
icon={<ExternalLink className="text-foreground-muted" strokeWidth={1} />}
|
||||
>
|
||||
<Link
|
||||
href={doc.links.api}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
|
||||
// className={cn(badgeVariants({ variant: 'default' }), 'gap-1')}
|
||||
>
|
||||
API Reference
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const InputOtp = () => {
|
||||
if (doc.source?.inputOtp) {
|
||||
return (
|
||||
@@ -107,9 +169,9 @@ const SourcePanel = forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement> &
|
||||
alt="inputOtp"
|
||||
className="h-6 w-6 rounded-full"
|
||||
/>
|
||||
<span className="hidden font-bold sm:inline-block">input-otp</span>
|
||||
<span className="hidden font-bold text-xs sm:inline-block">input-otp</span>
|
||||
</div>
|
||||
<span className="text-foreground-light text-sm">
|
||||
<span className="text-foreground-light text-xs">
|
||||
This component is based on input-otp by guilhermerodz
|
||||
</span>
|
||||
</div>
|
||||
@@ -145,7 +207,7 @@ const SourcePanel = forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement> &
|
||||
<path d="M11.5 7C13.433 7 15 5.433 15 3.5C15 1.567 13.433 0 11.5 0C9.56704 0 8 1.567 8 3.5C8 5.433 9.56704 7 11.5 7Z"></path>
|
||||
</svg>
|
||||
<div className="flex flex-row items-center justify-between text-sm w-full">
|
||||
<span className="text-foreground-light text-sm">This component uses Radix UI</span>
|
||||
<span className="text-foreground-light text-xs">This component uses Radix UI</span>
|
||||
{doc.links ? (
|
||||
<div className="flex items-center gap-2 justify-end">
|
||||
{doc.links?.doc && (
|
||||
@@ -195,6 +257,7 @@ const SourcePanel = forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement> &
|
||||
<RadixPanel />
|
||||
<VaulPanel />
|
||||
<InputOtp />
|
||||
<ReactAccesibleTreeViewPanel />
|
||||
{/* <ShadcnPanel /> */}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -364,6 +364,11 @@ export const docsConfig: DocsConfig = {
|
||||
href: '/docs/components/tooltip',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: 'Tree View',
|
||||
href: '/docs/components/tree-view',
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
---
|
||||
title: Tree View
|
||||
description: A tree view that assembles all the functionalities of the Accordion component to create a tree view.
|
||||
component: true
|
||||
links:
|
||||
doc: https://dgreene1.github.io/react-accessible-treeview/
|
||||
api: https://dgreene1.github.io/react-accessible-treeview/docs/api
|
||||
source:
|
||||
reactAccessibleTreeview: true
|
||||
---
|
||||
|
||||
<ComponentPreview name="tree-view-demo" peekCode wide />
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { TreeView, TreeViewItem } from 'ui'
|
||||
import { flattenTree } from 'react-accessible-treeview'
|
||||
```
|
||||
|
||||
```tsx
|
||||
const data: {
|
||||
name: ''
|
||||
children: [
|
||||
{
|
||||
name: 'Current batch'
|
||||
},
|
||||
{
|
||||
name: 'Older queries'
|
||||
},
|
||||
{
|
||||
name: 'query all users'
|
||||
},
|
||||
{
|
||||
name: 'users in last day'
|
||||
},
|
||||
{
|
||||
name: 'new users over time'
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<TreeView
|
||||
{...args}
|
||||
data={flattenTree(args.data)}
|
||||
aria-label="directory tree"
|
||||
nodeRenderer={({ element, isBranch, isExpanded, getNodeProps, level, isSelected }) => (
|
||||
<TreeViewItem
|
||||
isExpanded={isExpanded}
|
||||
isBranch={isBranch}
|
||||
isSelected={isSelected}
|
||||
level={level}
|
||||
xPadding={16}
|
||||
name={element.name}
|
||||
{...getNodeProps()}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### With directories
|
||||
|
||||
Use a `children` node in your data object to create a tree view with directories.
|
||||
|
||||
```tsx {9-16}
|
||||
const data = {
|
||||
//..
|
||||
children: [
|
||||
{
|
||||
name: 'Current batch',
|
||||
},
|
||||
{
|
||||
name: 'Older queries',
|
||||
children: [
|
||||
{
|
||||
name: 'all countries',
|
||||
},
|
||||
{
|
||||
name: 'add new countries',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
<ComponentPreview name="tree-view-directories" />
|
||||
|
||||
### With inline editing
|
||||
|
||||
The Tree View can be edited in the app to enable different types of inline editing.
|
||||
|
||||
In this example, right click on any item to rename it, and we traverse the data object setting `isEditing` to true.
|
||||
`TreeViewItem` then accepts a `isEditing` prop to show and focus an inline input to edit.
|
||||
|
||||
<ComponentPreview name="tree-view-edit" />
|
||||
|
||||
### With multi selection
|
||||
|
||||
Use the `multiSelect`, `togglableSelect`, `clickAction` set to "EXCLUSIVE_SELECT" to enable a multi-select tree view.
|
||||
|
||||
An example can be seen on the [react-accessible-treeview docs](https://dgreene1.github.io/react-accessible-treeview/docs/examples-MultiSelectDirectoryTree).
|
||||
|
||||
```tsx {4-6}
|
||||
<TreeView
|
||||
data={flattenTree(args.data)}
|
||||
aria-label="directory tree"
|
||||
multiSelect
|
||||
togglableSelect
|
||||
clickAction="EXCLUSIVE_SELECT"
|
||||
// other options..
|
||||
/>
|
||||
```
|
||||
|
||||
<ComponentPreview name="tree-view-multi-select" />
|
||||
@@ -50,6 +50,9 @@ const NestedProperties = defineNestedType(() => ({
|
||||
inputOtp: {
|
||||
type: 'boolean',
|
||||
},
|
||||
reactAccessibleTreeview: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { TreeView, TreeViewItem } from 'ui'
|
||||
import { flattenTree } from 'react-accessible-treeview'
|
||||
|
||||
export default function TreeViewDemo() {
|
||||
const data = {
|
||||
name: '',
|
||||
children: [
|
||||
{
|
||||
name: 'Current batch',
|
||||
},
|
||||
{
|
||||
name: 'Older queries',
|
||||
},
|
||||
{
|
||||
name: 'query all users',
|
||||
},
|
||||
{
|
||||
name: 'users in last day',
|
||||
},
|
||||
{
|
||||
name: 'new users over time',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
return (
|
||||
<TreeView
|
||||
data={flattenTree(data)}
|
||||
aria-label="directory tree"
|
||||
className="w-[420px]"
|
||||
nodeRenderer={({ element, isBranch, isExpanded, getNodeProps, level, isSelected }) => (
|
||||
<TreeViewItem
|
||||
isExpanded={isExpanded}
|
||||
isBranch={isBranch}
|
||||
isSelected={isSelected}
|
||||
level={level}
|
||||
xPadding={16}
|
||||
name={element.name}
|
||||
{...getNodeProps()}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { TreeView, TreeViewItem } from 'ui'
|
||||
import { flattenTree } from 'react-accessible-treeview'
|
||||
|
||||
export default function TreeViewDemo() {
|
||||
const data = {
|
||||
name: '',
|
||||
children: [
|
||||
{
|
||||
name: 'Current batch',
|
||||
children: [{ name: 'index.js' }, { name: 'styles.css' }],
|
||||
},
|
||||
{
|
||||
name: 'Older queries',
|
||||
children: [
|
||||
{
|
||||
name: 'all countries',
|
||||
},
|
||||
{
|
||||
name: 'add new countries',
|
||||
},
|
||||
{
|
||||
name: 'regions',
|
||||
},
|
||||
{
|
||||
name: 'regions by customer',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'query all users',
|
||||
},
|
||||
{
|
||||
name: 'users in last day',
|
||||
},
|
||||
{
|
||||
name: 'new users over time',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
return (
|
||||
<TreeView
|
||||
data={flattenTree(data)}
|
||||
aria-label="directory tree"
|
||||
className="w-[420px]"
|
||||
nodeRenderer={({ element, isBranch, isExpanded, getNodeProps, level, isSelected }) => (
|
||||
<TreeViewItem
|
||||
isExpanded={isExpanded}
|
||||
isBranch={isBranch}
|
||||
isSelected={isSelected}
|
||||
level={level}
|
||||
xPadding={16}
|
||||
name={element.name}
|
||||
{...getNodeProps()}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { useState } from 'react'
|
||||
import { TreeView, TreeViewItem } from 'ui'
|
||||
import { flattenTree } from 'react-accessible-treeview'
|
||||
import {
|
||||
ContextMenu_Shadcn_,
|
||||
ContextMenuContent_Shadcn_,
|
||||
ContextMenuItem_Shadcn_,
|
||||
ContextMenuSeparator_Shadcn_,
|
||||
ContextMenuTrigger_Shadcn_,
|
||||
} from 'ui'
|
||||
|
||||
const data = {
|
||||
name: 'basic tree',
|
||||
children: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Current batch',
|
||||
children: [
|
||||
{ id: 2, name: 'index.js' },
|
||||
{ id: 3, name: 'styles.css' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Older queries',
|
||||
children: [
|
||||
{
|
||||
id: 5,
|
||||
name: 'all countries',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'add new countries',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'regions',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'regions by customer',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: 'query all users',
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: 'users in last day',
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: 'new users over time',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default function TreeViewDemo() {
|
||||
const [treeData, setDataTreeState] = useState(data)
|
||||
|
||||
function handleRenameSelect(id: number) {
|
||||
// find id in treeData and set it to edit mode
|
||||
let updatedTreeData = { ...treeData }
|
||||
const findNode = (node: any) => {
|
||||
if (node.id === id) {
|
||||
node.metadata = { isEditing: true }
|
||||
}
|
||||
if (node.children) {
|
||||
node.children.forEach(findNode)
|
||||
}
|
||||
}
|
||||
console.log(updatedTreeData)
|
||||
updatedTreeData.children.forEach(findNode)
|
||||
setDataTreeState(updatedTreeData)
|
||||
}
|
||||
|
||||
return (
|
||||
<TreeView
|
||||
data={flattenTree(data)}
|
||||
aria-label="directory tree"
|
||||
nodeRenderer={({ element, isBranch, isExpanded, getNodeProps, level, isSelected }) => {
|
||||
return (
|
||||
<ContextMenu_Shadcn_ modal={false}>
|
||||
<ContextMenuTrigger_Shadcn_ asChild>
|
||||
<TreeViewItem
|
||||
isExpanded={isExpanded}
|
||||
isBranch={isBranch}
|
||||
isSelected={isSelected}
|
||||
level={level}
|
||||
xPadding={16}
|
||||
name={element.name}
|
||||
isEditing={element.metadata?.isEditing === true}
|
||||
onEditSubmit={(value) => {
|
||||
console.log('value', value)
|
||||
let updatedTreeData = { ...treeData }
|
||||
const findNode = (node: any) => {
|
||||
if (node.id === element.id) {
|
||||
node.name = value
|
||||
node.metadata = { isEditing: false }
|
||||
}
|
||||
if (node.children) {
|
||||
node.children.forEach(findNode)
|
||||
}
|
||||
}
|
||||
updatedTreeData.children.forEach(findNode)
|
||||
setDataTreeState(updatedTreeData)
|
||||
}}
|
||||
{...getNodeProps()}
|
||||
/>
|
||||
</ContextMenuTrigger_Shadcn_>
|
||||
<ContextMenuContent_Shadcn_
|
||||
onCloseAutoFocus={(e) => {
|
||||
// stop focus propagation
|
||||
// so input in TreeViewItem gets focus
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<ContextMenuItem_Shadcn_
|
||||
onSelect={() => {
|
||||
handleRenameSelect(element.id as number)
|
||||
}}
|
||||
onFocusCapture={(e) => {
|
||||
// stop focus propagation
|
||||
// so input in TreeViewItem gets focus
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
Rename
|
||||
</ContextMenuItem_Shadcn_>
|
||||
<ContextMenuItem_Shadcn_ disabled>Open in new tab</ContextMenuItem_Shadcn_>
|
||||
<ContextMenuItem_Shadcn_ disabled>Share with team</ContextMenuItem_Shadcn_>
|
||||
<ContextMenuSeparator_Shadcn_ />
|
||||
<ContextMenuItem_Shadcn_ disabled>Delete</ContextMenuItem_Shadcn_>
|
||||
</ContextMenuContent_Shadcn_>
|
||||
</ContextMenu_Shadcn_>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import { useState } from 'react'
|
||||
import { TreeView, TreeViewItem } from 'ui'
|
||||
import { flattenTree } from 'react-accessible-treeview'
|
||||
import {
|
||||
ContextMenu_Shadcn_,
|
||||
ContextMenuContent_Shadcn_,
|
||||
ContextMenuItem_Shadcn_,
|
||||
ContextMenuSeparator_Shadcn_,
|
||||
ContextMenuTrigger_Shadcn_,
|
||||
} from 'ui'
|
||||
|
||||
const data = {
|
||||
name: 'basic tree',
|
||||
children: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Current batch',
|
||||
children: [
|
||||
{ id: 2, name: 'index.js' },
|
||||
{ id: 3, name: 'styles.css' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Older queries',
|
||||
children: [
|
||||
{
|
||||
id: 5,
|
||||
name: 'all countries',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'add new countries',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'regions',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'regions by customer',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: 'query all users',
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: 'users in last day',
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: 'new users over time',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default function TreeViewDemo() {
|
||||
const [treeData, setDataTreeState] = useState(data)
|
||||
|
||||
function handleRenameSelect(id: number) {
|
||||
// find id in treeData and set it to edit mode
|
||||
let updatedTreeData = { ...treeData }
|
||||
const findNode = (node: any) => {
|
||||
if (node.id === id) {
|
||||
node.metadata = { isEditing: true }
|
||||
}
|
||||
if (node.children) {
|
||||
node.children.forEach(findNode)
|
||||
}
|
||||
}
|
||||
console.log(updatedTreeData)
|
||||
updatedTreeData.children.forEach(findNode)
|
||||
setDataTreeState(updatedTreeData)
|
||||
}
|
||||
|
||||
return (
|
||||
<TreeView
|
||||
data={flattenTree(data)}
|
||||
aria-label="directory tree"
|
||||
togglableSelect
|
||||
clickAction="EXCLUSIVE_SELECT"
|
||||
multiSelect
|
||||
nodeRenderer={({ element, isBranch, isExpanded, getNodeProps, level, isSelected }) => {
|
||||
return (
|
||||
<ContextMenu_Shadcn_ modal={false}>
|
||||
<ContextMenuTrigger_Shadcn_ asChild>
|
||||
<TreeViewItem
|
||||
isExpanded={isExpanded}
|
||||
isBranch={isBranch}
|
||||
isSelected={isSelected}
|
||||
level={level}
|
||||
xPadding={16}
|
||||
name={element.name}
|
||||
isEditing={element.metadata?.isEditing === true}
|
||||
onEditSubmit={(value) => {
|
||||
console.log('value', value)
|
||||
let updatedTreeData = { ...treeData }
|
||||
const findNode = (node: any) => {
|
||||
if (node.id === element.id) {
|
||||
node.name = value
|
||||
node.metadata = { isEditing: false }
|
||||
}
|
||||
if (node.children) {
|
||||
node.children.forEach(findNode)
|
||||
}
|
||||
}
|
||||
updatedTreeData.children.forEach(findNode)
|
||||
setDataTreeState(updatedTreeData)
|
||||
}}
|
||||
{...getNodeProps()}
|
||||
/>
|
||||
</ContextMenuTrigger_Shadcn_>
|
||||
<ContextMenuContent_Shadcn_
|
||||
onCloseAutoFocus={(e) => {
|
||||
// stop focus propagation
|
||||
// so input in TreeViewItem gets focus
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<ContextMenuItem_Shadcn_
|
||||
onSelect={(e) => {
|
||||
handleRenameSelect(element.id as number)
|
||||
}}
|
||||
onFocusCapture={(e) => {
|
||||
// stop focus propagation
|
||||
// so input in TreeViewItem gets focus
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
Rename
|
||||
</ContextMenuItem_Shadcn_>
|
||||
<ContextMenuItem_Shadcn_ disabled>Open in new tab</ContextMenuItem_Shadcn_>
|
||||
<ContextMenuItem_Shadcn_ disabled>Share with team</ContextMenuItem_Shadcn_>
|
||||
<ContextMenuSeparator_Shadcn_ />
|
||||
<ContextMenuItem_Shadcn_ disabled>Delete</ContextMenuItem_Shadcn_>
|
||||
</ContextMenuContent_Shadcn_>
|
||||
</ContextMenu_Shadcn_>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -986,6 +986,26 @@ export const examples: Registry = [
|
||||
type: 'components:example',
|
||||
files: ['example/multi-select-form.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'tree-view-demo',
|
||||
type: 'components:example',
|
||||
files: ['example/tree-view-demo.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'tree-view-edit',
|
||||
type: 'components:example',
|
||||
files: ['example/tree-view-edit.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'tree-view-directories',
|
||||
type: 'components:example',
|
||||
files: ['example/tree-view-directories.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'tree-view-multi-select',
|
||||
type: 'components:example',
|
||||
files: ['example/tree-view-multi-select.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'color-usage-surface-www-and-docs',
|
||||
type: 'docs:example',
|
||||
|
||||
@@ -242,6 +242,8 @@ export * from './src/components/shadcn/ui/resizable'
|
||||
export * from './src/components/radio-group-stacked'
|
||||
export * from './src/components/radio-group-card'
|
||||
|
||||
export * from './src/components/TreeView'
|
||||
|
||||
// links
|
||||
|
||||
export * from './src/components/TextLink'
|
||||
|
||||
@@ -1,438 +0,0 @@
|
||||
import { StoryObj } from '@storybook/react'
|
||||
import React, { useState } from 'react'
|
||||
import { flattenTree } from 'react-accessible-treeview'
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuTrigger,
|
||||
} from '../shadcn/ui/context-menu'
|
||||
import { TreeView, TreeViewItem } from './TreeView'
|
||||
|
||||
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
|
||||
export default {
|
||||
title: 'Primitives/TreeView',
|
||||
component: TreeView,
|
||||
subcomponents: { TreeViewItem },
|
||||
parameters: {
|
||||
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
|
||||
layout: 'centered',
|
||||
},
|
||||
decorators: [
|
||||
(Story: any) => {
|
||||
return (
|
||||
<div className="bg-studio border-l border-r">
|
||||
<Story />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
],
|
||||
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
|
||||
tags: ['autodocs'],
|
||||
// More on argTypes: https://storybook.js.org/docs/api/argtypes
|
||||
argTypes: {
|
||||
data: {},
|
||||
},
|
||||
}
|
||||
|
||||
type Story = StoryObj<
|
||||
// Omit data so raw data can be passed to the story
|
||||
Omit<typeof TreeView, 'data'> & { data: any; className?: string }
|
||||
>
|
||||
|
||||
export const DefaultFlatTree: Story = {
|
||||
args: {
|
||||
className: 'w-[420px] h-[520px]',
|
||||
data: {
|
||||
name: '',
|
||||
children: [
|
||||
{
|
||||
name: 'Current batch',
|
||||
},
|
||||
{
|
||||
name: 'Older queries',
|
||||
},
|
||||
{
|
||||
name: 'query all users',
|
||||
},
|
||||
{
|
||||
name: 'users in last day',
|
||||
},
|
||||
{
|
||||
name: 'new users over time',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 👇 To avoid linting issues, it is recommended to use a function with a capitalized name.
|
||||
* If you are not concerned with linting, you may use an arrow function.
|
||||
*/
|
||||
render: function Render(args) {
|
||||
return (
|
||||
<TreeView
|
||||
{...args}
|
||||
data={flattenTree(args.data)}
|
||||
aria-label="directory tree"
|
||||
nodeRenderer={({ element, isBranch, isExpanded, getNodeProps, level, isSelected }) => (
|
||||
<TreeViewItem
|
||||
isExpanded={isExpanded}
|
||||
isBranch={isBranch}
|
||||
isSelected={isSelected}
|
||||
level={level}
|
||||
xPadding={16}
|
||||
name={element.name}
|
||||
{...getNodeProps()}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const WithDirectories: Story = {
|
||||
args: {
|
||||
className: 'w-[420px] h-[520px]',
|
||||
data: {
|
||||
name: '',
|
||||
children: [
|
||||
{
|
||||
name: 'Current batch',
|
||||
children: [{ name: 'index.js' }, { name: 'styles.css' }],
|
||||
},
|
||||
{
|
||||
name: 'Older queries',
|
||||
children: [
|
||||
{
|
||||
name: 'all countries',
|
||||
},
|
||||
{
|
||||
name: 'add new countries',
|
||||
},
|
||||
{
|
||||
name: 'regions',
|
||||
},
|
||||
{
|
||||
name: 'regions by customer',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'query all users',
|
||||
},
|
||||
{
|
||||
name: 'users in last day',
|
||||
},
|
||||
{
|
||||
name: 'new users over time',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 👇 To avoid linting issues, it is recommended to use a function with a capitalized name.
|
||||
* If you are not concerned with linting, you may use an arrow function.
|
||||
*/
|
||||
render: function Render(args) {
|
||||
return (
|
||||
<TreeView
|
||||
{...args}
|
||||
data={flattenTree(args.data)}
|
||||
aria-label="directory tree"
|
||||
nodeRenderer={({ element, isBranch, isExpanded, getNodeProps, level, isSelected }) => (
|
||||
<TreeViewItem
|
||||
isExpanded={isExpanded}
|
||||
isBranch={isBranch}
|
||||
isSelected={isSelected}
|
||||
level={level}
|
||||
xPadding={16}
|
||||
name={element.name}
|
||||
{...getNodeProps()}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}
|
||||
export const withEditEntity: Story = {
|
||||
args: {
|
||||
className: 'w-[420px] h-[520px]',
|
||||
data: {
|
||||
name: 'basic tree',
|
||||
children: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Current batch',
|
||||
children: [
|
||||
{ id: 2, name: 'index.js' },
|
||||
{ id: 3, name: 'styles.css' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Older queries',
|
||||
children: [
|
||||
{
|
||||
id: 5,
|
||||
name: 'all countries',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'add new countries',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'regions',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'regions by customer',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: 'query all users',
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: 'users in last day',
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: 'new users over time',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 👇 To avoid linting issues, it is recommended to use a function with a capitalized name.
|
||||
* If you are not concerned with linting, you may use an arrow function.
|
||||
*/
|
||||
render: function Render(args) {
|
||||
const [treeData, setDataTreeState] = useState(args.data)
|
||||
|
||||
function handleRenameSelect(id) {
|
||||
// find id in treeData and set it to edit mode
|
||||
let updatedTreeData = { ...treeData }
|
||||
const findNode = (node: any) => {
|
||||
if (node.id === id) {
|
||||
node.metadata = { isEditing: true }
|
||||
}
|
||||
if (node.children) {
|
||||
node.children.forEach(findNode)
|
||||
}
|
||||
}
|
||||
console.log(updatedTreeData)
|
||||
updatedTreeData.children.forEach(findNode)
|
||||
setDataTreeState(updatedTreeData)
|
||||
}
|
||||
|
||||
return (
|
||||
<TreeView
|
||||
{...args}
|
||||
data={flattenTree(args.data)}
|
||||
aria-label="directory tree"
|
||||
nodeRenderer={({ element, isBranch, isExpanded, getNodeProps, level, isSelected }) => {
|
||||
return (
|
||||
<ContextMenu modal={false}>
|
||||
<ContextMenuTrigger asChild>
|
||||
<TreeViewItem
|
||||
isExpanded={isExpanded}
|
||||
isBranch={isBranch}
|
||||
isSelected={isSelected}
|
||||
level={level}
|
||||
xPadding={16}
|
||||
name={element.name}
|
||||
isEditing={element.metadata?.isEditing === true}
|
||||
onEditSubmit={(value) => {
|
||||
console.log('value', value)
|
||||
let updatedTreeData = { ...treeData }
|
||||
const findNode = (node: any) => {
|
||||
if (node.id === element.id) {
|
||||
node.name = value
|
||||
node.metadata = { isEditing: false }
|
||||
}
|
||||
if (node.children) {
|
||||
node.children.forEach(findNode)
|
||||
}
|
||||
}
|
||||
updatedTreeData.children.forEach(findNode)
|
||||
setDataTreeState(updatedTreeData)
|
||||
}}
|
||||
{...getNodeProps()}
|
||||
/>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent
|
||||
onCloseAutoFocus={(e) => {
|
||||
// stop focus propagation
|
||||
// so input in TreeViewItem gets focus
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<ContextMenuItem
|
||||
onSelect={(e) => {
|
||||
handleRenameSelect(element.id)
|
||||
}}
|
||||
onFocusCapture={(e) => {
|
||||
// stop focus propagation
|
||||
// so input in TreeViewItem gets focus
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
Rename
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem disabled>Open in new tab</ContextMenuItem>
|
||||
<ContextMenuItem disabled>Share with team</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem disabled>Delete</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}
|
||||
export const withMultiSelect: Story = {
|
||||
args: {
|
||||
className: 'w-[420px] h-[520px]',
|
||||
multiselect: true,
|
||||
data: {
|
||||
name: 'basic tree',
|
||||
children: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Current batch',
|
||||
children: [
|
||||
{ id: 2, name: 'index.js' },
|
||||
{ id: 3, name: 'styles.css' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Older queries',
|
||||
children: [
|
||||
{
|
||||
id: 5,
|
||||
name: 'all countries',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'add new countries',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'regions',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'regions by customer',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: 'query all users',
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: 'users in last day',
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: 'new users over time',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 👇 To avoid linting issues, it is recommended to use a function with a capitalized name.
|
||||
* If you are not concerned with linting, you may use an arrow function.
|
||||
*/
|
||||
render: function Render(args) {
|
||||
const [treeData, setDataTreeState] = useState(args.data)
|
||||
|
||||
function handleRenameSelect(id) {
|
||||
// find id in treeData and set it to edit mode
|
||||
let updatedTreeData = { ...treeData }
|
||||
const findNode = (node: any) => {
|
||||
if (node.id === id) {
|
||||
node.metadata = { isEditing: true }
|
||||
}
|
||||
if (node.children) {
|
||||
node.children.forEach(findNode)
|
||||
}
|
||||
}
|
||||
console.log(updatedTreeData)
|
||||
updatedTreeData.children.forEach(findNode)
|
||||
setDataTreeState(updatedTreeData)
|
||||
}
|
||||
|
||||
return (
|
||||
<TreeView
|
||||
{...args}
|
||||
data={flattenTree(args.data)}
|
||||
aria-label="directory tree"
|
||||
nodeRenderer={({ element, isBranch, isExpanded, getNodeProps, level, isSelected }) => {
|
||||
return (
|
||||
<ContextMenu modal={false}>
|
||||
<ContextMenuTrigger asChild>
|
||||
<TreeViewItem
|
||||
isExpanded={isExpanded}
|
||||
isBranch={isBranch}
|
||||
isSelected={isSelected}
|
||||
level={level}
|
||||
xPadding={16}
|
||||
name={element.name}
|
||||
isEditing={element.metadata?.isEditing === true}
|
||||
onEditSubmit={(value) => {
|
||||
console.log('value', value)
|
||||
let updatedTreeData = { ...treeData }
|
||||
const findNode = (node: any) => {
|
||||
if (node.id === element.id) {
|
||||
node.name = value
|
||||
node.metadata = { isEditing: false }
|
||||
}
|
||||
if (node.children) {
|
||||
node.children.forEach(findNode)
|
||||
}
|
||||
}
|
||||
updatedTreeData.children.forEach(findNode)
|
||||
setDataTreeState(updatedTreeData)
|
||||
}}
|
||||
{...getNodeProps()}
|
||||
/>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent
|
||||
onCloseAutoFocus={(e) => {
|
||||
// stop focus propagation
|
||||
// so input in TreeViewItem gets focus
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<ContextMenuItem
|
||||
onSelect={(e) => {
|
||||
handleRenameSelect(element.id)
|
||||
}}
|
||||
onFocusCapture={(e) => {
|
||||
// stop focus propagation
|
||||
// so input in TreeViewItem gets focus
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
Rename
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem disabled>Open in new tab</ContextMenuItem>
|
||||
<ContextMenuItem disabled>Share with team</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem disabled>Delete</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client'
|
||||
|
||||
import { ChevronRight, FolderClosed, FolderOpen } from 'lucide-react'
|
||||
import { ComponentPropsWithoutRef, forwardRef, useEffect, useRef, useState } from 'react'
|
||||
import TreeViewPrimitive from 'react-accessible-treeview'
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { StoryObj } from '@storybook/react'
|
||||
import React from 'react'
|
||||
import { TreeViewItem } from './TreeView'
|
||||
|
||||
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
|
||||
export default {
|
||||
title: 'Primitives/TreeView',
|
||||
component: TreeViewItem,
|
||||
parameters: {
|
||||
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
|
||||
layout: 'centered',
|
||||
},
|
||||
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
|
||||
tags: ['autodocs'],
|
||||
// More on argTypes: https://storybook.js.org/docs/api/argtypes
|
||||
argTypes: {},
|
||||
}
|
||||
|
||||
type Story = StoryObj<typeof TreeViewItem>
|
||||
|
||||
export const SingleItem: Story = {
|
||||
args: {
|
||||
isSelected: false,
|
||||
level: 1,
|
||||
xPadding: 16,
|
||||
isBranch: false,
|
||||
isExpanded: false,
|
||||
levelPadding: 16,
|
||||
name: 'Element name',
|
||||
},
|
||||
/**
|
||||
* 👇 To avoid linting issues, it is recommended to use a function with a capitalized name.
|
||||
* If you are not concerned with linting, you may use an arrow function.
|
||||
*/
|
||||
render: function Render(args) {
|
||||
return <TreeViewItem {...args}>Element name</TreeViewItem>
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './TreeView'
|
||||
Reference in new issue
Block a user