@pyreon/elements 0.24.4 → 0.24.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -12
- package/src/Element/component.tsx +0 -315
- package/src/Element/constants.ts +0 -96
- package/src/Element/index.ts +0 -6
- package/src/Element/types.ts +0 -168
- package/src/Element/utils.ts +0 -15
- package/src/List/component.tsx +0 -105
- package/src/List/index.ts +0 -5
- package/src/Overlay/component.tsx +0 -140
- package/src/Overlay/context.tsx +0 -36
- package/src/Overlay/index.ts +0 -7
- package/src/Overlay/positioning.ts +0 -191
- package/src/Overlay/useOverlay.tsx +0 -461
- package/src/Portal/component.tsx +0 -54
- package/src/Portal/index.ts +0 -5
- package/src/Text/component.tsx +0 -67
- package/src/Text/index.ts +0 -5
- package/src/Text/styled.ts +0 -30
- package/src/Util/component.tsx +0 -43
- package/src/Util/index.ts +0 -5
- package/src/__tests__/Content.test.tsx +0 -123
- package/src/__tests__/Element-slot-reactivity.browser.test.tsx +0 -152
- package/src/__tests__/Element.test.ts +0 -819
- package/src/__tests__/Iterator.test.ts +0 -492
- package/src/__tests__/Iterator.types.test.ts +0 -237
- package/src/__tests__/List.test.ts +0 -199
- package/src/__tests__/Overlay.test.ts +0 -492
- package/src/__tests__/Portal.test.ts +0 -156
- package/src/__tests__/Text.test.ts +0 -274
- package/src/__tests__/Util.test.ts +0 -63
- package/src/__tests__/Wrapper-innerhtml.test.tsx +0 -178
- package/src/__tests__/Wrapper.test.tsx +0 -196
- package/src/__tests__/elements.browser.test.tsx +0 -132
- package/src/__tests__/equalBeforeAfter.test.ts +0 -122
- package/src/__tests__/helpers.test.ts +0 -65
- package/src/__tests__/integration.test.tsx +0 -118
- package/src/__tests__/internElementBundle.test.ts +0 -102
- package/src/__tests__/iterator-function-children.test.tsx +0 -120
- package/src/__tests__/native-markers.test.ts +0 -13
- package/src/__tests__/overlayContext.test.tsx +0 -78
- package/src/__tests__/perf-stress.browser.test.tsx +0 -119
- package/src/__tests__/positioning.test.ts +0 -90
- package/src/__tests__/responsiveProps.test.ts +0 -328
- package/src/__tests__/slot-component-reference.test.tsx +0 -157
- package/src/__tests__/useOverlay.test.ts +0 -1336
- package/src/__tests__/utils.test.ts +0 -69
- package/src/__tests__/wrapper-block-cascade.test.ts +0 -121
- package/src/constants.ts +0 -1
- package/src/env.d.ts +0 -6
- package/src/helpers/Content/component.tsx +0 -75
- package/src/helpers/Content/index.ts +0 -3
- package/src/helpers/Content/styled.ts +0 -105
- package/src/helpers/Content/types.ts +0 -49
- package/src/helpers/Iterator/component.tsx +0 -316
- package/src/helpers/Iterator/index.ts +0 -30
- package/src/helpers/Iterator/types.ts +0 -138
- package/src/helpers/Wrapper/component.tsx +0 -180
- package/src/helpers/Wrapper/constants.ts +0 -10
- package/src/helpers/Wrapper/index.ts +0 -3
- package/src/helpers/Wrapper/styled.ts +0 -64
- package/src/helpers/Wrapper/types.ts +0 -56
- package/src/helpers/Wrapper/utils.ts +0 -7
- package/src/helpers/index.ts +0 -4
- package/src/helpers/internElementBundle.ts +0 -37
- package/src/helpers/isPyreonComponent.ts +0 -46
- package/src/index.ts +0 -42
- package/src/manifest.ts +0 -190
- package/src/tests/manifest-snapshot.test.ts +0 -45
- package/src/types.ts +0 -112
- package/src/utils.ts +0 -5
|
@@ -1,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
|
-
})
|