@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,1336 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
-
3
- // ---------------------------------------------------------------------------
4
- // Mocks
5
- // ---------------------------------------------------------------------------
6
-
7
- vi.mock('@pyreon/reactivity', () => {
8
- const signal = <T>(initial: T) => {
9
- let value = initial
10
- const s = (() => value) as (() => T) & {
11
- set: (v: T) => void
12
- update: (fn: (c: T) => T) => void
13
- peek: () => T
14
- subscribe: (listener: () => void) => () => void
15
- direct: (updater: () => void) => () => void
16
- label: string | undefined
17
- debug: () => { name: string | undefined; value: T; subscriberCount: number }
18
- }
19
- s.set = (v: T) => {
20
- value = v
21
- }
22
- s.update = (fn: (c: T) => T) => {
23
- value = fn(value)
24
- }
25
- s.peek = () => value
26
- s.subscribe = () => () => {
27
- /* noop */
28
- }
29
- s.direct = () => () => {
30
- /* noop */
31
- }
32
- s.label = undefined
33
- s.debug = () => ({ name: undefined, value, subscriberCount: 0 })
34
- return s
35
- }
36
-
37
- // See sibling Overlay.test.ts mock: `@pyreon/core` imports
38
- // `setSnapshotCapture` and calls it at module load to install the
39
- // reactive-effect context-snapshot DI hook. The mock provides a no-op
40
- // so the `@pyreon/core` import doesn't throw "No 'setSnapshotCapture'
41
- // export is defined on the '@pyreon/reactivity' mock."
42
- const setSnapshotCapture = () => {}
43
- return { signal, setSnapshotCapture }
44
- })
45
-
46
- vi.mock('@pyreon/core', async (importOriginal) => {
47
- const actual = (await importOriginal()) as Record<string, unknown>
48
- return {
49
- ...actual,
50
- onMount: vi.fn(),
51
- onUnmount: vi.fn(),
52
- Portal: actual.Fragment,
53
- }
54
- })
55
-
56
- vi.mock('@pyreon/ui-core', async () => {
57
- const throttle = <F extends (...args: any[]) => any>(fn: F, _delay: number) => {
58
- const wrapped = (...args: any[]) => fn(...args)
59
- wrapped.cancel = () => {
60
- /* no-op */
61
- }
62
- return wrapped as F & { cancel: () => void }
63
- }
64
-
65
- return { render: vi.fn(), throttle }
66
- })
67
-
68
- vi.mock('@pyreon/unistyle', () => ({
69
- value: (v: unknown, _base?: number) => (typeof v === 'number' ? `${v}px` : v),
70
- }))
71
-
72
- const mockSetBlocked = vi.fn()
73
- const mockSetUnblocked = vi.fn()
74
-
75
- vi.mock('../Overlay/context', async (importOriginal) => {
76
- const actual = (await importOriginal()) as Record<string, unknown>
77
- return {
78
- ...actual,
79
- useOverlayContext: () => ({
80
- setBlocked: mockSetBlocked,
81
- setUnblocked: mockSetUnblocked,
82
- }),
83
- }
84
- })
85
-
86
- vi.mock('~/utils', () => ({
87
- IS_DEVELOPMENT: false,
88
- }))
89
-
90
- import { useOverlay } from '../Overlay'
91
-
92
- // ---------------------------------------------------------------------------
93
- // Helpers
94
- // ---------------------------------------------------------------------------
95
-
96
- const mockElement = (rect: Partial<DOMRect> = {}): HTMLElement => {
97
- const el = document.createElement('div')
98
- el.getBoundingClientRect = () => ({
99
- top: 0,
100
- bottom: 0,
101
- left: 0,
102
- right: 0,
103
- width: 0,
104
- height: 0,
105
- x: 0,
106
- y: 0,
107
- toJSON: () => {},
108
- ...rect,
109
- })
110
- return el
111
- }
112
-
113
- // Set viewport dimensions for position tests
114
- const setViewport = (width: number, height: number) => {
115
- Object.defineProperty(window, 'innerWidth', { value: width, configurable: true })
116
- Object.defineProperty(window, 'innerHeight', { value: height, configurable: true })
117
- }
118
-
119
- beforeEach(() => {
120
- vi.clearAllMocks()
121
- vi.useFakeTimers()
122
- setViewport(1024, 768)
123
- })
124
-
125
- afterEach(() => {
126
- vi.useRealTimers()
127
- })
128
-
129
- // ---------------------------------------------------------------------------
130
- // Tests
131
- // ---------------------------------------------------------------------------
132
-
133
- describe('useOverlay', () => {
134
- // =========================================================================
135
- // 1. Default state
136
- // =========================================================================
137
- describe('default state', () => {
138
- it('active is false by default', () => {
139
- const o = useOverlay()
140
- expect(o.active()).toBe(false)
141
- })
142
-
143
- it('align defaults to bottom', () => {
144
- const o = useOverlay()
145
- expect(o.align).toBe('bottom')
146
- })
147
-
148
- it('alignX defaults to left', () => {
149
- const o = useOverlay()
150
- expect(o.alignX()).toBe('left')
151
- })
152
-
153
- it('alignY defaults to bottom', () => {
154
- const o = useOverlay()
155
- expect(o.alignY()).toBe('bottom')
156
- })
157
-
158
- it('blocked is false by default', () => {
159
- const o = useOverlay()
160
- expect(o.blocked()).toBe(false)
161
- })
162
- })
163
-
164
- // =========================================================================
165
- // 2. isOpen=true
166
- // =========================================================================
167
- describe('isOpen=true', () => {
168
- it('active starts true when isOpen is true', () => {
169
- const o = useOverlay({ isOpen: true })
170
- expect(o.active()).toBe(true)
171
- })
172
- })
173
-
174
- // =========================================================================
175
- // 3. Disabled state
176
- // =========================================================================
177
- describe('disabled', () => {
178
- it('forces active to false when disabled is true', () => {
179
- const o = useOverlay({ isOpen: true, disabled: true })
180
- expect(o.active()).toBe(false)
181
- })
182
-
183
- it('prevents event handling when disabled', () => {
184
- const onOpen = vi.fn()
185
- const o = useOverlay({ openOn: 'click', disabled: true, onOpen })
186
- const triggerEl = mockElement()
187
- o.triggerRef(triggerEl)
188
- const cleanup = o.setupListeners()
189
-
190
- const click = new MouseEvent('click', { bubbles: true })
191
- Object.defineProperty(click, 'target', { value: triggerEl })
192
- window.dispatchEvent(click)
193
-
194
- expect(o.active()).toBe(false)
195
- expect(onOpen).not.toHaveBeenCalled()
196
- cleanup()
197
- })
198
- })
199
-
200
- // =========================================================================
201
- // 4. triggerRef / contentRef
202
- // =========================================================================
203
- describe('triggerRef and contentRef', () => {
204
- it('triggerRef is a callable function', () => {
205
- const o = useOverlay()
206
- expect(typeof o.triggerRef).toBe('function')
207
- })
208
-
209
- it('contentRef is a callable function', () => {
210
- const o = useOverlay()
211
- expect(typeof o.contentRef).toBe('function')
212
- })
213
-
214
- it('triggerRef accepts an HTMLElement', () => {
215
- const o = useOverlay()
216
- const el = mockElement()
217
- expect(() => o.triggerRef(el)).not.toThrow()
218
- })
219
-
220
- it('contentRef accepts an HTMLElement', () => {
221
- const o = useOverlay()
222
- const el = mockElement()
223
- expect(() => o.contentRef(el)).not.toThrow()
224
- })
225
-
226
- it('triggerRef accepts null', () => {
227
- const o = useOverlay()
228
- expect(() => o.triggerRef(null)).not.toThrow()
229
- })
230
-
231
- it('contentRef accepts null', () => {
232
- const o = useOverlay()
233
- expect(() => o.contentRef(null)).not.toThrow()
234
- })
235
- })
236
-
237
- // =========================================================================
238
- // 5. showContent / hideContent
239
- // =========================================================================
240
- describe('showContent / hideContent', () => {
241
- it('showContent sets active to true', () => {
242
- const o = useOverlay()
243
- o.showContent()
244
- expect(o.active()).toBe(true)
245
- })
246
-
247
- it('hideContent sets active to false', () => {
248
- const o = useOverlay({ isOpen: true })
249
- o.hideContent()
250
- expect(o.active()).toBe(false)
251
- })
252
-
253
- it('showContent calls onOpen callback', () => {
254
- const onOpen = vi.fn()
255
- const o = useOverlay({ onOpen })
256
- o.showContent()
257
- expect(onOpen).toHaveBeenCalledOnce()
258
- })
259
-
260
- it('hideContent calls onClose callback', () => {
261
- const onClose = vi.fn()
262
- const o = useOverlay({ isOpen: true, onClose })
263
- o.hideContent()
264
- expect(onClose).toHaveBeenCalledOnce()
265
- })
266
-
267
- it('showContent calls ctx.setBlocked', () => {
268
- const o = useOverlay()
269
- o.showContent()
270
- expect(mockSetBlocked).toHaveBeenCalledOnce()
271
- })
272
-
273
- it('hideContent calls ctx.setUnblocked', () => {
274
- const o = useOverlay({ isOpen: true })
275
- o.hideContent()
276
- expect(mockSetUnblocked).toHaveBeenCalledOnce()
277
- })
278
-
279
- it('toggle between show and hide works', () => {
280
- const o = useOverlay()
281
- o.showContent()
282
- expect(o.active()).toBe(true)
283
- o.hideContent()
284
- expect(o.active()).toBe(false)
285
- o.showContent()
286
- expect(o.active()).toBe(true)
287
- })
288
- })
289
-
290
- // =========================================================================
291
- // 6. Blocked state
292
- // =========================================================================
293
- describe('blocked state', () => {
294
- it('setBlocked increments blocked count', () => {
295
- const o = useOverlay()
296
- o.setBlocked()
297
- expect(o.blocked()).toBe(true)
298
- })
299
-
300
- it('setUnblocked decrements blocked count', () => {
301
- const o = useOverlay()
302
- o.setBlocked()
303
- o.setUnblocked()
304
- expect(o.blocked()).toBe(false)
305
- })
306
-
307
- it('multiple setBlocked calls require matching setUnblocked calls', () => {
308
- const o = useOverlay()
309
- o.setBlocked()
310
- o.setBlocked()
311
- o.setUnblocked()
312
- expect(o.blocked()).toBe(true)
313
- o.setUnblocked()
314
- expect(o.blocked()).toBe(false)
315
- })
316
-
317
- it('setUnblocked does not go below zero', () => {
318
- const o = useOverlay()
319
- o.setUnblocked()
320
- o.setUnblocked()
321
- expect(o.blocked()).toBe(false)
322
- })
323
-
324
- it('blocked overlay ignores click events', () => {
325
- const onOpen = vi.fn()
326
- const o = useOverlay({ openOn: 'click', onOpen })
327
- const triggerEl = mockElement()
328
- o.triggerRef(triggerEl)
329
- const cleanup = o.setupListeners()
330
-
331
- // Block the overlay
332
- o.setBlocked()
333
-
334
- const click = new MouseEvent('click', { bubbles: true })
335
- Object.defineProperty(click, 'target', { value: triggerEl })
336
- window.dispatchEvent(click)
337
-
338
- expect(o.active()).toBe(false)
339
- expect(onOpen).not.toHaveBeenCalled()
340
- cleanup()
341
- })
342
- })
343
-
344
- // =========================================================================
345
- // 7. setupListeners
346
- // =========================================================================
347
- describe('setupListeners', () => {
348
- it('returns a cleanup function', () => {
349
- const o = useOverlay()
350
- const cleanup = o.setupListeners()
351
- expect(typeof cleanup).toBe('function')
352
- cleanup()
353
- })
354
-
355
- it('cleanup removes event listeners without error', () => {
356
- const o = useOverlay()
357
- const cleanup = o.setupListeners()
358
- expect(() => cleanup()).not.toThrow()
359
- })
360
-
361
- it('cleanup can be called multiple times safely', () => {
362
- const o = useOverlay()
363
- const cleanup = o.setupListeners()
364
- cleanup()
365
- expect(() => cleanup()).not.toThrow()
366
- })
367
- })
368
-
369
- // =========================================================================
370
- // 8. Click handling
371
- // =========================================================================
372
- describe('click handling', () => {
373
- it('openOn=click: clicking trigger when inactive opens overlay', () => {
374
- const o = useOverlay({ openOn: 'click' })
375
- const triggerEl = mockElement()
376
- o.triggerRef(triggerEl)
377
- const cleanup = o.setupListeners()
378
-
379
- const click = new MouseEvent('click', { bubbles: true })
380
- Object.defineProperty(click, 'target', { value: triggerEl })
381
- window.dispatchEvent(click)
382
-
383
- expect(o.active()).toBe(true)
384
- cleanup()
385
- })
386
-
387
- it('openOn=click: clicking non-trigger when inactive does not open', () => {
388
- const o = useOverlay({ openOn: 'click' })
389
- const triggerEl = mockElement()
390
- o.triggerRef(triggerEl)
391
- const cleanup = o.setupListeners()
392
-
393
- const outsideEl = document.createElement('div')
394
- const click = new MouseEvent('click', { bubbles: true })
395
- Object.defineProperty(click, 'target', { value: outsideEl })
396
- window.dispatchEvent(click)
397
-
398
- expect(o.active()).toBe(false)
399
- cleanup()
400
- })
401
-
402
- it('closeOn=click: any click when active closes overlay', () => {
403
- const o = useOverlay({ openOn: 'click', closeOn: 'click', isOpen: true })
404
- const triggerEl = mockElement()
405
- o.triggerRef(triggerEl)
406
- const cleanup = o.setupListeners()
407
-
408
- const outsideEl = document.createElement('div')
409
- const click = new MouseEvent('click', { bubbles: true })
410
- Object.defineProperty(click, 'target', { value: outsideEl })
411
- window.dispatchEvent(click)
412
-
413
- expect(o.active()).toBe(false)
414
- cleanup()
415
- })
416
-
417
- it('closeOn=clickOnTrigger: clicking trigger when active closes overlay', () => {
418
- const o = useOverlay({ openOn: 'click', closeOn: 'clickOnTrigger', isOpen: true })
419
- const triggerEl = mockElement()
420
- o.triggerRef(triggerEl)
421
- const cleanup = o.setupListeners()
422
-
423
- const click = new MouseEvent('click', { bubbles: true })
424
- Object.defineProperty(click, 'target', { value: triggerEl })
425
- window.dispatchEvent(click)
426
-
427
- expect(o.active()).toBe(false)
428
- cleanup()
429
- })
430
-
431
- it('closeOn=clickOnTrigger: clicking outside does not close overlay', () => {
432
- const o = useOverlay({ openOn: 'click', closeOn: 'clickOnTrigger', isOpen: true })
433
- const triggerEl = mockElement()
434
- o.triggerRef(triggerEl)
435
- const cleanup = o.setupListeners()
436
-
437
- const outsideEl = document.createElement('div')
438
- const click = new MouseEvent('click', { bubbles: true })
439
- Object.defineProperty(click, 'target', { value: outsideEl })
440
- window.dispatchEvent(click)
441
-
442
- expect(o.active()).toBe(true)
443
- cleanup()
444
- })
445
-
446
- it('closeOn=clickOutsideContent: click outside content closes overlay', () => {
447
- const o = useOverlay({ openOn: 'click', closeOn: 'clickOutsideContent', isOpen: true })
448
- const triggerEl = mockElement()
449
- const contentEl = mockElement()
450
- o.triggerRef(triggerEl)
451
- o.contentRef(contentEl)
452
- const cleanup = o.setupListeners()
453
-
454
- const outsideEl = document.createElement('div')
455
- const click = new MouseEvent('click', { bubbles: true })
456
- Object.defineProperty(click, 'target', { value: outsideEl })
457
- window.dispatchEvent(click)
458
-
459
- expect(o.active()).toBe(false)
460
- cleanup()
461
- })
462
-
463
- it('closeOn=clickOutsideContent: click inside content does not close overlay', () => {
464
- const o = useOverlay({ openOn: 'click', closeOn: 'clickOutsideContent', isOpen: true })
465
- const triggerEl = mockElement()
466
- const contentEl = mockElement()
467
- const childEl = document.createElement('span')
468
- contentEl.appendChild(childEl)
469
- o.triggerRef(triggerEl)
470
- o.contentRef(contentEl)
471
- const cleanup = o.setupListeners()
472
-
473
- const click = new MouseEvent('click', { bubbles: true })
474
- Object.defineProperty(click, 'target', { value: childEl })
475
- window.dispatchEvent(click)
476
-
477
- expect(o.active()).toBe(true)
478
- cleanup()
479
- })
480
-
481
- it('click on trigger child element opens overlay (contains check)', () => {
482
- const o = useOverlay({ openOn: 'click' })
483
- const triggerEl = mockElement()
484
- const innerEl = document.createElement('span')
485
- triggerEl.appendChild(innerEl)
486
- o.triggerRef(triggerEl)
487
- const cleanup = o.setupListeners()
488
-
489
- const click = new MouseEvent('click', { bubbles: true })
490
- Object.defineProperty(click, 'target', { value: innerEl })
491
- window.dispatchEvent(click)
492
-
493
- expect(o.active()).toBe(true)
494
- cleanup()
495
- })
496
- })
497
-
498
- // =========================================================================
499
- // 9. ESC handling
500
- // =========================================================================
501
- describe('ESC handling', () => {
502
- it('closeOnEsc=true: Escape key when active closes overlay', () => {
503
- const o = useOverlay({ closeOnEsc: true, isOpen: true })
504
- const cleanup = o.setupListeners()
505
-
506
- const esc = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })
507
- window.dispatchEvent(esc)
508
-
509
- expect(o.active()).toBe(false)
510
- cleanup()
511
- })
512
-
513
- it('closeOnEsc=true: Escape key when inactive does nothing', () => {
514
- const onClose = vi.fn()
515
- const o = useOverlay({ closeOnEsc: true, onClose })
516
- const cleanup = o.setupListeners()
517
-
518
- const esc = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })
519
- window.dispatchEvent(esc)
520
-
521
- expect(o.active()).toBe(false)
522
- expect(onClose).not.toHaveBeenCalled()
523
- cleanup()
524
- })
525
-
526
- it('closeOnEsc=false: Escape key does not close overlay', () => {
527
- const o = useOverlay({ closeOnEsc: false, isOpen: true })
528
- const cleanup = o.setupListeners()
529
-
530
- const esc = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })
531
- window.dispatchEvent(esc)
532
-
533
- expect(o.active()).toBe(true)
534
- cleanup()
535
- })
536
-
537
- it('closeOnEsc: Escape does not close when blocked', () => {
538
- const o = useOverlay({ closeOnEsc: true, isOpen: true })
539
- const cleanup = o.setupListeners()
540
- o.setBlocked()
541
-
542
- const esc = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })
543
- window.dispatchEvent(esc)
544
-
545
- expect(o.active()).toBe(true)
546
- cleanup()
547
- })
548
-
549
- it('non-Escape key does not close overlay', () => {
550
- const o = useOverlay({ closeOnEsc: true, isOpen: true })
551
- const cleanup = o.setupListeners()
552
-
553
- const enter = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })
554
- window.dispatchEvent(enter)
555
-
556
- expect(o.active()).toBe(true)
557
- cleanup()
558
- })
559
- })
560
-
561
- // =========================================================================
562
- // 10. Hover handling
563
- // =========================================================================
564
- describe('hover handling', () => {
565
- it('openOn=hover: mouseenter on trigger opens overlay', () => {
566
- const o = useOverlay({ openOn: 'hover', closeOn: 'hover' })
567
- const triggerEl = mockElement()
568
- o.triggerRef(triggerEl)
569
- const cleanup = o.setupListeners()
570
-
571
- triggerEl.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }))
572
-
573
- expect(o.active()).toBe(true)
574
- cleanup()
575
- })
576
-
577
- it('closeOn=hover: mouseleave from trigger schedules hide with hoverDelay', () => {
578
- const o = useOverlay({ openOn: 'hover', closeOn: 'hover', hoverDelay: 100 })
579
- const triggerEl = mockElement()
580
- o.triggerRef(triggerEl)
581
- const cleanup = o.setupListeners()
582
-
583
- // Open first
584
- triggerEl.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }))
585
- expect(o.active()).toBe(true)
586
-
587
- // Leave trigger
588
- triggerEl.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true }))
589
- // Should still be active (delay not elapsed)
590
- expect(o.active()).toBe(true)
591
-
592
- // Advance timer
593
- vi.advanceTimersByTime(100)
594
- expect(o.active()).toBe(false)
595
- cleanup()
596
- })
597
-
598
- it('mouseenter on content cancels hide timeout', () => {
599
- const o = useOverlay({ openOn: 'hover', closeOn: 'hover', hoverDelay: 100 })
600
- const triggerEl = mockElement()
601
- const contentEl = mockElement()
602
- o.triggerRef(triggerEl)
603
- o.contentRef(contentEl)
604
- const cleanup = o.setupListeners()
605
-
606
- // Open
607
- triggerEl.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }))
608
- expect(o.active()).toBe(true)
609
-
610
- // Leave trigger (starts hide timer)
611
- triggerEl.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true }))
612
-
613
- // Enter content (cancels hide timer)
614
- contentEl.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }))
615
-
616
- // Advance past delay
617
- vi.advanceTimersByTime(200)
618
- expect(o.active()).toBe(true)
619
- cleanup()
620
- })
621
-
622
- it('mouseleave from content schedules hide', () => {
623
- const o = useOverlay({ openOn: 'hover', closeOn: 'hover', hoverDelay: 50 })
624
- const triggerEl = mockElement()
625
- const contentEl = mockElement()
626
- o.triggerRef(triggerEl)
627
- o.contentRef(contentEl)
628
- const cleanup = o.setupListeners()
629
-
630
- // Open
631
- triggerEl.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }))
632
- expect(o.active()).toBe(true)
633
-
634
- // Leave content
635
- contentEl.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true }))
636
- vi.advanceTimersByTime(50)
637
- expect(o.active()).toBe(false)
638
- cleanup()
639
- })
640
-
641
- it('hover: scroll event closes overlay when closeOn=hover and active', () => {
642
- const o = useOverlay({ openOn: 'hover', closeOn: 'hover' })
643
- const triggerEl = mockElement()
644
- o.triggerRef(triggerEl)
645
- const cleanup = o.setupListeners()
646
-
647
- // Open
648
- triggerEl.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }))
649
- expect(o.active()).toBe(true)
650
-
651
- // Scroll (should trigger processVisibilityEvent with closeOn=hover + scroll)
652
- window.dispatchEvent(new Event('scroll'))
653
- expect(o.active()).toBe(false)
654
- cleanup()
655
- })
656
-
657
- it('openOn=hover: mouseenter when already active does not call onOpen again', () => {
658
- const onOpen = vi.fn()
659
- const o = useOverlay({ openOn: 'hover', closeOn: 'hover', onOpen })
660
- const triggerEl = mockElement()
661
- o.triggerRef(triggerEl)
662
- const cleanup = o.setupListeners()
663
-
664
- triggerEl.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }))
665
- triggerEl.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }))
666
-
667
- expect(onOpen).toHaveBeenCalledOnce()
668
- cleanup()
669
- })
670
- })
671
-
672
- // =========================================================================
673
- // 11. Position calculation (dropdown)
674
- // =========================================================================
675
- describe('position calculation - dropdown', () => {
676
- const setupDropdown = (opts: Parameters<typeof useOverlay>[0] = {}) => {
677
- const o = useOverlay({
678
- type: 'dropdown',
679
- align: 'bottom',
680
- alignX: 'left',
681
- isOpen: true,
682
- ...opts,
683
- })
684
-
685
- const triggerEl = mockElement({
686
- top: 100,
687
- bottom: 130,
688
- left: 50,
689
- right: 150,
690
- width: 100,
691
- height: 30,
692
- })
693
- const contentEl = mockElement({
694
- top: 0,
695
- bottom: 200,
696
- left: 0,
697
- right: 200,
698
- width: 200,
699
- height: 200,
700
- })
701
-
702
- o.triggerRef(triggerEl)
703
- o.contentRef(contentEl)
704
-
705
- return { o, triggerEl, contentEl }
706
- }
707
-
708
- it('bottom align: positions content below trigger', () => {
709
- const { o, contentEl } = setupDropdown({ align: 'bottom', alignX: 'left' })
710
-
711
- // Trigger setContentPosition by calling showContent (which sets active)
712
- // active is already true and contentRef is set, but isContentLoaded is separate
713
- // contentRef callback sets isContentLoaded
714
-
715
- // After contentRef callback, isContentLoaded is true. We need to trigger position calc.
716
- // The hook doesn't auto-trigger - it exposes setupListeners. Let's simulate resize.
717
- const cleanup = o.setupListeners()
718
- window.dispatchEvent(new Event('resize'))
719
-
720
- // Content should be positioned: top = trigger.bottom + offsetY = 130
721
- expect(contentEl.style.top).toBe('130px')
722
- // left = trigger.left + offsetX = 50
723
- expect(contentEl.style.left).toBe('50px')
724
- cleanup()
725
- })
726
-
727
- it('top align: positions content above trigger', () => {
728
- const { o, contentEl } = setupDropdown({ align: 'top', alignX: 'left' })
729
- const cleanup = o.setupListeners()
730
- window.dispatchEvent(new Event('resize'))
731
-
732
- // top = trigger.top - offsetY - content.height = 100 - 0 - 200 = -100
733
- // Doesn't fit top (-100 < 0), so falls back to bottom: trigger.bottom + offsetY = 130
734
- expect(contentEl.style.top).toBe('130px')
735
- cleanup()
736
- })
737
-
738
- it('top align with room above: positions content above trigger', () => {
739
- const o = useOverlay({
740
- type: 'dropdown',
741
- align: 'top',
742
- alignX: 'left',
743
- isOpen: true,
744
- })
745
- const triggerEl = mockElement({
746
- top: 400,
747
- bottom: 430,
748
- left: 50,
749
- right: 150,
750
- width: 100,
751
- height: 30,
752
- })
753
- const contentEl = mockElement({
754
- top: 0,
755
- bottom: 100,
756
- left: 0,
757
- right: 200,
758
- width: 200,
759
- height: 100,
760
- })
761
- o.triggerRef(triggerEl)
762
- o.contentRef(contentEl)
763
- const cleanup = o.setupListeners()
764
- window.dispatchEvent(new Event('resize'))
765
-
766
- // top = trigger.top - offsetY - content.height = 400 - 0 - 100 = 300
767
- expect(contentEl.style.top).toBe('300px')
768
- cleanup()
769
- })
770
-
771
- it('alignX=right: positions content aligned to right edge', () => {
772
- const { o, contentEl } = setupDropdown({ align: 'bottom', alignX: 'right' })
773
- const cleanup = o.setupListeners()
774
- window.dispatchEvent(new Event('resize'))
775
-
776
- // right pos = trigger.right - offsetX - content.width = 150 - 0 - 200 = -50
777
- // fitsRight = -50 >= 0 → false, falls back to leftPos = trigger.left + offsetX = 50
778
- expect(contentEl.style.left).toBe('50px')
779
- cleanup()
780
- })
781
-
782
- it('alignX=center: centers content horizontally under trigger', () => {
783
- const { o, contentEl } = setupDropdown({ align: 'bottom', alignX: 'center' })
784
- const cleanup = o.setupListeners()
785
- window.dispatchEvent(new Event('resize'))
786
-
787
- // center = trigger.left + (trigger.right - trigger.left) / 2 - content.width / 2
788
- // = 50 + (150 - 50) / 2 - 200 / 2 = 50 + 50 - 100 = 0
789
- // fitsCL = 0 >= 0 → true, fitsCR = 0 + 200 <= 1024 → true
790
- expect(contentEl.style.left).toBe('0px')
791
- cleanup()
792
- })
793
-
794
- it('with offsets: applies offsetX and offsetY', () => {
795
- const o = useOverlay({
796
- type: 'dropdown',
797
- align: 'bottom',
798
- alignX: 'left',
799
- offsetX: 10,
800
- offsetY: 5,
801
- isOpen: true,
802
- })
803
- const triggerEl = mockElement({
804
- top: 100,
805
- bottom: 130,
806
- left: 50,
807
- right: 150,
808
- width: 100,
809
- height: 30,
810
- })
811
- const contentEl = mockElement({
812
- top: 0,
813
- bottom: 100,
814
- left: 0,
815
- right: 100,
816
- width: 100,
817
- height: 100,
818
- })
819
- o.triggerRef(triggerEl)
820
- o.contentRef(contentEl)
821
- const cleanup = o.setupListeners()
822
- window.dispatchEvent(new Event('resize'))
823
-
824
- // top = trigger.bottom + offsetY = 130 + 5 = 135
825
- expect(contentEl.style.top).toBe('135px')
826
- // left = trigger.left + offsetX = 50 + 10 = 60
827
- expect(contentEl.style.left).toBe('60px')
828
- cleanup()
829
- })
830
- })
831
-
832
- // =========================================================================
833
- // 11b. Position calculation - horizontal dropdown
834
- // =========================================================================
835
- describe('position calculation - horizontal dropdown', () => {
836
- it('align=right: positions content to the right of trigger', () => {
837
- const o = useOverlay({
838
- type: 'dropdown',
839
- align: 'right',
840
- alignY: 'top',
841
- isOpen: true,
842
- })
843
- const triggerEl = mockElement({
844
- top: 100,
845
- bottom: 130,
846
- left: 50,
847
- right: 150,
848
- width: 100,
849
- height: 30,
850
- })
851
- const contentEl = mockElement({
852
- top: 0,
853
- bottom: 100,
854
- left: 0,
855
- right: 100,
856
- width: 100,
857
- height: 100,
858
- })
859
- o.triggerRef(triggerEl)
860
- o.contentRef(contentEl)
861
- const cleanup = o.setupListeners()
862
- window.dispatchEvent(new Event('resize'))
863
-
864
- // rightPos = trigger.right + offsetX = 150 + 0 = 150
865
- // fitsRight = 150 + 100 <= 1024 → true
866
- expect(contentEl.style.left).toBe('150px')
867
- // topPos = trigger.top + offsetY = 100
868
- // fitsTop = 100 + 100 <= 768 → true
869
- expect(contentEl.style.top).toBe('100px')
870
- cleanup()
871
- })
872
-
873
- it('align=left: positions content to the left of trigger', () => {
874
- const o = useOverlay({
875
- type: 'dropdown',
876
- align: 'left',
877
- alignY: 'top',
878
- isOpen: true,
879
- })
880
- const triggerEl = mockElement({
881
- top: 100,
882
- bottom: 130,
883
- left: 300,
884
- right: 400,
885
- width: 100,
886
- height: 30,
887
- })
888
- const contentEl = mockElement({
889
- top: 0,
890
- bottom: 100,
891
- left: 0,
892
- right: 100,
893
- width: 100,
894
- height: 100,
895
- })
896
- o.triggerRef(triggerEl)
897
- o.contentRef(contentEl)
898
- const cleanup = o.setupListeners()
899
- window.dispatchEvent(new Event('resize'))
900
-
901
- // leftPos = trigger.left - offsetX - content.width = 300 - 0 - 100 = 200
902
- // fitsLeft = 200 >= 0 → true
903
- expect(contentEl.style.left).toBe('200px')
904
- cleanup()
905
- })
906
-
907
- it('align=right, alignY=center: vertically centers content', () => {
908
- const o = useOverlay({
909
- type: 'dropdown',
910
- align: 'right',
911
- alignY: 'center',
912
- isOpen: true,
913
- })
914
- const triggerEl = mockElement({
915
- top: 300,
916
- bottom: 330,
917
- left: 50,
918
- right: 150,
919
- width: 100,
920
- height: 30,
921
- })
922
- const contentEl = mockElement({
923
- top: 0,
924
- bottom: 100,
925
- left: 0,
926
- right: 100,
927
- width: 100,
928
- height: 100,
929
- })
930
- o.triggerRef(triggerEl)
931
- o.contentRef(contentEl)
932
- const cleanup = o.setupListeners()
933
- window.dispatchEvent(new Event('resize'))
934
-
935
- // center = trigger.top + (trigger.bottom - trigger.top) / 2 - content.height / 2
936
- // = 300 + (330 - 300) / 2 - 100 / 2 = 300 + 15 - 50 = 265
937
- expect(contentEl.style.top).toBe('265px')
938
- cleanup()
939
- })
940
-
941
- it('align=right, alignY=bottom: positions from bottom', () => {
942
- const o = useOverlay({
943
- type: 'dropdown',
944
- align: 'right',
945
- alignY: 'bottom',
946
- isOpen: true,
947
- })
948
- const triggerEl = mockElement({
949
- top: 300,
950
- bottom: 330,
951
- left: 50,
952
- right: 150,
953
- width: 100,
954
- height: 30,
955
- })
956
- const contentEl = mockElement({
957
- top: 0,
958
- bottom: 100,
959
- left: 0,
960
- right: 100,
961
- width: 100,
962
- height: 100,
963
- })
964
- o.triggerRef(triggerEl)
965
- o.contentRef(contentEl)
966
- const cleanup = o.setupListeners()
967
- window.dispatchEvent(new Event('resize'))
968
-
969
- // bottomPos = trigger.bottom - offsetY - content.height = 330 - 0 - 100 = 230
970
- // fitsBottom = 230 >= 0 → true
971
- expect(contentEl.style.top).toBe('230px')
972
- cleanup()
973
- })
974
- })
975
-
976
- // =========================================================================
977
- // 12. Modal positioning
978
- // =========================================================================
979
- describe('position calculation - modal', () => {
980
- it('modal type: centers content by default', () => {
981
- const o = useOverlay({
982
- type: 'modal',
983
- alignX: 'center',
984
- alignY: 'center',
985
- isOpen: true,
986
- })
987
- const contentEl = mockElement({
988
- top: 0,
989
- bottom: 200,
990
- left: 0,
991
- right: 300,
992
- width: 300,
993
- height: 200,
994
- })
995
- o.contentRef(contentEl)
996
- const cleanup = o.setupListeners()
997
- window.dispatchEvent(new Event('resize'))
998
-
999
- // left = innerWidth / 2 - width / 2 = 1024 / 2 - 300 / 2 = 362
1000
- expect(contentEl.style.left).toBe('362px')
1001
- // top = innerHeight / 2 - height / 2 = 768 / 2 - 200 / 2 = 284
1002
- expect(contentEl.style.top).toBe('284px')
1003
- cleanup()
1004
- })
1005
-
1006
- it('modal type: alignX=left positions left edge', () => {
1007
- const o = useOverlay({
1008
- type: 'modal',
1009
- alignX: 'left',
1010
- alignY: 'top',
1011
- offsetX: 20,
1012
- offsetY: 10,
1013
- isOpen: true,
1014
- })
1015
- const contentEl = mockElement({ width: 300, height: 200 })
1016
- o.contentRef(contentEl)
1017
- const cleanup = o.setupListeners()
1018
- window.dispatchEvent(new Event('resize'))
1019
-
1020
- expect(contentEl.style.left).toBe('20px')
1021
- expect(contentEl.style.top).toBe('10px')
1022
- cleanup()
1023
- })
1024
-
1025
- it('modal type: alignX=right positions right edge', () => {
1026
- const o = useOverlay({
1027
- type: 'modal',
1028
- alignX: 'right',
1029
- alignY: 'bottom',
1030
- offsetX: 15,
1031
- offsetY: 25,
1032
- isOpen: true,
1033
- })
1034
- const contentEl = mockElement({ width: 300, height: 200 })
1035
- o.contentRef(contentEl)
1036
- const cleanup = o.setupListeners()
1037
- window.dispatchEvent(new Event('resize'))
1038
-
1039
- expect(contentEl.style.right).toBe('15px')
1040
- expect(contentEl.style.bottom).toBe('25px')
1041
- cleanup()
1042
- })
1043
-
1044
- it('modal type: sets document.body overflow to hidden', () => {
1045
- const o = useOverlay({ type: 'modal', isOpen: true })
1046
- const contentEl = mockElement({ width: 300, height: 200 })
1047
- o.contentRef(contentEl)
1048
- const cleanup = o.setupListeners()
1049
-
1050
- expect(document.body.style.overflow).toBe('hidden')
1051
- cleanup()
1052
- expect(document.body.style.overflow).toBe('')
1053
- })
1054
- })
1055
-
1056
- // =========================================================================
1057
- // 13. Position - custom type
1058
- // =========================================================================
1059
- describe('position calculation - custom type', () => {
1060
- it('custom type: does not set position styles', () => {
1061
- const o = useOverlay({
1062
- type: 'custom',
1063
- isOpen: true,
1064
- })
1065
- const contentEl = mockElement({ width: 100, height: 100 })
1066
- o.contentRef(contentEl)
1067
- const cleanup = o.setupListeners()
1068
- window.dispatchEvent(new Event('resize'))
1069
-
1070
- // computePosition returns {} for custom type
1071
- expect(contentEl.style.top).toBe('')
1072
- expect(contentEl.style.left).toBe('')
1073
- cleanup()
1074
- })
1075
- })
1076
-
1077
- // =========================================================================
1078
- // 14. Alignment signal updates after positioning
1079
- // =========================================================================
1080
- describe('alignment signal updates', () => {
1081
- it('updates alignX signal when position flips horizontally', () => {
1082
- setViewport(200, 768)
1083
- const o = useOverlay({
1084
- type: 'dropdown',
1085
- align: 'bottom',
1086
- alignX: 'left',
1087
- isOpen: true,
1088
- })
1089
- const triggerEl = mockElement({
1090
- top: 100,
1091
- bottom: 130,
1092
- left: 50,
1093
- right: 150,
1094
- width: 100,
1095
- height: 30,
1096
- })
1097
- // Content wider than remaining space on the left side
1098
- const contentEl = mockElement({
1099
- width: 200,
1100
- height: 50,
1101
- })
1102
- o.triggerRef(triggerEl)
1103
- o.contentRef(contentEl)
1104
- const cleanup = o.setupListeners()
1105
- window.dispatchEvent(new Event('resize'))
1106
-
1107
- // leftPos = 50 + 0 = 50, fitsLeft = 50 + 200 <= 200 → false
1108
- // rightPos = 150 - 0 - 200 = -50, fitsRight = -50 >= 0 → false
1109
- // Falls back: sel(fitsLeft, leftPos, rightPos) → rightPos = -50
1110
- // resolvedAlignX = sel(fitsLeft, "left", "right") → "right"
1111
- expect(o.alignX()).toBe('right')
1112
- cleanup()
1113
- })
1114
-
1115
- it('updates alignY signal when vertical position flips to top', () => {
1116
- setViewport(1024, 200)
1117
- const o = useOverlay({
1118
- type: 'dropdown',
1119
- align: 'bottom',
1120
- alignX: 'left',
1121
- isOpen: true,
1122
- })
1123
- const triggerEl = mockElement({
1124
- top: 100,
1125
- bottom: 130,
1126
- left: 50,
1127
- right: 150,
1128
- width: 100,
1129
- height: 30,
1130
- })
1131
- const contentEl = mockElement({
1132
- width: 100,
1133
- height: 100,
1134
- })
1135
- o.triggerRef(triggerEl)
1136
- o.contentRef(contentEl)
1137
- const cleanup = o.setupListeners()
1138
- window.dispatchEvent(new Event('resize'))
1139
-
1140
- // bottomPos = 130, fitsBottom = 130 + 100 <= 200 → false
1141
- // useTop = sel(align === "top", fitsTop, !fitsBottom) = sel(false, _, !false) = true
1142
- // resolvedAlignY = "top"
1143
- expect(o.alignY()).toBe('top')
1144
- cleanup()
1145
- })
1146
- })
1147
-
1148
- // =========================================================================
1149
- // 15. Resize reposition
1150
- // =========================================================================
1151
- describe('resize handling', () => {
1152
- it('recalculates position on window resize', () => {
1153
- const o = useOverlay({
1154
- type: 'dropdown',
1155
- align: 'bottom',
1156
- alignX: 'left',
1157
- isOpen: true,
1158
- })
1159
- const triggerEl = mockElement({
1160
- top: 100,
1161
- bottom: 130,
1162
- left: 50,
1163
- right: 150,
1164
- width: 100,
1165
- height: 30,
1166
- })
1167
- const contentEl = mockElement({
1168
- width: 100,
1169
- height: 100,
1170
- })
1171
- o.triggerRef(triggerEl)
1172
- o.contentRef(contentEl)
1173
- const cleanup = o.setupListeners()
1174
-
1175
- // First resize
1176
- window.dispatchEvent(new Event('resize'))
1177
- expect(contentEl.style.top).toBe('130px')
1178
-
1179
- // Change trigger position and resize again
1180
- triggerEl.getBoundingClientRect = () => ({
1181
- top: 200,
1182
- bottom: 230,
1183
- left: 50,
1184
- right: 150,
1185
- width: 100,
1186
- height: 30,
1187
- x: 50,
1188
- y: 200,
1189
- toJSON: () => {},
1190
- })
1191
- window.dispatchEvent(new Event('resize'))
1192
- expect(contentEl.style.top).toBe('230px')
1193
- cleanup()
1194
- })
1195
- })
1196
-
1197
- // =========================================================================
1198
- // 16. Parent container
1199
- // =========================================================================
1200
- describe('parentContainer', () => {
1201
- it('sets overflow hidden on parent when closeOn is not hover', () => {
1202
- const parent = document.createElement('div')
1203
- const o = useOverlay({ parentContainer: parent, closeOn: 'click' })
1204
- const cleanup = o.setupListeners()
1205
-
1206
- expect(parent.style.overflow).toBe('hidden')
1207
- cleanup()
1208
- expect(parent.style.overflow).toBe('')
1209
- })
1210
-
1211
- it('does not set overflow hidden on parent when closeOn is hover', () => {
1212
- const parent = document.createElement('div')
1213
- const o = useOverlay({ parentContainer: parent, closeOn: 'hover', openOn: 'hover' })
1214
- const cleanup = o.setupListeners()
1215
-
1216
- expect(parent.style.overflow).not.toBe('hidden')
1217
- cleanup()
1218
- })
1219
- })
1220
-
1221
- // =========================================================================
1222
- // 17. Provider
1223
- // =========================================================================
1224
- describe('Provider', () => {
1225
- it('exposes Provider component', () => {
1226
- const o = useOverlay()
1227
- expect(typeof o.Provider).toBe('function')
1228
- })
1229
- })
1230
-
1231
- // =========================================================================
1232
- // 18. Independent instances
1233
- // =========================================================================
1234
- describe('independent instances', () => {
1235
- it('two useOverlay instances do not share state', () => {
1236
- const o1 = useOverlay()
1237
- const o2 = useOverlay()
1238
-
1239
- o1.showContent()
1240
- expect(o1.active()).toBe(true)
1241
- expect(o2.active()).toBe(false)
1242
-
1243
- o2.showContent()
1244
- o1.hideContent()
1245
- expect(o1.active()).toBe(false)
1246
- expect(o2.active()).toBe(true)
1247
- })
1248
- })
1249
-
1250
- // =========================================================================
1251
- // 19. Manual open/close mode
1252
- // =========================================================================
1253
- describe('manual mode', () => {
1254
- it('openOn=manual: click on trigger does not open', () => {
1255
- const o = useOverlay({ openOn: 'manual', closeOn: 'manual' })
1256
- const triggerEl = mockElement()
1257
- o.triggerRef(triggerEl)
1258
- const cleanup = o.setupListeners()
1259
-
1260
- const click = new MouseEvent('click', { bubbles: true })
1261
- Object.defineProperty(click, 'target', { value: triggerEl })
1262
- window.dispatchEvent(click)
1263
-
1264
- expect(o.active()).toBe(false)
1265
- cleanup()
1266
- })
1267
-
1268
- it('manual mode: only showContent/hideContent toggle state', () => {
1269
- const o = useOverlay({ openOn: 'manual', closeOn: 'manual' })
1270
- const cleanup = o.setupListeners()
1271
-
1272
- o.showContent()
1273
- expect(o.active()).toBe(true)
1274
-
1275
- o.hideContent()
1276
- expect(o.active()).toBe(false)
1277
- cleanup()
1278
- })
1279
- })
1280
-
1281
- // =========================================================================
1282
- // 20. Position with absolute + ancestor offset
1283
- // =========================================================================
1284
- describe('position absolute with ancestor offset', () => {
1285
- it('adjusts position for offset parent', () => {
1286
- const o = useOverlay({
1287
- type: 'dropdown',
1288
- align: 'bottom',
1289
- alignX: 'left',
1290
- position: 'absolute',
1291
- isOpen: true,
1292
- })
1293
- const triggerEl = mockElement({
1294
- top: 200,
1295
- bottom: 230,
1296
- left: 100,
1297
- right: 200,
1298
- width: 100,
1299
- height: 30,
1300
- })
1301
- const contentEl = mockElement({
1302
- width: 100,
1303
- height: 50,
1304
- })
1305
-
1306
- // Mock offsetParent
1307
- const offsetParent = document.createElement('div')
1308
- offsetParent.getBoundingClientRect = () => ({
1309
- top: 50,
1310
- bottom: 400,
1311
- left: 30,
1312
- right: 500,
1313
- width: 470,
1314
- height: 350,
1315
- x: 30,
1316
- y: 50,
1317
- toJSON: () => {},
1318
- })
1319
- Object.defineProperty(contentEl, 'offsetParent', {
1320
- value: offsetParent,
1321
- configurable: true,
1322
- })
1323
-
1324
- o.triggerRef(triggerEl)
1325
- o.contentRef(contentEl)
1326
- const cleanup = o.setupListeners()
1327
- window.dispatchEvent(new Event('resize'))
1328
-
1329
- // Without ancestor: top = 230, left = 100
1330
- // Adjusted: top = 230 - 50 = 180, left = 100 - 30 = 70
1331
- expect(contentEl.style.top).toBe('180px')
1332
- expect(contentEl.style.left).toBe('70px')
1333
- cleanup()
1334
- })
1335
- })
1336
- })