@zentauri-ui/zentauri-components 1.7.6 → 1.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/cli/registry.json +1 -0
- package/dist/chunk-GRJFGIZC.mjs +417 -0
- package/dist/chunk-GRJFGIZC.mjs.map +1 -0
- package/dist/chunk-QHEHBC6M.js +421 -0
- package/dist/chunk-QHEHBC6M.js.map +1 -0
- package/dist/design-system/index.d.ts +1 -0
- package/dist/design-system/index.d.ts.map +1 -1
- package/dist/design-system/tree-view.d.ts +66 -0
- package/dist/design-system/tree-view.d.ts.map +1 -0
- package/dist/ui/tree-view/animated/animations.d.ts +6 -0
- package/dist/ui/tree-view/animated/animations.d.ts.map +1 -0
- package/dist/ui/tree-view/animated/index.d.ts +5 -0
- package/dist/ui/tree-view/animated/index.d.ts.map +1 -0
- package/dist/ui/tree-view/animated/tree-view-animated.d.ts +6 -0
- package/dist/ui/tree-view/animated/tree-view-animated.d.ts.map +1 -0
- package/dist/ui/tree-view/animated/types.d.ts +6 -0
- package/dist/ui/tree-view/animated/types.d.ts.map +1 -0
- package/dist/ui/tree-view/animated.js +53 -0
- package/dist/ui/tree-view/animated.js.map +1 -0
- package/dist/ui/tree-view/animated.mjs +50 -0
- package/dist/ui/tree-view/animated.mjs.map +1 -0
- package/dist/ui/tree-view/index.d.ts +5 -0
- package/dist/ui/tree-view/index.d.ts.map +1 -0
- package/dist/ui/tree-view/tree-view-base.d.ts +15 -0
- package/dist/ui/tree-view/tree-view-base.d.ts.map +1 -0
- package/dist/ui/tree-view/tree-view.d.ts +6 -0
- package/dist/ui/tree-view/tree-view.d.ts.map +1 -0
- package/dist/ui/tree-view/types.d.ts +61 -0
- package/dist/ui/tree-view/types.d.ts.map +1 -0
- package/dist/ui/tree-view/variants.d.ts +9 -0
- package/dist/ui/tree-view/variants.d.ts.map +1 -0
- package/dist/ui/tree-view.js +27 -0
- package/dist/ui/tree-view.js.map +1 -0
- package/dist/ui/tree-view.mjs +14 -0
- package/dist/ui/tree-view.mjs.map +1 -0
- package/package.json +1 -1
- package/src/design-system/index.ts +1 -0
- package/src/design-system/tree-view.ts +113 -0
- package/src/ui/tree-view/animated/animations.ts +13 -0
- package/src/ui/tree-view/animated/index.ts +6 -0
- package/src/ui/tree-view/animated/tree-view-animated.tsx +52 -0
- package/src/ui/tree-view/animated/types.ts +6 -0
- package/src/ui/tree-view/index.ts +13 -0
- package/src/ui/tree-view/tree-view-base.tsx +496 -0
- package/src/ui/tree-view/tree-view.test.tsx +136 -0
- package/src/ui/tree-view/tree-view.tsx +9 -0
- package/src/ui/tree-view/types.ts +68 -0
- package/src/ui/tree-view/variants.ts +32 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { TreeView } from "./tree-view";
|
|
7
|
+
import type { TreeNode } from "./types";
|
|
8
|
+
|
|
9
|
+
const data: TreeNode[] = [
|
|
10
|
+
{
|
|
11
|
+
id: "src",
|
|
12
|
+
label: "src",
|
|
13
|
+
children: [
|
|
14
|
+
{
|
|
15
|
+
id: "components",
|
|
16
|
+
label: "components",
|
|
17
|
+
children: [{ id: "button", label: "button.tsx" }],
|
|
18
|
+
},
|
|
19
|
+
{ id: "index", label: "index.ts" },
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
{ id: "readme", label: "README.md" },
|
|
23
|
+
{ id: "locked", label: "locked.ts", disabled: true },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
describe("TreeView", () => {
|
|
27
|
+
it("should expose displayName", () => {
|
|
28
|
+
expect(TreeView.displayName).toBe("TreeView");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should render a tree role with collapsed branches hidden", () => {
|
|
32
|
+
render(<TreeView data={data} aria-label="Files" />);
|
|
33
|
+
const tree = screen.getByRole("tree", { name: "Files" });
|
|
34
|
+
expect(tree).toBeTruthy();
|
|
35
|
+
expect(tree.getAttribute("data-slot")).toBe("tree-view");
|
|
36
|
+
// src, readme, locked are visible; nested children are not
|
|
37
|
+
expect(screen.getAllByRole("treeitem")).toHaveLength(3);
|
|
38
|
+
expect(screen.queryByText("index.ts")).not.toBeInTheDocument();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should mark parent nodes with aria-expanded and aria-level", () => {
|
|
42
|
+
render(<TreeView data={data} />);
|
|
43
|
+
const src = screen.getByText("src").closest('[role="treeitem"]');
|
|
44
|
+
expect(src).toHaveAttribute("aria-expanded", "false");
|
|
45
|
+
expect(src).toHaveAttribute("aria-level", "1");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should reveal children when a branch is clicked", async () => {
|
|
49
|
+
const user = userEvent.setup();
|
|
50
|
+
render(<TreeView data={data} />);
|
|
51
|
+
await user.click(screen.getByText("src"));
|
|
52
|
+
expect(
|
|
53
|
+
screen.getByText("src").closest('[role="treeitem"]'),
|
|
54
|
+
).toHaveAttribute("aria-expanded", "true");
|
|
55
|
+
expect(screen.getByText("index.ts")).toBeInTheDocument();
|
|
56
|
+
expect(screen.getByText("components")).toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should select a node and call onSelect", async () => {
|
|
60
|
+
const user = userEvent.setup();
|
|
61
|
+
const onSelect = vi.fn();
|
|
62
|
+
render(<TreeView data={data} onSelect={onSelect} />);
|
|
63
|
+
await user.click(screen.getByText("README.md"));
|
|
64
|
+
expect(onSelect).toHaveBeenCalledWith(
|
|
65
|
+
expect.objectContaining({ id: "readme" }),
|
|
66
|
+
);
|
|
67
|
+
expect(
|
|
68
|
+
screen.getByText("README.md").closest('[role="treeitem"]'),
|
|
69
|
+
).toHaveAttribute("aria-selected", "true");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should not select disabled nodes", async () => {
|
|
73
|
+
const user = userEvent.setup();
|
|
74
|
+
const onSelect = vi.fn();
|
|
75
|
+
render(<TreeView data={data} onSelect={onSelect} />);
|
|
76
|
+
const locked = screen.getByText("locked.ts").closest('[role="treeitem"]')!;
|
|
77
|
+
expect(locked).toHaveAttribute("aria-disabled", "true");
|
|
78
|
+
await user.click(locked);
|
|
79
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
80
|
+
expect(locked).toHaveAttribute("aria-selected", "false");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should expand with ArrowRight and move into children", async () => {
|
|
84
|
+
const user = userEvent.setup();
|
|
85
|
+
render(<TreeView data={data} />);
|
|
86
|
+
const src = screen.getByText("src").closest('[role="treeitem"]') as HTMLElement;
|
|
87
|
+
src.focus();
|
|
88
|
+
await user.keyboard("{ArrowRight}");
|
|
89
|
+
expect(src).toHaveAttribute("aria-expanded", "true");
|
|
90
|
+
await user.keyboard("{ArrowRight}");
|
|
91
|
+
expect(document.activeElement?.getAttribute("data-node-id")).toBe(
|
|
92
|
+
"components",
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should collapse with ArrowLeft on an expanded branch", async () => {
|
|
97
|
+
const user = userEvent.setup();
|
|
98
|
+
render(<TreeView data={data} defaultExpanded={["src"]} />);
|
|
99
|
+
const src = screen.getByText("src").closest('[role="treeitem"]') as HTMLElement;
|
|
100
|
+
src.focus();
|
|
101
|
+
await user.keyboard("{ArrowLeft}");
|
|
102
|
+
expect(src).toHaveAttribute("aria-expanded", "false");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should support controlled expansion", async () => {
|
|
106
|
+
const user = userEvent.setup();
|
|
107
|
+
const onExpandedChange = vi.fn();
|
|
108
|
+
function Controlled() {
|
|
109
|
+
const [expanded, setExpanded] = useState<string[]>([]);
|
|
110
|
+
return (
|
|
111
|
+
<TreeView
|
|
112
|
+
data={data}
|
|
113
|
+
expanded={expanded}
|
|
114
|
+
onExpandedChange={(ids) => {
|
|
115
|
+
onExpandedChange(ids);
|
|
116
|
+
setExpanded(ids);
|
|
117
|
+
}}
|
|
118
|
+
/>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
render(<Controlled />);
|
|
122
|
+
await user.click(screen.getByText("src"));
|
|
123
|
+
expect(onExpandedChange).toHaveBeenCalledWith(["src"]);
|
|
124
|
+
expect(screen.getByText("index.ts")).toBeInTheDocument();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should use a custom renderNode", () => {
|
|
128
|
+
render(
|
|
129
|
+
<TreeView
|
|
130
|
+
data={data}
|
|
131
|
+
renderNode={({ node }) => <span>node:{node.id}</span>}
|
|
132
|
+
/>,
|
|
133
|
+
);
|
|
134
|
+
expect(screen.getByText("node:readme")).toBeInTheDocument();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// tree-view.tsx — default static entry (no framer-motion on expand/collapse)
|
|
2
|
+
import { TreeViewBase } from "./tree-view-base";
|
|
3
|
+
import type { TreeViewProps } from "./types";
|
|
4
|
+
|
|
5
|
+
export function TreeView(props: TreeViewProps) {
|
|
6
|
+
return <TreeViewBase {...props} />;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
TreeView.displayName = "TreeView";
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { VariantProps } from "class-variance-authority";
|
|
2
|
+
import type { ComponentType, ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import type { treeViewVariants } from "./variants";
|
|
5
|
+
|
|
6
|
+
export type TreeViewVariantProps = VariantProps<typeof treeViewVariants>;
|
|
7
|
+
|
|
8
|
+
export type TreeNode = {
|
|
9
|
+
/** Stable, unique identifier used for expansion and selection state. */
|
|
10
|
+
id: string;
|
|
11
|
+
label: ReactNode;
|
|
12
|
+
/** Optional leading icon rendered before the label. */
|
|
13
|
+
icon?: ReactNode;
|
|
14
|
+
children?: TreeNode[];
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type TreeViewRenderArgs = {
|
|
19
|
+
node: TreeNode;
|
|
20
|
+
depth: number;
|
|
21
|
+
isExpanded: boolean;
|
|
22
|
+
isSelected: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type TreeViewBaseProps = TreeViewVariantProps & {
|
|
26
|
+
data: TreeNode[];
|
|
27
|
+
/** Uncontrolled set of expanded node ids. */
|
|
28
|
+
defaultExpanded?: string[];
|
|
29
|
+
/** Controlled set of expanded node ids. */
|
|
30
|
+
expanded?: string[];
|
|
31
|
+
onExpandedChange?: (ids: string[]) => void;
|
|
32
|
+
/** Uncontrolled selected node id. */
|
|
33
|
+
defaultSelected?: string;
|
|
34
|
+
/** Controlled selected node id. */
|
|
35
|
+
selected?: string;
|
|
36
|
+
onSelect?: (node: TreeNode) => void;
|
|
37
|
+
/** Replace the default label rendering for each node. */
|
|
38
|
+
renderNode?: (args: TreeViewRenderArgs) => ReactNode;
|
|
39
|
+
/** Draw vertical indentation guide lines for nested levels. */
|
|
40
|
+
showGuides?: boolean;
|
|
41
|
+
className?: string;
|
|
42
|
+
"aria-label"?: string;
|
|
43
|
+
"aria-labelledby"?: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type TreeViewProps = TreeViewBaseProps;
|
|
47
|
+
|
|
48
|
+
export type TreeGroupProps = {
|
|
49
|
+
open: boolean;
|
|
50
|
+
level: number;
|
|
51
|
+
children: ReactNode;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type TreeViewCtx = {
|
|
55
|
+
appearance: NonNullable<TreeViewBaseProps["appearance"]>;
|
|
56
|
+
size: NonNullable<TreeViewBaseProps["size"]>;
|
|
57
|
+
showGuides: boolean;
|
|
58
|
+
GroupComponent: ComponentType<TreeGroupProps>;
|
|
59
|
+
isExpanded: (id: string) => boolean;
|
|
60
|
+
toggleExpanded: (id: string) => void;
|
|
61
|
+
setExpanded: (id: string, open: boolean) => void;
|
|
62
|
+
selectedId: string | undefined;
|
|
63
|
+
activeId: string | undefined;
|
|
64
|
+
selectNode: (node: TreeNode) => void;
|
|
65
|
+
registerItem: (id: string, el: HTMLDivElement | null) => void;
|
|
66
|
+
onItemKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) => void;
|
|
67
|
+
renderNode?: (args: TreeViewRenderArgs) => ReactNode;
|
|
68
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { cva } from "class-variance-authority";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
zuiTreeViewAppearances,
|
|
5
|
+
zuiTreeViewBase,
|
|
6
|
+
zuiTreeViewItemAppearances,
|
|
7
|
+
zuiTreeViewItemBase,
|
|
8
|
+
zuiTreeViewItemSizes,
|
|
9
|
+
zuiTreeViewSizes,
|
|
10
|
+
} from "../../design-system/tree-view";
|
|
11
|
+
|
|
12
|
+
export const treeViewVariants = cva(zuiTreeViewBase, {
|
|
13
|
+
variants: {
|
|
14
|
+
appearance: zuiTreeViewAppearances,
|
|
15
|
+
size: zuiTreeViewSizes,
|
|
16
|
+
},
|
|
17
|
+
defaultVariants: {
|
|
18
|
+
appearance: "default",
|
|
19
|
+
size: "md",
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const treeViewItemVariants = cva(zuiTreeViewItemBase, {
|
|
24
|
+
variants: {
|
|
25
|
+
appearance: zuiTreeViewItemAppearances,
|
|
26
|
+
size: zuiTreeViewItemSizes,
|
|
27
|
+
},
|
|
28
|
+
defaultVariants: {
|
|
29
|
+
appearance: "default",
|
|
30
|
+
size: "md",
|
|
31
|
+
},
|
|
32
|
+
});
|