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