@pyreon/elements 0.11.0 → 0.11.2
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/package.json +14 -12
- package/src/Element/component.tsx +211 -0
- package/src/Element/constants.ts +96 -0
- package/src/Element/index.ts +6 -0
- package/src/Element/types.ts +168 -0
- package/src/Element/utils.ts +15 -0
- package/src/List/component.tsx +57 -0
- package/src/List/index.ts +5 -0
- package/src/Overlay/component.tsx +131 -0
- package/src/Overlay/context.tsx +37 -0
- package/src/Overlay/index.ts +7 -0
- package/src/Overlay/useOverlay.tsx +616 -0
- package/src/Portal/component.tsx +41 -0
- package/src/Portal/index.ts +5 -0
- package/src/Text/component.tsx +65 -0
- package/src/Text/index.ts +5 -0
- package/src/Text/styled.ts +30 -0
- package/src/Util/component.tsx +43 -0
- package/src/Util/index.ts +5 -0
- package/src/__tests__/Content.test.tsx +115 -0
- package/src/__tests__/Element.test.ts +604 -0
- package/src/__tests__/Iterator.test.ts +483 -0
- package/src/__tests__/List.test.ts +199 -0
- package/src/__tests__/Overlay.test.ts +485 -0
- package/src/__tests__/Portal.test.ts +82 -0
- package/src/__tests__/Text.test.ts +274 -0
- package/src/__tests__/Util.test.ts +63 -0
- package/src/__tests__/Wrapper.test.tsx +152 -0
- package/src/__tests__/equalBeforeAfter.test.ts +122 -0
- package/src/__tests__/helpers.test.ts +65 -0
- package/src/__tests__/overlayContext.test.tsx +78 -0
- package/src/__tests__/responsiveProps.test.ts +298 -0
- package/src/__tests__/useOverlay.test.ts +1330 -0
- package/src/__tests__/utils.test.ts +69 -0
- package/src/constants.ts +1 -0
- package/src/helpers/Content/component.tsx +51 -0
- package/src/helpers/Content/index.ts +3 -0
- package/src/helpers/Content/styled.ts +105 -0
- package/src/helpers/Content/types.ts +49 -0
- package/src/helpers/Iterator/component.tsx +252 -0
- package/src/helpers/Iterator/index.ts +13 -0
- package/src/helpers/Iterator/types.ts +79 -0
- package/src/helpers/Wrapper/component.tsx +78 -0
- package/src/helpers/Wrapper/constants.ts +10 -0
- package/src/helpers/Wrapper/index.ts +3 -0
- package/src/helpers/Wrapper/styled.ts +69 -0
- package/src/helpers/Wrapper/types.ts +56 -0
- package/src/helpers/Wrapper/utils.ts +7 -0
- package/src/helpers/index.ts +4 -0
- package/src/index.ts +37 -0
- package/src/types.ts +81 -0
- package/src/utils.ts +1 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import type { ComponentFn, VNode, VNodeChild } from "@pyreon/core"
|
|
2
|
+
import { describe, expect, it, vi } from "vitest"
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Mocks — signal() in @pyreon/reactivity returns a Signal object (callable
|
|
6
|
+
// with .set/.update methods), but useOverlay destructures it as a tuple
|
|
7
|
+
// [getter, setter]. We mock signal() to return a simple tuple.
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
vi.mock("@pyreon/reactivity", () => {
|
|
11
|
+
const signal = <T>(initial: T) => {
|
|
12
|
+
let value = initial
|
|
13
|
+
const s = (() => value) as (() => T) & {
|
|
14
|
+
set: (v: T) => void
|
|
15
|
+
update: (fn: (c: T) => T) => void
|
|
16
|
+
peek: () => T
|
|
17
|
+
subscribe: (listener: () => void) => () => void
|
|
18
|
+
direct: (updater: () => void) => () => void
|
|
19
|
+
label: string | undefined
|
|
20
|
+
debug: () => { name: string | undefined; value: T; subscriberCount: number }
|
|
21
|
+
}
|
|
22
|
+
s.set = (v: T) => {
|
|
23
|
+
value = v
|
|
24
|
+
}
|
|
25
|
+
s.update = (fn: (c: T) => T) => {
|
|
26
|
+
value = fn(value)
|
|
27
|
+
}
|
|
28
|
+
s.peek = () => value
|
|
29
|
+
s.subscribe = () => () => {
|
|
30
|
+
/* noop */
|
|
31
|
+
}
|
|
32
|
+
s.direct = () => () => {
|
|
33
|
+
/* noop */
|
|
34
|
+
}
|
|
35
|
+
s.label = undefined
|
|
36
|
+
s.debug = () => ({ name: undefined, value, subscriberCount: 0 })
|
|
37
|
+
return s
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { signal }
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// onMount / onUnmount are no-ops outside a renderer
|
|
44
|
+
vi.mock("@pyreon/core", async (importOriginal) => {
|
|
45
|
+
const actual = (await importOriginal()) as Record<string, unknown>
|
|
46
|
+
return {
|
|
47
|
+
...actual,
|
|
48
|
+
onMount: vi.fn(),
|
|
49
|
+
onUnmount: vi.fn(),
|
|
50
|
+
Portal: actual.Fragment, // Portal stub — just renders children like a Fragment
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// render + throttle from @pyreon/ui-core
|
|
55
|
+
vi.mock("@pyreon/ui-core", async () => {
|
|
56
|
+
const { h: createElement } = await import("@pyreon/core")
|
|
57
|
+
|
|
58
|
+
const render = (content: unknown, attachProps?: Record<string, unknown>) => {
|
|
59
|
+
if (!content) return null
|
|
60
|
+
const t = typeof content
|
|
61
|
+
if (t === "string" || t === "number" || t === "boolean" || t === "bigint") {
|
|
62
|
+
return content as VNodeChild
|
|
63
|
+
}
|
|
64
|
+
if (Array.isArray(content)) return content as VNodeChild
|
|
65
|
+
if (typeof content === "function") {
|
|
66
|
+
return createElement(content as ComponentFn, (attachProps ?? {}) as any)
|
|
67
|
+
}
|
|
68
|
+
if (typeof content === "object") {
|
|
69
|
+
return content as VNodeChild
|
|
70
|
+
}
|
|
71
|
+
return content as VNodeChild
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const throttle = <F extends (...args: any[]) => any>(fn: F, _delay: number) => {
|
|
75
|
+
const wrapped = (...args: any[]) => fn(...args)
|
|
76
|
+
wrapped.cancel = () => {
|
|
77
|
+
/* no-op */
|
|
78
|
+
}
|
|
79
|
+
return wrapped as F & { cancel: () => void }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { render, throttle }
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// @pyreon/unistyle — value() used in assignContentPosition
|
|
86
|
+
vi.mock("@pyreon/unistyle", () => ({
|
|
87
|
+
value: (v: unknown) => (typeof v === "number" ? `${v}px` : v),
|
|
88
|
+
}))
|
|
89
|
+
|
|
90
|
+
import { Fragment, h } from "@pyreon/core"
|
|
91
|
+
import { Overlay, useOverlay } from "../Overlay"
|
|
92
|
+
|
|
93
|
+
const asVNode = (v: unknown) => v as VNode
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// useOverlay
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
describe("useOverlay", () => {
|
|
99
|
+
describe("active state", () => {
|
|
100
|
+
it("starts inactive by default", () => {
|
|
101
|
+
const overlay = useOverlay()
|
|
102
|
+
expect(overlay.active()).toBe(false)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it("starts active when isOpen is true", () => {
|
|
106
|
+
const overlay = useOverlay({ isOpen: true })
|
|
107
|
+
expect(overlay.active()).toBe(true)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it("showContent sets active to true", () => {
|
|
111
|
+
const overlay = useOverlay()
|
|
112
|
+
overlay.showContent()
|
|
113
|
+
expect(overlay.active()).toBe(true)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it("hideContent sets active to false", () => {
|
|
117
|
+
const overlay = useOverlay({ isOpen: true })
|
|
118
|
+
overlay.hideContent()
|
|
119
|
+
expect(overlay.active()).toBe(false)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it("showContent is idempotent", () => {
|
|
123
|
+
const overlay = useOverlay()
|
|
124
|
+
overlay.showContent()
|
|
125
|
+
overlay.showContent()
|
|
126
|
+
expect(overlay.active()).toBe(true)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it("hideContent is idempotent", () => {
|
|
130
|
+
const overlay = useOverlay()
|
|
131
|
+
overlay.hideContent()
|
|
132
|
+
overlay.hideContent()
|
|
133
|
+
expect(overlay.active()).toBe(false)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it("toggle between show/hide works", () => {
|
|
137
|
+
const overlay = useOverlay()
|
|
138
|
+
overlay.showContent()
|
|
139
|
+
expect(overlay.active()).toBe(true)
|
|
140
|
+
overlay.hideContent()
|
|
141
|
+
expect(overlay.active()).toBe(false)
|
|
142
|
+
overlay.showContent()
|
|
143
|
+
expect(overlay.active()).toBe(true)
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe("callbacks", () => {
|
|
148
|
+
it("calls onOpen when showing content", () => {
|
|
149
|
+
let opened = false
|
|
150
|
+
const overlay = useOverlay({
|
|
151
|
+
onOpen: () => {
|
|
152
|
+
opened = true
|
|
153
|
+
},
|
|
154
|
+
})
|
|
155
|
+
overlay.showContent()
|
|
156
|
+
expect(opened).toBe(true)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it("calls onClose when hiding content", () => {
|
|
160
|
+
let closed = false
|
|
161
|
+
const overlay = useOverlay({
|
|
162
|
+
isOpen: true,
|
|
163
|
+
onClose: () => {
|
|
164
|
+
closed = true
|
|
165
|
+
},
|
|
166
|
+
})
|
|
167
|
+
overlay.hideContent()
|
|
168
|
+
expect(closed).toBe(true)
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
describe("alignment signals", () => {
|
|
173
|
+
it("exposes alignX signal with default", () => {
|
|
174
|
+
const overlay = useOverlay({ alignX: "center" })
|
|
175
|
+
expect(overlay.alignX()).toBe("center")
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it("exposes alignY signal with default", () => {
|
|
179
|
+
const overlay = useOverlay({ alignY: "top" })
|
|
180
|
+
expect(overlay.alignY()).toBe("top")
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it("defaults alignX to left", () => {
|
|
184
|
+
const overlay = useOverlay()
|
|
185
|
+
expect(overlay.alignX()).toBe("left")
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it("defaults alignY to bottom", () => {
|
|
189
|
+
const overlay = useOverlay()
|
|
190
|
+
expect(overlay.alignY()).toBe("bottom")
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
describe("ref callbacks", () => {
|
|
195
|
+
it("provides triggerRef callback", () => {
|
|
196
|
+
const overlay = useOverlay()
|
|
197
|
+
expect(typeof overlay.triggerRef).toBe("function")
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it("provides contentRef callback", () => {
|
|
201
|
+
const overlay = useOverlay()
|
|
202
|
+
expect(typeof overlay.contentRef).toBe("function")
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
describe("blocked state", () => {
|
|
207
|
+
it("starts unblocked", () => {
|
|
208
|
+
const overlay = useOverlay()
|
|
209
|
+
expect(overlay.blocked()).toBe(false)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it("setBlocked increments blocked count", () => {
|
|
213
|
+
const overlay = useOverlay()
|
|
214
|
+
overlay.setBlocked()
|
|
215
|
+
expect(overlay.blocked()).toBe(true)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it("setUnblocked decrements blocked count", () => {
|
|
219
|
+
const overlay = useOverlay()
|
|
220
|
+
overlay.setBlocked()
|
|
221
|
+
overlay.setUnblocked()
|
|
222
|
+
expect(overlay.blocked()).toBe(false)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it("multiple setBlocked calls require equal setUnblocked calls", () => {
|
|
226
|
+
const overlay = useOverlay()
|
|
227
|
+
overlay.setBlocked()
|
|
228
|
+
overlay.setBlocked()
|
|
229
|
+
overlay.setUnblocked()
|
|
230
|
+
expect(overlay.blocked()).toBe(true)
|
|
231
|
+
overlay.setUnblocked()
|
|
232
|
+
expect(overlay.blocked()).toBe(false)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it("setUnblocked does not go below zero", () => {
|
|
236
|
+
const overlay = useOverlay()
|
|
237
|
+
overlay.setUnblocked()
|
|
238
|
+
overlay.setUnblocked()
|
|
239
|
+
expect(overlay.blocked()).toBe(false)
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
describe("setupListeners", () => {
|
|
244
|
+
it("returns a cleanup function", () => {
|
|
245
|
+
const overlay = useOverlay()
|
|
246
|
+
const cleanup = overlay.setupListeners()
|
|
247
|
+
expect(typeof cleanup).toBe("function")
|
|
248
|
+
cleanup()
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
describe("disabled state", () => {
|
|
253
|
+
it("forces active to false when disabled", () => {
|
|
254
|
+
const overlay = useOverlay({ isOpen: true, disabled: true })
|
|
255
|
+
expect(overlay.active()).toBe(false)
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
describe("each hook instance has independent state", () => {
|
|
260
|
+
it("two useOverlay instances do not share state", () => {
|
|
261
|
+
const overlay1 = useOverlay()
|
|
262
|
+
const overlay2 = useOverlay()
|
|
263
|
+
|
|
264
|
+
overlay1.showContent()
|
|
265
|
+
expect(overlay1.active()).toBe(true)
|
|
266
|
+
expect(overlay2.active()).toBe(false)
|
|
267
|
+
|
|
268
|
+
overlay2.showContent()
|
|
269
|
+
expect(overlay1.active()).toBe(true)
|
|
270
|
+
expect(overlay2.active()).toBe(true)
|
|
271
|
+
|
|
272
|
+
overlay1.hideContent()
|
|
273
|
+
expect(overlay1.active()).toBe(false)
|
|
274
|
+
expect(overlay2.active()).toBe(true)
|
|
275
|
+
})
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
describe("Provider", () => {
|
|
279
|
+
it("exposes Provider component", () => {
|
|
280
|
+
const overlay = useOverlay()
|
|
281
|
+
expect(typeof overlay.Provider).toBe("function")
|
|
282
|
+
})
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
describe("static align property", () => {
|
|
286
|
+
it("returns align value passed in props", () => {
|
|
287
|
+
const overlay = useOverlay({ align: "top" })
|
|
288
|
+
expect(overlay.align).toBe("top")
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it("defaults align to bottom", () => {
|
|
292
|
+
const overlay = useOverlay()
|
|
293
|
+
expect(overlay.align).toBe("bottom")
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
// Overlay component
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
describe("Overlay component", () => {
|
|
302
|
+
describe("VNode structure", () => {
|
|
303
|
+
it("returns a Fragment", () => {
|
|
304
|
+
const result = asVNode(
|
|
305
|
+
Overlay({
|
|
306
|
+
trigger: h("button", null, "Click"),
|
|
307
|
+
children: h("div", null, "Panel"),
|
|
308
|
+
}),
|
|
309
|
+
)
|
|
310
|
+
expect(result.type).toBe(Fragment)
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it("has trigger as first child and reactive function as second child", () => {
|
|
314
|
+
const result = asVNode(
|
|
315
|
+
Overlay({
|
|
316
|
+
trigger: h("button", null, "Click"),
|
|
317
|
+
children: h("div", null, "Panel"),
|
|
318
|
+
}),
|
|
319
|
+
)
|
|
320
|
+
expect(result.children.length).toBe(2)
|
|
321
|
+
expect(typeof result.children[1]).toBe("function")
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
it("content function returns null when closed", () => {
|
|
325
|
+
const result = asVNode(
|
|
326
|
+
Overlay({
|
|
327
|
+
trigger: h("button", null, "Click"),
|
|
328
|
+
children: h("div", null, "Panel"),
|
|
329
|
+
}),
|
|
330
|
+
)
|
|
331
|
+
const contentFn = result.children[1] as () => VNodeChild
|
|
332
|
+
expect(contentFn()).toBeNull()
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
it("content function returns Portal VNode when opened via isOpen", () => {
|
|
336
|
+
const result = asVNode(
|
|
337
|
+
Overlay({
|
|
338
|
+
trigger: h("button", null, "Click"),
|
|
339
|
+
children: h("div", null, "Panel"),
|
|
340
|
+
isOpen: true,
|
|
341
|
+
}),
|
|
342
|
+
)
|
|
343
|
+
const contentFn = result.children[1] as () => VNodeChild
|
|
344
|
+
// Portal is mocked as Fragment, so we just check it returns something
|
|
345
|
+
expect(contentFn()).not.toBeNull()
|
|
346
|
+
})
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
describe("trigger rendered via ComponentFn receives overlay props", () => {
|
|
350
|
+
it("passes active and aria props to trigger component", () => {
|
|
351
|
+
const TriggerComp: ComponentFn = (props: any) =>
|
|
352
|
+
h("button", null, props.active ? "Open" : "Closed")
|
|
353
|
+
|
|
354
|
+
const result = asVNode(
|
|
355
|
+
Overlay({
|
|
356
|
+
trigger: TriggerComp,
|
|
357
|
+
children: h("div", null, "Panel"),
|
|
358
|
+
}),
|
|
359
|
+
)
|
|
360
|
+
const triggerVNode = asVNode(result.children[0])
|
|
361
|
+
expect(triggerVNode.type).toBe(TriggerComp)
|
|
362
|
+
expect(triggerVNode.props.active).toBe(false)
|
|
363
|
+
expect(triggerVNode.props["aria-expanded"]).toBe(false)
|
|
364
|
+
expect(triggerVNode.props["aria-haspopup"]).toBe("menu")
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
it("passes active=true when isOpen is true", () => {
|
|
368
|
+
const TriggerComp: ComponentFn = (_props: any) => h("button", null, "T")
|
|
369
|
+
|
|
370
|
+
const result = asVNode(
|
|
371
|
+
Overlay({
|
|
372
|
+
trigger: TriggerComp,
|
|
373
|
+
children: h("div", null, "Panel"),
|
|
374
|
+
isOpen: true,
|
|
375
|
+
}),
|
|
376
|
+
)
|
|
377
|
+
const triggerVNode = asVNode(result.children[0])
|
|
378
|
+
expect(triggerVNode.props.active).toBe(true)
|
|
379
|
+
expect(triggerVNode.props["aria-expanded"]).toBe(true)
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
it("passes aria-haspopup=dialog for modal type", () => {
|
|
383
|
+
const TriggerComp: ComponentFn = (_props: any) => h("button", null, "T")
|
|
384
|
+
|
|
385
|
+
const result = asVNode(
|
|
386
|
+
Overlay({
|
|
387
|
+
trigger: TriggerComp,
|
|
388
|
+
children: h("div", null, "Panel"),
|
|
389
|
+
type: "modal",
|
|
390
|
+
}),
|
|
391
|
+
)
|
|
392
|
+
const triggerVNode = asVNode(result.children[0])
|
|
393
|
+
expect(triggerVNode.props["aria-haspopup"]).toBe("dialog")
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
it("passes aria-haspopup=true for tooltip type", () => {
|
|
397
|
+
const TriggerComp: ComponentFn = (_props: any) => h("button", null, "T")
|
|
398
|
+
|
|
399
|
+
const result = asVNode(
|
|
400
|
+
Overlay({
|
|
401
|
+
trigger: TriggerComp,
|
|
402
|
+
children: h("div", null, "Panel"),
|
|
403
|
+
type: "tooltip",
|
|
404
|
+
}),
|
|
405
|
+
)
|
|
406
|
+
const triggerVNode = asVNode(result.children[0])
|
|
407
|
+
expect(triggerVNode.props["aria-haspopup"]).toBe("true")
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
it("passes ref via triggerRefName prop", () => {
|
|
411
|
+
const TriggerComp: ComponentFn = (_props: any) => h("button", null, "T")
|
|
412
|
+
|
|
413
|
+
const result = asVNode(
|
|
414
|
+
Overlay({
|
|
415
|
+
trigger: TriggerComp,
|
|
416
|
+
children: h("div", null, "Panel"),
|
|
417
|
+
triggerRefName: "innerRef",
|
|
418
|
+
}),
|
|
419
|
+
)
|
|
420
|
+
const triggerVNode = asVNode(result.children[0])
|
|
421
|
+
expect(typeof triggerVNode.props.innerRef).toBe("function")
|
|
422
|
+
// default 'ref' should not be set
|
|
423
|
+
expect(triggerVNode.props.ref).toBeUndefined()
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
it("passes showContent/hideContent for manual openOn", () => {
|
|
427
|
+
const TriggerComp: ComponentFn = (_props: any) => h("button", null, "T")
|
|
428
|
+
|
|
429
|
+
const result = asVNode(
|
|
430
|
+
Overlay({
|
|
431
|
+
trigger: TriggerComp,
|
|
432
|
+
children: h("div", null, "Panel"),
|
|
433
|
+
openOn: "manual",
|
|
434
|
+
}),
|
|
435
|
+
)
|
|
436
|
+
const triggerVNode = asVNode(result.children[0])
|
|
437
|
+
expect(typeof triggerVNode.props.showContent).toBe("function")
|
|
438
|
+
expect(typeof triggerVNode.props.hideContent).toBe("function")
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
it("does not pass showContent/hideContent for click openOn", () => {
|
|
442
|
+
const TriggerComp: ComponentFn = (_props: any) => h("button", null, "T")
|
|
443
|
+
|
|
444
|
+
const result = asVNode(
|
|
445
|
+
Overlay({
|
|
446
|
+
trigger: TriggerComp,
|
|
447
|
+
children: h("div", null, "Panel"),
|
|
448
|
+
openOn: "click",
|
|
449
|
+
closeOn: "click",
|
|
450
|
+
}),
|
|
451
|
+
)
|
|
452
|
+
const triggerVNode = asVNode(result.children[0])
|
|
453
|
+
expect(triggerVNode.props.showContent).toBeUndefined()
|
|
454
|
+
expect(triggerVNode.props.hideContent).toBeUndefined()
|
|
455
|
+
})
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
describe("trigger as VNode is passed through", () => {
|
|
459
|
+
it("returns trigger VNode as-is when not a function", () => {
|
|
460
|
+
const trigger = h("button", { id: "btn" }, "Click")
|
|
461
|
+
const result = asVNode(
|
|
462
|
+
Overlay({
|
|
463
|
+
trigger,
|
|
464
|
+
children: h("div", null, "Panel"),
|
|
465
|
+
}),
|
|
466
|
+
)
|
|
467
|
+
// render() passes VNode objects through directly
|
|
468
|
+
const triggerChild = asVNode(result.children[0])
|
|
469
|
+
expect(triggerChild.type).toBe("button")
|
|
470
|
+
expect(triggerChild.props.id).toBe("btn")
|
|
471
|
+
})
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
describe("displayName and metadata", () => {
|
|
475
|
+
it("has displayName set", () => {
|
|
476
|
+
expect(Overlay.displayName).toBeDefined()
|
|
477
|
+
expect(Overlay.displayName).toContain("Overlay")
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
it("has PYREON__COMPONENT set", () => {
|
|
481
|
+
expect(Overlay.PYREON__COMPONENT).toBeDefined()
|
|
482
|
+
expect(Overlay.PYREON__COMPONENT).toContain("Overlay")
|
|
483
|
+
})
|
|
484
|
+
})
|
|
485
|
+
})
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { VNode } from "@pyreon/core"
|
|
2
|
+
import { Portal as CorePortal, h } from "@pyreon/core"
|
|
3
|
+
import { describe, expect, it } from "vitest"
|
|
4
|
+
import { Portal } from "../Portal"
|
|
5
|
+
|
|
6
|
+
const asVNode = (v: unknown) => v as VNode
|
|
7
|
+
|
|
8
|
+
describe("Portal", () => {
|
|
9
|
+
describe("rendering", () => {
|
|
10
|
+
it("returns a VNode whose type is CorePortal", () => {
|
|
11
|
+
const child = h("div", null, "modal content")
|
|
12
|
+
const result = asVNode(Portal({ children: child }))
|
|
13
|
+
expect(result.type).toBe(CorePortal)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it("defaults target to document.body when DOMLocation is not provided", () => {
|
|
17
|
+
const child = h("div", null, "content")
|
|
18
|
+
const result = asVNode(Portal({ children: child }))
|
|
19
|
+
const props = result.props as Record<string, unknown>
|
|
20
|
+
expect(props.target).toBe(document.body)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it("uses DOMLocation as target when provided", () => {
|
|
24
|
+
const customTarget = document.createElement("div")
|
|
25
|
+
const child = h("span", null, "inside")
|
|
26
|
+
const result = asVNode(Portal({ DOMLocation: customTarget, children: child }))
|
|
27
|
+
const props = result.props as Record<string, unknown>
|
|
28
|
+
expect(props.target).toBe(customTarget)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("passes children through to the CorePortal VNode", () => {
|
|
32
|
+
const child = h("div", { class: "modal" }, "Modal content")
|
|
33
|
+
const result = asVNode(Portal({ children: child }))
|
|
34
|
+
const props = result.props as Record<string, unknown>
|
|
35
|
+
expect(props.children).toBe(child)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it("passes string children", () => {
|
|
39
|
+
const result = asVNode(Portal({ children: "text content" }))
|
|
40
|
+
const props = result.props as Record<string, unknown>
|
|
41
|
+
expect(props.children).toBe("text content")
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("passes number children", () => {
|
|
45
|
+
const result = asVNode(Portal({ children: 42 }))
|
|
46
|
+
const props = result.props as Record<string, unknown>
|
|
47
|
+
expect(props.children).toBe(42)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it("passes nested VNode children", () => {
|
|
51
|
+
const nested = h("div", null, h("span", null, "level 1"), h("span", null, "level 2"))
|
|
52
|
+
const result = asVNode(Portal({ children: nested }))
|
|
53
|
+
const props = result.props as Record<string, unknown>
|
|
54
|
+
const childVNode = asVNode(props.children)
|
|
55
|
+
expect(childVNode.type).toBe("div")
|
|
56
|
+
expect(childVNode.children).toHaveLength(2)
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe("tag prop", () => {
|
|
61
|
+
it("accepts tag prop without affecting output type", () => {
|
|
62
|
+
const child = h("div", null, "content")
|
|
63
|
+
const result = asVNode(Portal({ tag: "section", children: child }))
|
|
64
|
+
// tag is accepted but not used — output is still a Portal VNode
|
|
65
|
+
expect(result.type).toBe(CorePortal)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe("statics", () => {
|
|
70
|
+
it("has correct displayName", () => {
|
|
71
|
+
expect(Portal.displayName).toBe("@pyreon/elements/Portal")
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it("has correct pkgName", () => {
|
|
75
|
+
expect(Portal.pkgName).toBe("@pyreon/elements")
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it("has correct PYREON__COMPONENT", () => {
|
|
79
|
+
expect(Portal.PYREON__COMPONENT).toBe("@pyreon/elements/Portal")
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
})
|