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