@exegia/corpora-ui 0.20.0 → 0.22.0
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/dist-lib/components/blocks/auth/auth-flow-atom.d.ts +77 -0
- package/dist-lib/components/blocks/auth/auth-session-atom.d.ts +28 -0
- package/dist-lib/components/blocks/auth/auth-state-type.d.ts +71 -0
- package/dist-lib/components/blocks/auth/auth-state.d.ts +9 -0
- package/dist-lib/components/blocks/auth/use-auth-state.d.ts +52 -0
- package/dist-lib/components/blocks/layout.d.ts +4 -1
- package/dist-lib/components/blocks/nav/sidebar/__tests__/ai-sidebar-atom.test.d.ts +1 -0
- package/dist-lib/components/blocks/nav/sidebar/ai-sidebar-atom.d.ts +217 -0
- package/dist-lib/components/blocks/nav/sidebar/index.d.ts +3 -1
- package/dist-lib/components/blocks/nav/sidebar/sidebar-context.d.ts +3 -0
- package/dist-lib/components/blocks/nav/sidebar/sidebar-row.d.ts +3 -1
- package/dist-lib/components/blocks/nav/sidebar/type.d.ts +109 -29
- package/dist-lib/components/blocks/nav/sidebar/use-ai-sidebar-state.d.ts +29 -0
- package/dist-lib/components/blocks/nav/sidebar/use-ai-sidebar.d.ts +6 -1
- package/dist-lib/components/blocks/nav/sidebar/utils.d.ts +1 -1
- package/dist-lib/components/blocks/profile/index.d.ts +7 -0
- package/dist-lib/components/blocks/profile/profile-card-atom.d.ts +68 -0
- package/dist-lib/components/blocks/profile/profile-card-block.d.ts +28 -39
- package/dist-lib/components/blocks/profile/type.d.ts +80 -0
- package/dist-lib/components/blocks/profile/use-profile-card-state.d.ts +24 -0
- package/dist-lib/components/blocks/profile/use-profile-card.d.ts +34 -0
- package/dist-lib/components/blocks/shell/__tests__/shell-fit-atom.test.d.ts +1 -0
- package/dist-lib/components/blocks/shell/__tests__/shell-metrics.test.d.ts +1 -0
- package/dist-lib/components/blocks/shell/animated-panel-provider.d.ts +1 -1
- package/dist-lib/components/blocks/shell/animated-panel.d.ts +1 -1
- package/dist-lib/components/blocks/shell/index.d.ts +4 -12
- package/dist-lib/components/blocks/shell/shell-fit-atom.d.ts +68 -0
- package/dist-lib/components/blocks/shell/shell-metrics.d.ts +54 -0
- package/dist-lib/components/blocks/shell/type.d.ts +115 -3
- package/dist-lib/components/blocks/shell/use-shell-fit-state.d.ts +25 -0
- package/dist-lib/components/blocks/shell/use-shell-fit.d.ts +14 -0
- package/dist-lib/components/blocks/shell/use-shell-panels.d.ts +12 -1
- package/dist-lib/components/blocks/shell/utils.d.ts +32 -0
- package/dist-lib/components/composed/__tests__/logo.test.d.ts +1 -0
- package/dist-lib/components/composed/logo.d.ts +34 -0
- package/dist-lib/components/composed/tree/__tests__/tree-atom.test.d.ts +1 -0
- package/dist-lib/components/composed/tree/constants.d.ts +4 -4
- package/dist-lib/components/composed/tree/index.d.ts +3 -1
- package/dist-lib/components/composed/tree/tree-atom.d.ts +160 -0
- package/dist-lib/components/composed/tree/tree-node.d.ts +6 -1
- package/dist-lib/components/composed/tree/type.d.ts +116 -18
- package/dist-lib/components/composed/tree/use-tree-dnd.d.ts +18 -7
- package/dist-lib/components/composed/tree/use-tree-state.d.ts +25 -0
- package/dist-lib/components/composed/tree/use-tree.d.ts +4 -0
- package/dist-lib/components/composed/user-avatar.d.ts +5 -28
- package/dist-lib/components/user-avatar/__tests__/user-avatar.test.d.ts +1 -0
- package/dist-lib/components/user-avatar/component.d.ts +1 -8
- package/dist-lib/components/user-avatar/index.d.ts +5 -0
- package/dist-lib/components/user-avatar/presence-badge.d.ts +16 -0
- package/dist-lib/components/user-avatar/type.d.ts +53 -0
- package/dist-lib/components/user-avatar/use-user-avatar-state.d.ts +26 -0
- package/dist-lib/components/user-avatar/use-user-avatar.d.ts +37 -0
- package/dist-lib/components/user-avatar/user-avatar-atom.d.ts +64 -0
- package/dist-lib/components/user-avatar/utils.d.ts +22 -0
- package/dist-lib/index.d.ts +6 -3
- package/dist-lib/index.js +3732 -2670
- package/dist-lib/index.js.map +1 -1
- package/dist-lib/state/exegia-provider.d.ts +51 -0
- package/dist-lib/state/index.d.ts +4 -0
- package/dist-lib/state/store.d.ts +17 -0
- package/package.json +15 -12
- package/src/components/blocks/auth/__tests__/auth-state-atom.test.tsx +247 -0
- package/src/components/blocks/auth/auth-flow-atom.ts +238 -0
- package/src/components/blocks/auth/auth-session-atom.ts +82 -0
- package/src/components/blocks/auth/auth-state-type.ts +97 -0
- package/src/components/blocks/auth/auth-state.ts +52 -0
- package/src/components/blocks/auth/use-auth-state.ts +127 -0
- package/src/components/blocks/layout.ts +30 -2
- package/src/components/blocks/nav/sidebar/__tests__/ai-sidebar-atom.test.tsx +325 -0
- package/src/components/blocks/nav/sidebar/ai-sidebar-atom.ts +698 -0
- package/src/components/blocks/nav/sidebar/ai-sidebar.tsx +48 -42
- package/src/components/blocks/nav/sidebar/index.ts +53 -1
- package/src/components/blocks/nav/sidebar/sidebar-context.ts +15 -0
- package/src/components/blocks/nav/sidebar/sidebar-row.tsx +119 -50
- package/src/components/blocks/nav/sidebar/type.ts +125 -30
- package/src/components/blocks/nav/sidebar/use-ai-sidebar-state.ts +123 -0
- package/src/components/blocks/nav/sidebar/use-ai-sidebar.ts +301 -248
- package/src/components/blocks/nav/sidebar/utils.ts +1 -1
- package/src/components/blocks/profile/__tests__/profile-card-block.test.tsx +194 -0
- package/src/components/blocks/profile/index.ts +28 -0
- package/src/components/blocks/profile/profile-card-atom.ts +247 -0
- package/src/components/blocks/profile/profile-card-block.tsx +153 -74
- package/src/components/blocks/profile/type.ts +95 -0
- package/src/components/blocks/profile/use-profile-card-state.ts +67 -0
- package/src/components/blocks/profile/use-profile-card.ts +126 -0
- package/src/components/blocks/shell/__tests__/shell-fit-atom.test.tsx +360 -0
- package/src/components/blocks/shell/__tests__/shell-layout.test.tsx +192 -3
- package/src/components/blocks/shell/__tests__/shell-metrics.test.ts +108 -0
- package/src/components/blocks/shell/animated-panel-inset.tsx +5 -1
- package/src/components/blocks/shell/animated-panel-provider.tsx +77 -11
- package/src/components/blocks/shell/animated-panel-trigger.tsx +4 -0
- package/src/components/blocks/shell/animated-panel.tsx +126 -29
- package/src/components/blocks/shell/index.ts +18 -13
- package/src/components/blocks/shell/shell-fit-atom.ts +243 -0
- package/src/components/blocks/shell/shell-layout.tsx +55 -53
- package/src/components/blocks/shell/shell-metrics.ts +79 -0
- package/src/components/blocks/shell/type.ts +130 -3
- package/src/components/blocks/shell/use-shell-fit-state.ts +49 -0
- package/src/components/blocks/shell/use-shell-fit.ts +135 -0
- package/src/components/blocks/shell/use-shell-panels.ts +57 -4
- package/src/components/blocks/shell/utils.ts +44 -4
- package/src/components/composed/__tests__/logo.test.tsx +60 -0
- package/src/components/composed/logo.tsx +159 -0
- package/src/components/composed/tree/CLAUDE.md +132 -0
- package/src/components/composed/tree/__tests__/tree-atom.test.tsx +217 -0
- package/src/components/composed/tree/__tests__/tree.test.tsx +162 -37
- package/src/components/composed/tree/__tests__/use-tree.test.tsx +2 -1
- package/src/components/composed/tree/constants.ts +4 -4
- package/src/components/composed/tree/index.ts +38 -0
- package/src/components/composed/tree/tree-atom.ts +590 -0
- package/src/components/composed/tree/tree-node.tsx +117 -78
- package/src/components/composed/tree/tree.tsx +24 -16
- package/src/components/composed/tree/type.ts +125 -15
- package/src/components/composed/tree/use-tree-dnd.ts +82 -51
- package/src/components/composed/tree/use-tree-state.ts +105 -0
- package/src/components/composed/tree/use-tree.ts +185 -184
- package/src/components/composed/user-avatar.tsx +11 -99
- package/src/components/user-avatar/__tests__/user-avatar.test.tsx +286 -0
- package/src/components/user-avatar/component.tsx +151 -22
- package/src/components/user-avatar/fallback.tsx +3 -1
- package/src/components/user-avatar/index.ts +19 -0
- package/src/components/user-avatar/presence-badge.tsx +56 -0
- package/src/components/user-avatar/type.ts +58 -0
- package/src/components/user-avatar/use-user-avatar-state.ts +60 -0
- package/src/components/user-avatar/use-user-avatar.ts +180 -0
- package/src/components/user-avatar/user-avatar-atom.ts +218 -0
- package/src/components/user-avatar/utils.ts +94 -0
- package/src/index.ts +35 -3
- package/src/state/exegia-provider.tsx +79 -0
- package/src/state/index.ts +4 -0
- package/src/state/store.ts +19 -0
- package/dist-lib/components/blocks/nav/sidebar-block.d.ts +0 -14
- package/dist-lib/components/blocks/nav/sidebar-nav-row.d.ts +0 -3
- package/dist-lib/components/blocks/nav/types.d.ts +0 -60
- package/dist-lib/components/blocks/nav/utils.d.ts +0 -1
- package/dist-lib/components/blocks/shell/animated-sidebar-content.d.ts +0 -2
- package/dist-lib/components/blocks/shell/animated-sidebar-footer.d.ts +0 -2
- package/dist-lib/components/blocks/shell/animated-sidebar-group-content.d.ts +0 -2
- package/dist-lib/components/blocks/shell/animated-sidebar-group-label.d.ts +0 -2
- package/dist-lib/components/blocks/shell/animated-sidebar-group.d.ts +0 -2
- package/dist-lib/components/blocks/shell/animated-sidebar-header.d.ts +0 -2
- package/dist-lib/components/blocks/shell/animated-sidebar-menu-button.d.ts +0 -3
- package/dist-lib/components/blocks/shell/animated-sidebar-menu-item.d.ts +0 -2
- package/dist-lib/components/blocks/shell/animated-sidebar-menu-sub-button.d.ts +0 -3
- package/dist-lib/components/blocks/shell/animated-sidebar-menu-sub-item.d.ts +0 -2
- package/dist-lib/components/blocks/shell/animated-sidebar-menu-sub.d.ts +0 -2
- package/dist-lib/components/blocks/shell/animated-sidebar-menu.d.ts +0 -2
- package/src/components/blocks/nav/__tests__/sidebar-block.test.tsx +0 -125
- package/src/components/blocks/nav/sidebar-block.tsx +0 -142
- package/src/components/blocks/nav/sidebar-nav-row.tsx +0 -78
- package/src/components/blocks/nav/types.ts +0 -65
- package/src/components/blocks/nav/utils.ts +0 -4
- package/src/components/blocks/shell/animated-sidebar-content.tsx +0 -19
- package/src/components/blocks/shell/animated-sidebar-footer.tsx +0 -19
- package/src/components/blocks/shell/animated-sidebar-group-content.tsx +0 -16
- package/src/components/blocks/shell/animated-sidebar-group-label.tsx +0 -29
- package/src/components/blocks/shell/animated-sidebar-group.tsx +0 -16
- package/src/components/blocks/shell/animated-sidebar-header.tsx +0 -16
- package/src/components/blocks/shell/animated-sidebar-menu-button.tsx +0 -151
- package/src/components/blocks/shell/animated-sidebar-menu-item.tsx +0 -20
- package/src/components/blocks/shell/animated-sidebar-menu-sub-button.tsx +0 -85
- package/src/components/blocks/shell/animated-sidebar-menu-sub-item.tsx +0 -19
- package/src/components/blocks/shell/animated-sidebar-menu-sub.tsx +0 -44
- package/src/components/blocks/shell/animated-sidebar-menu.tsx +0 -26
- /package/dist-lib/components/blocks/{nav/__tests__/sidebar-block.test.d.ts → auth/__tests__/auth-state-atom.test.d.ts} +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { describe, expect, mock, test } from "bun:test"
|
|
2
|
-
import { fireEvent, render, screen, waitFor } from "@testing-library/react"
|
|
2
|
+
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"
|
|
3
3
|
import userEvent from "@testing-library/user-event"
|
|
4
4
|
import { FileTextIcon, FolderIcon, SearchIcon } from "lucide-react"
|
|
5
5
|
|
|
6
|
+
import { RAIL_COLLAPSED_WIDTH } from "../constants"
|
|
6
7
|
import { Tree } from "../index"
|
|
7
8
|
import type { TreeNode } from "../type"
|
|
8
9
|
import { moveNode } from "../utils"
|
|
@@ -78,36 +79,45 @@ const FILES: TreeNode[] = [
|
|
|
78
79
|
|
|
79
80
|
/** Branch exits run through AnimatePresence — let them settle on real
|
|
80
81
|
* timers, then query once (happy-dom serves stale waitFor queries while
|
|
81
|
-
* an exit is in flight).
|
|
82
|
-
|
|
82
|
+
* an exit is in flight). Wrapped in act so the presence teardown that lands
|
|
83
|
+
* during the wait is a tracked React update, not an "outside act" warning. */
|
|
84
|
+
const settleExit = () =>
|
|
85
|
+
act(() => new Promise<void>((resolve) => setTimeout(resolve, 400)))
|
|
83
86
|
|
|
84
87
|
describe("Tree · navigation", () => {
|
|
85
|
-
test("3-level data renders top level as collapsible sections
|
|
88
|
+
test("3-level data renders top level as collapsible sections above button rows", async () => {
|
|
86
89
|
const user = userEvent.setup()
|
|
87
90
|
render(<Tree items={SECTIONED} variant="navigation" />)
|
|
88
91
|
|
|
89
92
|
const section = screen.getByRole("button", { name: "Research" })
|
|
90
93
|
expect(section.getAttribute("aria-expanded")).toBe("true")
|
|
91
|
-
// Sections are buttons
|
|
92
|
-
|
|
93
|
-
expect(
|
|
94
|
+
// Sections are plain heading buttons; item rows are the shared Button.
|
|
95
|
+
// Neither is an anchor — `href` is metadata for onNavigate now.
|
|
96
|
+
expect(document.querySelector("a")).toBeNull()
|
|
97
|
+
const search = screen.getByRole("button", { name: "Search" })
|
|
98
|
+
expect(search.getAttribute("data-slot")).toBe("tree-row")
|
|
99
|
+
expect(search.getAttribute("type")).toBe("button")
|
|
94
100
|
|
|
95
101
|
// Collapsing the section folds its items away.
|
|
96
102
|
await user.click(section)
|
|
97
103
|
await settleExit()
|
|
98
|
-
expect(screen.queryByRole("
|
|
104
|
+
expect(screen.queryByRole("button", { name: "Search" })).toBeNull()
|
|
99
105
|
})
|
|
100
106
|
|
|
101
|
-
test("2-level data keeps top level as
|
|
107
|
+
test("2-level data keeps top level as leaf rows and parents as toggles", async () => {
|
|
102
108
|
const user = userEvent.setup()
|
|
103
109
|
render(<Tree items={FLAT_NAV} variant="navigation" />)
|
|
104
110
|
|
|
105
|
-
|
|
111
|
+
// A leaf row carries no aria-expanded; a parent row does.
|
|
112
|
+
const search = screen.getByRole("button", { name: "Search" })
|
|
113
|
+
expect(search.getAttribute("aria-expanded")).toBeNull()
|
|
106
114
|
const parent = screen.getByRole("button", { name: "Library" })
|
|
107
115
|
expect(parent.getAttribute("aria-expanded")).toBe("false")
|
|
108
116
|
|
|
109
117
|
await user.click(parent)
|
|
110
|
-
expect(
|
|
118
|
+
expect(
|
|
119
|
+
await screen.findByRole("button", { name: "Manuscripts" })
|
|
120
|
+
).toBeDefined()
|
|
111
121
|
})
|
|
112
122
|
|
|
113
123
|
test("read-only variants render no drag or rename affordances", () => {
|
|
@@ -122,14 +132,15 @@ describe("Tree · navigation", () => {
|
|
|
122
132
|
<Tree items={SECTIONED} variant="navigation" />
|
|
123
133
|
)
|
|
124
134
|
// Library starts collapsed — its children are absent.
|
|
125
|
-
expect(screen.queryByRole("
|
|
135
|
+
expect(screen.queryByRole("button", { name: "Codices" })).toBeNull()
|
|
126
136
|
|
|
127
137
|
rerender(<Tree activeId="codices" items={SECTIONED} variant="navigation" />)
|
|
128
|
-
const active = await screen.findByRole("
|
|
138
|
+
const active = await screen.findByRole("button", { name: "Codices" })
|
|
129
139
|
expect(active.getAttribute("aria-current")).toBe("page")
|
|
140
|
+
expect(active.hasAttribute("data-active")).toBe(true)
|
|
130
141
|
})
|
|
131
142
|
|
|
132
|
-
test("onNavigate fires for
|
|
143
|
+
test("onNavigate fires for leaf rows after the node's own onSelect", async () => {
|
|
133
144
|
const user = userEvent.setup()
|
|
134
145
|
const order: string[] = []
|
|
135
146
|
const items: TreeNode[] = [
|
|
@@ -147,29 +158,115 @@ describe("Tree · navigation", () => {
|
|
|
147
158
|
variant="navigation"
|
|
148
159
|
/>
|
|
149
160
|
)
|
|
150
|
-
await user.click(screen.getByRole("
|
|
161
|
+
await user.click(screen.getByRole("button", { name: "Alpha" }))
|
|
151
162
|
expect(order).toEqual(["onSelect", "onNavigate"])
|
|
152
163
|
})
|
|
164
|
+
|
|
165
|
+
test("href is passed through to onNavigate, never rendered as an anchor", async () => {
|
|
166
|
+
const user = userEvent.setup()
|
|
167
|
+
const onNavigate = mock((_node: TreeNode) => {})
|
|
168
|
+
render(<Tree items={FLAT_NAV} onNavigate={onNavigate} variant="navigation" />)
|
|
169
|
+
|
|
170
|
+
const search = screen.getByRole("button", { name: "Search" })
|
|
171
|
+
expect(search.tagName).toBe("BUTTON")
|
|
172
|
+
expect(search.getAttribute("href")).toBeNull()
|
|
173
|
+
|
|
174
|
+
await user.click(search)
|
|
175
|
+
expect(onNavigate).toHaveBeenCalledTimes(1)
|
|
176
|
+
expect(onNavigate.mock.calls[0]?.[0]?.href).toBe("/search")
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test("sound=false strips the cuelume attributes from every row", () => {
|
|
180
|
+
render(<Tree items={FLAT_NAV} sound={false} variant="navigation" />)
|
|
181
|
+
for (const row of document.querySelectorAll('[data-slot="tree-row"]')) {
|
|
182
|
+
expect(row.hasAttribute("data-cuelume-press")).toBe(false)
|
|
183
|
+
expect(row.hasAttribute("data-cuelume-release")).toBe(false)
|
|
184
|
+
}
|
|
185
|
+
})
|
|
153
186
|
})
|
|
154
187
|
|
|
155
188
|
describe("Tree · toc", () => {
|
|
156
|
-
test("
|
|
189
|
+
test("parents select on the row and expand from the overlay toggle", async () => {
|
|
157
190
|
const user = userEvent.setup()
|
|
158
|
-
|
|
191
|
+
const onNavigate = mock((_node: TreeNode) => {})
|
|
192
|
+
render(<Tree items={TOC} onNavigate={onNavigate} variant="toc" />)
|
|
193
|
+
|
|
194
|
+
// Guide is a parent, but toc parents are selectable rows — the row
|
|
195
|
+
// itself carries no aria-expanded; the overlay chevron does.
|
|
196
|
+
const guide = screen.getByRole("button", { name: "Guide" })
|
|
197
|
+
expect(guide.getAttribute("aria-expanded")).toBeNull()
|
|
198
|
+
expect(guide.tagName).toBe("BUTTON")
|
|
199
|
+
const toggle = screen.getByRole("button", { name: "Expand Guide" })
|
|
200
|
+
expect(toggle.getAttribute("aria-expanded")).toBe("false")
|
|
201
|
+
|
|
202
|
+
// Pressing the row selects (and reports the node with its href) without
|
|
203
|
+
// expanding it.
|
|
204
|
+
await user.click(guide)
|
|
205
|
+
expect(onNavigate).toHaveBeenCalledTimes(1)
|
|
206
|
+
expect(onNavigate.mock.calls[0]?.[0]?.href).toBe("/guide")
|
|
207
|
+
expect(screen.queryByRole("button", { name: "Setup" })).toBeNull()
|
|
159
208
|
|
|
160
|
-
|
|
161
|
-
|
|
209
|
+
// The overlay toggle expands without selecting.
|
|
210
|
+
await user.click(toggle)
|
|
211
|
+
expect(await screen.findByRole("button", { name: "Setup" })).toBeDefined()
|
|
212
|
+
expect(onNavigate).toHaveBeenCalledTimes(1)
|
|
213
|
+
expect(toggle.getAttribute("aria-expanded")).toBe("true")
|
|
214
|
+
|
|
215
|
+
// Nested parents get their own overlay toggle; leaves get none.
|
|
216
|
+
await user.click(screen.getByRole("button", { name: "Expand Setup" }))
|
|
217
|
+
expect(await screen.findByRole("button", { name: "Install" })).toBeDefined()
|
|
218
|
+
expect(screen.queryByRole("button", { name: "Expand Install" })).toBeNull()
|
|
219
|
+
|
|
220
|
+
// Nothing in a toc renders as an anchor any more.
|
|
221
|
+
expect(document.querySelector("a")).toBeNull()
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
test("rows below the route level keep the native #{id} jump after select", async () => {
|
|
225
|
+
const user = userEvent.setup()
|
|
226
|
+
const order: string[] = []
|
|
227
|
+
window.location.hash = ""
|
|
228
|
+
render(
|
|
229
|
+
<Tree
|
|
230
|
+
items={TOC}
|
|
231
|
+
onNavigate={(node) => order.push(`navigate:${node.id}:${window.location.hash}`)}
|
|
232
|
+
variant="toc"
|
|
233
|
+
/>
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
// Depth 0 is a route (href "/guide"): no hash is written — routing is
|
|
237
|
+
// the consumer's job in onNavigate.
|
|
238
|
+
await user.click(screen.getByRole("button", { name: "Guide" }))
|
|
239
|
+
expect(window.location.hash).toBe("")
|
|
162
240
|
|
|
163
|
-
// Guide navigates on the row; its expansion is the overlay toggle.
|
|
164
241
|
await user.click(screen.getByRole("button", { name: "Expand Guide" }))
|
|
165
|
-
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
expect(
|
|
242
|
+
await user.click(await screen.findByRole("button", { name: "Setup" }))
|
|
243
|
+
// onNavigate ran first (hash still empty inside it), then the jump —
|
|
244
|
+
// the order the old anchor default gave.
|
|
245
|
+
expect(order).toEqual(["navigate:guide:", "navigate:setup:"])
|
|
246
|
+
expect(window.location.hash).toBe("#setup")
|
|
169
247
|
|
|
170
248
|
await user.click(screen.getByRole("button", { name: "Expand Setup" }))
|
|
171
|
-
|
|
172
|
-
expect(
|
|
249
|
+
await user.click(await screen.findByRole("button", { name: "Install" }))
|
|
250
|
+
expect(window.location.hash).toBe("#install")
|
|
251
|
+
|
|
252
|
+
// An explicit hash href wins over the id.
|
|
253
|
+
window.location.hash = ""
|
|
254
|
+
render(
|
|
255
|
+
<Tree
|
|
256
|
+
items={[
|
|
257
|
+
{
|
|
258
|
+
id: "top",
|
|
259
|
+
label: "Top",
|
|
260
|
+
children: [{ id: "leaf", label: "Leaf", href: "#custom" }],
|
|
261
|
+
},
|
|
262
|
+
]}
|
|
263
|
+
variant="toc"
|
|
264
|
+
/>
|
|
265
|
+
)
|
|
266
|
+
await user.click(screen.getByRole("button", { name: "Expand Top" }))
|
|
267
|
+
await user.click(await screen.findByRole("button", { name: "Leaf" }))
|
|
268
|
+
expect(window.location.hash).toBe("#custom")
|
|
269
|
+
window.location.hash = ""
|
|
173
270
|
})
|
|
174
271
|
})
|
|
175
272
|
|
|
@@ -188,26 +285,41 @@ describe("Tree · sidebar", () => {
|
|
|
188
285
|
|
|
189
286
|
rerender(<Tree collapsed items={RAIL} variant="sidebar" />)
|
|
190
287
|
await settleExit()
|
|
191
|
-
expect(rail()?.className).toContain("w-
|
|
288
|
+
expect(rail()?.className).toContain("w-10")
|
|
192
289
|
expect(rail()?.getAttribute("data-collapsed")).toBe("")
|
|
290
|
+
// Collapsed rows are square-ish tiles that fill the 40px rail.
|
|
291
|
+
const row = screen.getByRole("button", { name: "Search" })
|
|
292
|
+
expect(row.className).toContain("h-10!")
|
|
293
|
+
expect(row.className).toContain("rounded-xl!")
|
|
294
|
+
expect(row.className).toContain("justify-center")
|
|
193
295
|
|
|
194
296
|
// Back open — the direction that was broken.
|
|
195
297
|
rerender(<Tree items={RAIL} variant="sidebar" />)
|
|
196
298
|
await settleExit()
|
|
197
299
|
expect(rail()?.className).toContain("w-full")
|
|
198
|
-
expect(rail()?.className).not.toContain("w-
|
|
300
|
+
expect(rail()?.className).not.toContain("w-10")
|
|
199
301
|
expect(rail()?.getAttribute("data-collapsed")).toBeNull()
|
|
302
|
+
expect(screen.getByRole("button", { name: "Search" }).className).not.toContain(
|
|
303
|
+
"h-10!"
|
|
304
|
+
)
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
test("the collapsed px target matches the resting width class", () => {
|
|
308
|
+
// tree.tsx animates the rail to RAIL_COLLAPSED_WIDTH once measured, and
|
|
309
|
+
// falls back to the class before that. Tailwind's w-10 is 2.5rem = 40px;
|
|
310
|
+
// the two must agree or the rail jumps on its first collapse.
|
|
311
|
+
expect(RAIL_COLLAPSED_WIDTH).toBe(40)
|
|
200
312
|
})
|
|
201
313
|
|
|
202
314
|
test("collapsed shows icon-only rows that keep an accessible name", async () => {
|
|
203
315
|
const { rerender } = render(<Tree items={RAIL} variant="sidebar" />)
|
|
204
|
-
expect(screen.getByRole("
|
|
316
|
+
expect(screen.getByRole("button", { name: "Search" }).textContent).toContain(
|
|
205
317
|
"Search"
|
|
206
318
|
)
|
|
207
319
|
|
|
208
320
|
rerender(<Tree collapsed items={RAIL} variant="sidebar" />)
|
|
209
321
|
await settleExit()
|
|
210
|
-
const row = screen.getByRole("
|
|
322
|
+
const row = screen.getByRole("button", { name: "Search" })
|
|
211
323
|
// The label folds to nothing rather than unmounting — keeping it in the
|
|
212
324
|
// DOM is what lets the fold animate — so assert the fold, not absence.
|
|
213
325
|
const label = row.querySelector('[data-slot="tree-row-label"]')
|
|
@@ -221,7 +333,7 @@ describe("Tree · sidebar", () => {
|
|
|
221
333
|
// happy-dom can't drive Base UI's hover/focus-visible open logic, so
|
|
222
334
|
// assert the wiring: Base UI stamps its trigger attribute on the row.
|
|
223
335
|
const { rerender } = render(<Tree collapsed items={RAIL} variant="sidebar" />)
|
|
224
|
-
const collapsedRow = screen.getByRole("
|
|
336
|
+
const collapsedRow = screen.getByRole("button", { name: "Search" })
|
|
225
337
|
expect(collapsedRow.hasAttribute("data-base-ui-tooltip-trigger")).toBe(true)
|
|
226
338
|
|
|
227
339
|
// The tooltip wrapper stays mounted when the rail expands (it is only
|
|
@@ -235,7 +347,18 @@ describe("Tree · sidebar", () => {
|
|
|
235
347
|
// not read this test as covering it.
|
|
236
348
|
rerender(<Tree items={RAIL} variant="sidebar" />)
|
|
237
349
|
await settleExit()
|
|
238
|
-
expect(screen.getByRole("
|
|
350
|
+
expect(screen.getByRole("button", { name: "Search" })).toBe(collapsedRow)
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
test("the active rail row swaps to the secondary Button variant", () => {
|
|
354
|
+
render(<Tree activeId="search" items={RAIL} variant="sidebar" />)
|
|
355
|
+
const active = screen.getByRole("button", { name: "Search" })
|
|
356
|
+
const idle = screen.getByRole("button", { name: "Notes" })
|
|
357
|
+
expect(active.getAttribute("aria-current")).toBe("page")
|
|
358
|
+
// Button's cva emits bg-secondary for "secondary" and nothing of the
|
|
359
|
+
// sort for "ghost" — the cheapest observable proxy for the variant.
|
|
360
|
+
expect(active.className).toContain("bg-secondary")
|
|
361
|
+
expect(idle.className).not.toContain("bg-secondary")
|
|
239
362
|
})
|
|
240
363
|
|
|
241
364
|
test("nested children are ignored — the rail is single-level", () => {
|
|
@@ -287,8 +410,8 @@ describe("Tree · files", () => {
|
|
|
287
410
|
/>
|
|
288
411
|
)
|
|
289
412
|
|
|
290
|
-
//
|
|
291
|
-
//
|
|
413
|
+
// Every row is a <button> — one nesting the trailing button would be
|
|
414
|
+
// invalid markup React warns about.
|
|
292
415
|
for (const row of document.querySelectorAll('[data-slot="tree-row"]')) {
|
|
293
416
|
expect(row.querySelector("button, a")).toBeNull()
|
|
294
417
|
}
|
|
@@ -370,21 +493,23 @@ describe("Tree · keyboard", () => {
|
|
|
370
493
|
const user = userEvent.setup()
|
|
371
494
|
render(<Tree items={FLAT_NAV} variant="navigation" />)
|
|
372
495
|
|
|
373
|
-
const search = screen.getByRole("
|
|
496
|
+
const search = screen.getByRole("button", { name: "Search" })
|
|
374
497
|
search.focus()
|
|
375
498
|
await user.keyboard("{ArrowDown}")
|
|
376
499
|
const library = screen.getByRole("button", { name: "Library" })
|
|
377
500
|
expect(document.activeElement).toBe(library)
|
|
378
501
|
|
|
379
502
|
await user.keyboard("{ArrowRight}")
|
|
380
|
-
expect(
|
|
503
|
+
expect(
|
|
504
|
+
await screen.findByRole("button", { name: "Manuscripts" })
|
|
505
|
+
).toBeDefined()
|
|
381
506
|
await waitFor(() =>
|
|
382
507
|
expect(library.getAttribute("aria-expanded")).toBe("true")
|
|
383
508
|
)
|
|
384
509
|
|
|
385
510
|
await user.keyboard("{ArrowLeft}")
|
|
386
511
|
await settleExit()
|
|
387
|
-
expect(screen.queryByRole("
|
|
512
|
+
expect(screen.queryByRole("button", { name: "Manuscripts" })).toBeNull()
|
|
388
513
|
expect(document.activeElement).toBe(library)
|
|
389
514
|
})
|
|
390
515
|
|
|
@@ -35,7 +35,8 @@ const FILES: TreeNode[] = [
|
|
|
35
35
|
|
|
36
36
|
/** Branch exits run through AnimatePresence — let them settle before
|
|
37
37
|
* querying (happy-dom serves stale results while an exit is in flight). */
|
|
38
|
-
const settleExit = () =>
|
|
38
|
+
const settleExit = () =>
|
|
39
|
+
act(() => new Promise<void>((resolve) => setTimeout(resolve, 400)))
|
|
39
40
|
|
|
40
41
|
describe("useTree · expansion", () => {
|
|
41
42
|
test("expand, collapse and toggle drive a single node", () => {
|
|
@@ -15,10 +15,10 @@ export const TREE_MICRO_DURATION = 0.15
|
|
|
15
15
|
/** Sidebar rail label reveal/hide when `collapsed` flips. */
|
|
16
16
|
export const TREE_COLLAPSE_DURATION = 0.2
|
|
17
17
|
|
|
18
|
-
/** The collapsed rail's width in px —
|
|
19
|
-
* sync with the `w-
|
|
20
|
-
* expanded width has been measured. */
|
|
21
|
-
export const RAIL_COLLAPSED_WIDTH =
|
|
18
|
+
/** The collapsed rail's width in px — one `h-10 rounded-xl` icon row, edge
|
|
19
|
+
* to edge. Kept in sync with the `w-10` resting class the rail falls back to
|
|
20
|
+
* before its expanded width has been measured. */
|
|
21
|
+
export const RAIL_COLLAPSED_WIDTH = 40
|
|
22
22
|
|
|
23
23
|
/** Rail label text fade — starts as the width growth is finishing, so the
|
|
24
24
|
* label never reads as clipped mid-grow. */
|
|
@@ -1,13 +1,51 @@
|
|
|
1
1
|
export { Tree } from "./tree"
|
|
2
2
|
export { useTree } from "./use-tree"
|
|
3
|
+
export { useTreeActions, useTreeState } from "./use-tree-state"
|
|
3
4
|
export { moveNode, renameNode } from "./utils"
|
|
5
|
+
// The public atom surface. `@internal` atoms (config, handlers, mount, the
|
|
6
|
+
// projections) stay unexported on purpose — `export *` would make every
|
|
7
|
+
// internal a breaking-change surface for consumer apps.
|
|
8
|
+
export {
|
|
9
|
+
cancelTreeRenameAtom,
|
|
10
|
+
collapseAllTreeNodesAtom,
|
|
11
|
+
collapseTreeNodeAtom,
|
|
12
|
+
expandAllTreeNodesAtom,
|
|
13
|
+
expandTreeNodeAtom,
|
|
14
|
+
moveTreeNodeAtom,
|
|
15
|
+
removeTreeInstance,
|
|
16
|
+
renameTreeNodeAtom,
|
|
17
|
+
resetTreeAtom,
|
|
18
|
+
revealTreeNodeAtom,
|
|
19
|
+
selectTreeNodeAtom,
|
|
20
|
+
setTreeCollapsedAtom,
|
|
21
|
+
setTreeItemsAtom,
|
|
22
|
+
startTreeRenameAtom,
|
|
23
|
+
toggleTreeCollapsedAtom,
|
|
24
|
+
toggleTreeNodeAtom,
|
|
25
|
+
treeActiveIdAtom,
|
|
26
|
+
treeCanMoveAtom,
|
|
27
|
+
treeCanRenameAtom,
|
|
28
|
+
treeCollapsedAtom,
|
|
29
|
+
treeDraggedIdAtom,
|
|
30
|
+
treeDropTargetAtom,
|
|
31
|
+
treeExpandedIdsAtom,
|
|
32
|
+
treeItemsAtom,
|
|
33
|
+
treeRenamingIdAtom,
|
|
34
|
+
treeSectionedAtom,
|
|
35
|
+
treeSectionIdsAtom,
|
|
36
|
+
treeStateAtom,
|
|
37
|
+
} from "./tree-atom"
|
|
4
38
|
export type {
|
|
39
|
+
TreeActions,
|
|
5
40
|
TreeController,
|
|
6
41
|
TreeControllerProps,
|
|
7
42
|
TreeDataProps,
|
|
8
43
|
TreeDropPosition,
|
|
44
|
+
TreeDropTarget,
|
|
45
|
+
TreeInstanceId,
|
|
9
46
|
TreeNode,
|
|
10
47
|
TreeProps,
|
|
48
|
+
TreeState,
|
|
11
49
|
TreeVariant,
|
|
12
50
|
UseTreeOptions,
|
|
13
51
|
} from "./type"
|