diff --git a/apps/design-system/__registry__/index.tsx b/apps/design-system/__registry__/index.tsx index 164c1f1458d..8a807716cb6 100644 --- a/apps/design-system/__registry__/index.tsx +++ b/apps/design-system/__registry__/index.tsx @@ -1699,6 +1699,50 @@ export const Index: Record = { 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", diff --git a/apps/design-system/components/source-panel.tsx b/apps/design-system/components/source-panel.tsx index 51fac19dfa6..92151a863c8 100644 --- a/apps/design-system/components/source-panel.tsx +++ b/apps/design-system/components/source-panel.tsx @@ -9,12 +9,6 @@ import Image from 'next/image' const SourcePanel = forwardRef & { 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 & alt="Vaul" className="h-6 w-6 rounded-full" /> - vaul + vaul - + This component is based on vaul by emilkowalski @@ -89,6 +83,74 @@ const SourcePanel = forwardRef & } } + const ReactAccesibleTreeViewPanel = () => { + if (doc.source?.reactAccessibleTreeview) { + return ( +
+
+ dgreene1 + + react-accessible-treeview + + + Component based on react-accessible-treeview by dgreene1 + +
+ {doc.links ? ( +
+ {doc.links?.doc && ( + + )} + {doc.links?.api && ( + + )} +
+ ) : null} +
+ ) + } + } + const InputOtp = () => { if (doc.source?.inputOtp) { return ( @@ -107,9 +169,9 @@ const SourcePanel = forwardRef & alt="inputOtp" className="h-6 w-6 rounded-full" /> - input-otp + input-otp - + This component is based on input-otp by guilhermerodz @@ -145,7 +207,7 @@ const SourcePanel = forwardRef &
- This component uses Radix UI + This component uses Radix UI {doc.links ? (
{doc.links?.doc && ( @@ -195,6 +257,7 @@ const SourcePanel = forwardRef & + {/* */}
) diff --git a/apps/design-system/config/docs.ts b/apps/design-system/config/docs.ts index e695abf85d1..36ce6c7f38b 100644 --- a/apps/design-system/config/docs.ts +++ b/apps/design-system/config/docs.ts @@ -364,6 +364,11 @@ export const docsConfig: DocsConfig = { href: '/docs/components/tooltip', items: [], }, + { + title: 'Tree View', + href: '/docs/components/tree-view', + items: [], + }, ], }, ], diff --git a/apps/design-system/content/docs/components/tree-view.mdx b/apps/design-system/content/docs/components/tree-view.mdx new file mode 100644 index 00000000000..afd4b2b65c5 --- /dev/null +++ b/apps/design-system/content/docs/components/tree-view.mdx @@ -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 +--- + + + +## 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 ( + ( + + )} + /> + ) +} +``` + +## 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', + }, + ], + }, + ], +} +``` + + + +### 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. + + + +### 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} + +``` + + diff --git a/apps/design-system/contentlayer.config.js b/apps/design-system/contentlayer.config.js index b342b5027aa..edc30b91b1d 100644 --- a/apps/design-system/contentlayer.config.js +++ b/apps/design-system/contentlayer.config.js @@ -50,6 +50,9 @@ const NestedProperties = defineNestedType(() => ({ inputOtp: { type: 'boolean', }, + reactAccessibleTreeview: { + type: 'boolean', + }, }, })) diff --git a/apps/design-system/registry/default/example/tree-view-demo.tsx b/apps/design-system/registry/default/example/tree-view-demo.tsx new file mode 100644 index 00000000000..1f9c3094695 --- /dev/null +++ b/apps/design-system/registry/default/example/tree-view-demo.tsx @@ -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 ( + ( + + )} + /> + ) +} diff --git a/apps/design-system/registry/default/example/tree-view-directories.tsx b/apps/design-system/registry/default/example/tree-view-directories.tsx new file mode 100644 index 00000000000..78071a743f3 --- /dev/null +++ b/apps/design-system/registry/default/example/tree-view-directories.tsx @@ -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 ( + ( + + )} + /> + ) +} diff --git a/apps/design-system/registry/default/example/tree-view-edit.tsx b/apps/design-system/registry/default/example/tree-view-edit.tsx new file mode 100644 index 00000000000..873583e8896 --- /dev/null +++ b/apps/design-system/registry/default/example/tree-view-edit.tsx @@ -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 ( + { + return ( + + + { + 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()} + /> + + { + // stop focus propagation + // so input in TreeViewItem gets focus + e.stopPropagation() + }} + > + { + handleRenameSelect(element.id as number) + }} + onFocusCapture={(e) => { + // stop focus propagation + // so input in TreeViewItem gets focus + e.stopPropagation() + }} + > + Rename + + Open in new tab + Share with team + + Delete + + + ) + }} + /> + ) +} diff --git a/apps/design-system/registry/default/example/tree-view-multi-select.tsx b/apps/design-system/registry/default/example/tree-view-multi-select.tsx new file mode 100644 index 00000000000..2a6665302d2 --- /dev/null +++ b/apps/design-system/registry/default/example/tree-view-multi-select.tsx @@ -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 ( + { + return ( + + + { + 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()} + /> + + { + // stop focus propagation + // so input in TreeViewItem gets focus + e.stopPropagation() + }} + > + { + handleRenameSelect(element.id as number) + }} + onFocusCapture={(e) => { + // stop focus propagation + // so input in TreeViewItem gets focus + e.stopPropagation() + }} + > + Rename + + Open in new tab + Share with team + + Delete + + + ) + }} + /> + ) +} diff --git a/apps/design-system/registry/examples.ts b/apps/design-system/registry/examples.ts index b8ce37c50a8..dd0a1543894 100644 --- a/apps/design-system/registry/examples.ts +++ b/apps/design-system/registry/examples.ts @@ -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', diff --git a/packages/ui/index.tsx b/packages/ui/index.tsx index 13c571b5d67..bcf64fc3426 100644 --- a/packages/ui/index.tsx +++ b/packages/ui/index.tsx @@ -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' diff --git a/packages/ui/src/components/TreeView/TreeView.stories.tsx b/packages/ui/src/components/TreeView/TreeView.stories.tsx deleted file mode 100644 index bfd7ce2cebf..00000000000 --- a/packages/ui/src/components/TreeView/TreeView.stories.tsx +++ /dev/null @@ -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 ( -
- -
- ) - }, - ], - // 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 & { 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 ( - ( - - )} - /> - ) - }, -} - -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 ( - ( - - )} - /> - ) - }, -} -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 ( - { - return ( - - - { - 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()} - /> - - { - // stop focus propagation - // so input in TreeViewItem gets focus - e.stopPropagation() - }} - > - { - handleRenameSelect(element.id) - }} - onFocusCapture={(e) => { - // stop focus propagation - // so input in TreeViewItem gets focus - e.stopPropagation() - }} - > - Rename - - Open in new tab - Share with team - - Delete - - - ) - }} - /> - ) - }, -} -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 ( - { - return ( - - - { - 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()} - /> - - { - // stop focus propagation - // so input in TreeViewItem gets focus - e.stopPropagation() - }} - > - { - handleRenameSelect(element.id) - }} - onFocusCapture={(e) => { - // stop focus propagation - // so input in TreeViewItem gets focus - e.stopPropagation() - }} - > - Rename - - Open in new tab - Share with team - - Delete - - - ) - }} - /> - ) - }, -} diff --git a/packages/ui/src/components/TreeView/TreeView.tsx b/packages/ui/src/components/TreeView/TreeView.tsx index aa7ca7f186f..28d79124379 100644 --- a/packages/ui/src/components/TreeView/TreeView.tsx +++ b/packages/ui/src/components/TreeView/TreeView.tsx @@ -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' diff --git a/packages/ui/src/components/TreeView/TreeViewItem.stories.tsx b/packages/ui/src/components/TreeView/TreeViewItem.stories.tsx deleted file mode 100644 index 7a8e3260517..00000000000 --- a/packages/ui/src/components/TreeView/TreeViewItem.stories.tsx +++ /dev/null @@ -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 - -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 Element name - }, -} diff --git a/packages/ui/src/components/TreeView/index.tsx b/packages/ui/src/components/TreeView/index.tsx index e69de29bb2d..e1e4aea33b1 100644 --- a/packages/ui/src/components/TreeView/index.tsx +++ b/packages/ui/src/components/TreeView/index.tsx @@ -0,0 +1 @@ +export * from './TreeView'