@pyreon/elements 0.24.5 → 0.25.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.
Files changed (71) hide show
  1. package/lib/index.js +5 -1
  2. package/package.json +11 -13
  3. package/src/Element/component.tsx +0 -315
  4. package/src/Element/constants.ts +0 -96
  5. package/src/Element/index.ts +0 -6
  6. package/src/Element/types.ts +0 -168
  7. package/src/Element/utils.ts +0 -15
  8. package/src/List/component.tsx +0 -105
  9. package/src/List/index.ts +0 -5
  10. package/src/Overlay/component.tsx +0 -140
  11. package/src/Overlay/context.tsx +0 -36
  12. package/src/Overlay/index.ts +0 -7
  13. package/src/Overlay/positioning.ts +0 -191
  14. package/src/Overlay/useOverlay.tsx +0 -461
  15. package/src/Portal/component.tsx +0 -54
  16. package/src/Portal/index.ts +0 -5
  17. package/src/Text/component.tsx +0 -67
  18. package/src/Text/index.ts +0 -5
  19. package/src/Text/styled.ts +0 -30
  20. package/src/Util/component.tsx +0 -43
  21. package/src/Util/index.ts +0 -5
  22. package/src/__tests__/Content.test.tsx +0 -123
  23. package/src/__tests__/Element-slot-reactivity.browser.test.tsx +0 -152
  24. package/src/__tests__/Element.test.ts +0 -819
  25. package/src/__tests__/Iterator.test.ts +0 -492
  26. package/src/__tests__/Iterator.types.test.ts +0 -237
  27. package/src/__tests__/List.test.ts +0 -199
  28. package/src/__tests__/Overlay.test.ts +0 -492
  29. package/src/__tests__/Portal.test.ts +0 -156
  30. package/src/__tests__/Text.test.ts +0 -274
  31. package/src/__tests__/Util.test.ts +0 -63
  32. package/src/__tests__/Wrapper-innerhtml.test.tsx +0 -178
  33. package/src/__tests__/Wrapper.test.tsx +0 -196
  34. package/src/__tests__/elements.browser.test.tsx +0 -132
  35. package/src/__tests__/equalBeforeAfter.test.ts +0 -122
  36. package/src/__tests__/helpers.test.ts +0 -65
  37. package/src/__tests__/integration.test.tsx +0 -118
  38. package/src/__tests__/internElementBundle.test.ts +0 -102
  39. package/src/__tests__/iterator-function-children.test.tsx +0 -120
  40. package/src/__tests__/native-markers.test.ts +0 -13
  41. package/src/__tests__/overlayContext.test.tsx +0 -78
  42. package/src/__tests__/perf-stress.browser.test.tsx +0 -119
  43. package/src/__tests__/positioning.test.ts +0 -90
  44. package/src/__tests__/responsiveProps.test.ts +0 -328
  45. package/src/__tests__/slot-component-reference.test.tsx +0 -157
  46. package/src/__tests__/useOverlay.test.ts +0 -1336
  47. package/src/__tests__/utils.test.ts +0 -69
  48. package/src/__tests__/wrapper-block-cascade.test.ts +0 -121
  49. package/src/constants.ts +0 -1
  50. package/src/env.d.ts +0 -6
  51. package/src/helpers/Content/component.tsx +0 -75
  52. package/src/helpers/Content/index.ts +0 -3
  53. package/src/helpers/Content/styled.ts +0 -105
  54. package/src/helpers/Content/types.ts +0 -49
  55. package/src/helpers/Iterator/component.tsx +0 -316
  56. package/src/helpers/Iterator/index.ts +0 -30
  57. package/src/helpers/Iterator/types.ts +0 -138
  58. package/src/helpers/Wrapper/component.tsx +0 -180
  59. package/src/helpers/Wrapper/constants.ts +0 -10
  60. package/src/helpers/Wrapper/index.ts +0 -3
  61. package/src/helpers/Wrapper/styled.ts +0 -64
  62. package/src/helpers/Wrapper/types.ts +0 -56
  63. package/src/helpers/Wrapper/utils.ts +0 -7
  64. package/src/helpers/index.ts +0 -4
  65. package/src/helpers/internElementBundle.ts +0 -37
  66. package/src/helpers/isPyreonComponent.ts +0 -46
  67. package/src/index.ts +0 -42
  68. package/src/manifest.ts +0 -190
  69. package/src/tests/manifest-snapshot.test.ts +0 -45
  70. package/src/types.ts +0 -112
  71. 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
- })