@pyreon/kinetic 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 (37) hide show
  1. package/package.json +10 -12
  2. package/src/Collapse.tsx +0 -166
  3. package/src/Stagger.tsx +0 -63
  4. package/src/Transition.tsx +0 -280
  5. package/src/TransitionGroup.tsx +0 -139
  6. package/src/__tests__/Collapse.test.tsx +0 -803
  7. package/src/__tests__/GroupRenderer.test.tsx +0 -434
  8. package/src/__tests__/StaggerRenderer.test.tsx +0 -523
  9. package/src/__tests__/Transition.ssr.test.tsx +0 -183
  10. package/src/__tests__/Transition.test.tsx +0 -403
  11. package/src/__tests__/TransitionItem.test.tsx +0 -514
  12. package/src/__tests__/kinetic-modes.ssr.test.tsx +0 -214
  13. package/src/__tests__/kinetic.browser.test.tsx +0 -327
  14. package/src/__tests__/kinetic.test.tsx +0 -565
  15. package/src/__tests__/presets.test.ts +0 -46
  16. package/src/__tests__/stagger-component-children-hydration.test.tsx +0 -191
  17. package/src/__tests__/top-level-transition-stagger-function-children.test.tsx +0 -141
  18. package/src/__tests__/useAnimationEnd.test.ts +0 -194
  19. package/src/__tests__/useReducedMotion.test.ts +0 -160
  20. package/src/__tests__/useTransitionState.test.ts +0 -132
  21. package/src/__tests__/utils.test.ts +0 -139
  22. package/src/index.ts +0 -15
  23. package/src/jsx-augment.d.ts +0 -12
  24. package/src/kinetic/CollapseRenderer.tsx +0 -216
  25. package/src/kinetic/GroupRenderer.tsx +0 -149
  26. package/src/kinetic/StaggerRenderer.tsx +0 -94
  27. package/src/kinetic/TransitionItem.tsx +0 -250
  28. package/src/kinetic/TransitionRenderer.tsx +0 -230
  29. package/src/kinetic/createKineticComponent.tsx +0 -224
  30. package/src/kinetic/types.ts +0 -149
  31. package/src/kinetic.ts +0 -25
  32. package/src/presets.ts +0 -66
  33. package/src/types.ts +0 -118
  34. package/src/useAnimationEnd.ts +0 -59
  35. package/src/useReducedMotion.ts +0 -28
  36. package/src/useTransitionState.ts +0 -62
  37. package/src/utils.ts +0 -113
@@ -1,803 +0,0 @@
1
- import type { VNode } from '@pyreon/core'
2
- import { h } from '@pyreon/core'
3
- import { signal } from '@pyreon/reactivity'
4
- import Collapse from '../Collapse'
5
- import CollapseRenderer from '../kinetic/CollapseRenderer'
6
- import type { KineticConfig } from '../kinetic/types'
7
-
8
- let _reducedMotion = false
9
-
10
- vi.mock('../useReducedMotion', () => ({
11
- useReducedMotion: () => () => _reducedMotion,
12
- }))
13
-
14
- // Mock scrollHeight
15
- const mockScrollHeight = (value: number) => {
16
- Object.defineProperty(HTMLElement.prototype, 'scrollHeight', {
17
- configurable: true,
18
- get() {
19
- return value
20
- },
21
- })
22
- }
23
-
24
- const fireTransitionEnd = (el: HTMLElement) => {
25
- const event = new Event('transitionend', { bubbles: true })
26
- Object.defineProperty(event, 'target', { value: el })
27
- el.dispatchEvent(event)
28
- }
29
-
30
- /**
31
- * Helper: call Collapse and wire up mock elements to the refs.
32
- * Collapse creates a wrapper div (wrapperRef) and inner content div (contentRef).
33
- * We manually assign mock elements to the refs so the animation logic runs.
34
- */
35
- const setupCollapse = (props: Record<string, unknown>) => {
36
- const wrapperEl = document.createElement('div')
37
- const contentEl = document.createElement('div')
38
-
39
- // Mock offsetHeight for reflow forcing
40
- Object.defineProperty(wrapperEl, 'offsetHeight', {
41
- configurable: true,
42
- get() {
43
- return 0
44
- },
45
- })
46
-
47
- const vnode = Collapse(props as any)
48
-
49
- // Wire up refs: the wrapper div has ref={wrapperRef}, inner div has ref={contentRef}
50
- // In the VNode tree: <div ref={wrapperRef}><Show><div ref={contentRef}>...</div></Show></div>
51
- if (vnode?.props) {
52
- const vnodeProps = vnode.props as Record<string, unknown>
53
- // wrapperRef is on the outer div
54
- if (typeof vnodeProps.ref === 'function') {
55
- ;(vnodeProps.ref as (el: HTMLElement | null) => void)(wrapperEl)
56
- } else if (vnodeProps.ref && typeof vnodeProps.ref === 'object') {
57
- ;(vnodeProps.ref as { current: HTMLElement | null }).current = wrapperEl
58
- }
59
- }
60
-
61
- // Find contentRef in children (Show > div)
62
- if (vnode?.children) {
63
- const children = Array.isArray(vnode.children) ? vnode.children : [vnode.children]
64
- for (const child of children) {
65
- if (child && typeof child === 'object' && 'type' in (child as object)) {
66
- const showNode = child as any
67
- // Show's children contain <div ref={contentRef}>
68
- const showChildren = showNode.props?.children ?? showNode.children
69
- if (showChildren) {
70
- const sc = Array.isArray(showChildren) ? showChildren : [showChildren]
71
- for (const s of sc) {
72
- if (s && typeof s === 'object' && 'props' in s) {
73
- const ref = s.props?.ref
74
- if (ref && typeof ref === 'object') {
75
- ref.current = contentEl
76
- } else if (typeof ref === 'function') {
77
- ref(contentEl)
78
- }
79
- }
80
- }
81
- }
82
- }
83
- }
84
- }
85
-
86
- return { vnode, wrapperEl, contentEl }
87
- }
88
-
89
- describe('Collapse', () => {
90
- beforeEach(() => {
91
- vi.useFakeTimers()
92
- mockScrollHeight(200)
93
- })
94
-
95
- afterEach(() => vi.useRealTimers())
96
-
97
- it('returns a VNode', () => {
98
- const show = signal(true)
99
- const child = h('div', null, 'Hello') as VNode
100
- const vnode = Collapse({ show, children: child } as any)
101
- expect(vnode).not.toBeNull()
102
- })
103
-
104
- it('fires onEnter callback when entering', () => {
105
- const show = signal(false)
106
- const onEnter = vi.fn()
107
-
108
- setupCollapse({
109
- show,
110
- onEnter,
111
- children: h('div', null, 'Hello') as VNode,
112
- })
113
-
114
- show.set(true)
115
- expect(onEnter).toHaveBeenCalledTimes(1)
116
- })
117
-
118
- it('fires onAfterEnter after transitionend', () => {
119
- const show = signal(false)
120
- const onAfterEnter = vi.fn()
121
-
122
- const { wrapperEl } = setupCollapse({
123
- show,
124
- onAfterEnter,
125
- children: h('div', null, 'Hello') as VNode,
126
- })
127
-
128
- show.set(true)
129
- expect(onAfterEnter).not.toHaveBeenCalled()
130
-
131
- fireTransitionEnd(wrapperEl)
132
- expect(onAfterEnter).toHaveBeenCalledTimes(1)
133
- })
134
-
135
- it('fires onLeave callback when leaving', () => {
136
- const show = signal(true)
137
- const onLeave = vi.fn()
138
-
139
- setupCollapse({
140
- show,
141
- onLeave,
142
- children: h('div', null, 'Hello') as VNode,
143
- })
144
-
145
- show.set(false)
146
- expect(onLeave).toHaveBeenCalledTimes(1)
147
- })
148
-
149
- it('fires onAfterLeave after transitionend', () => {
150
- const show = signal(true)
151
- const onAfterLeave = vi.fn()
152
-
153
- const { wrapperEl } = setupCollapse({
154
- show,
155
- onAfterLeave,
156
- children: h('div', null, 'Hello') as VNode,
157
- })
158
-
159
- show.set(false)
160
- expect(onAfterLeave).not.toHaveBeenCalled()
161
-
162
- fireTransitionEnd(wrapperEl)
163
- expect(onAfterLeave).toHaveBeenCalledTimes(1)
164
- })
165
-
166
- it('animates height from 0 to scrollHeight on enter', () => {
167
- const show = signal(false)
168
-
169
- const { wrapperEl } = setupCollapse({
170
- show,
171
- children: h('div', null, 'Hello') as VNode,
172
- })
173
-
174
- show.set(true)
175
-
176
- expect(wrapperEl.style.height).toBe('200px')
177
- expect(wrapperEl.style.transition).toBe('height 300ms ease')
178
- })
179
-
180
- it('switches to height:auto after enter animation completes', () => {
181
- const show = signal(false)
182
-
183
- const { wrapperEl } = setupCollapse({
184
- show,
185
- children: h('div', null, 'Hello') as VNode,
186
- })
187
-
188
- show.set(true)
189
- fireTransitionEnd(wrapperEl)
190
-
191
- expect(wrapperEl.style.height).toBe('auto')
192
- expect(wrapperEl.style.overflow).toBe('')
193
- expect(wrapperEl.style.transition).toBe('')
194
- })
195
-
196
- it('animates height to 0 on leave', () => {
197
- const show = signal(true)
198
-
199
- const { wrapperEl } = setupCollapse({
200
- show,
201
- children: h('div', null, 'Hello') as VNode,
202
- })
203
-
204
- show.set(false)
205
-
206
- expect(wrapperEl.style.height).toBe('0px')
207
- expect(wrapperEl.style.overflow).toBe('hidden')
208
- })
209
-
210
- it('uses custom transition property', () => {
211
- const show = signal(false)
212
-
213
- const { wrapperEl } = setupCollapse({
214
- show,
215
- transition: 'height 500ms ease-in-out',
216
- children: h('div', null, 'Hello') as VNode,
217
- })
218
-
219
- show.set(true)
220
-
221
- expect(wrapperEl.style.transition).toBe('height 500ms ease-in-out')
222
- })
223
-
224
- it('appear=true animates on initial mount', async () => {
225
- const show = signal(true)
226
- const onEnter = vi.fn()
227
-
228
- const { wrapperEl } = setupCollapse({
229
- show,
230
- appear: true,
231
- onEnter,
232
- children: h('div', null, 'Hello') as VNode,
233
- })
234
-
235
- // appear defers via queueMicrotask so all refs are wired first
236
- await Promise.resolve()
237
-
238
- expect(onEnter).toHaveBeenCalledTimes(1)
239
- expect(wrapperEl.style.height).toBe('200px')
240
- })
241
-
242
- it('custom timeout completes leave when transitionend does not fire', () => {
243
- const show = signal(true)
244
- const onAfterLeave = vi.fn()
245
-
246
- setupCollapse({
247
- show,
248
- timeout: 800,
249
- onAfterLeave,
250
- children: h('div', null, 'Hello') as VNode,
251
- })
252
-
253
- show.set(false)
254
- expect(onAfterLeave).not.toHaveBeenCalled()
255
-
256
- vi.advanceTimersByTime(800)
257
-
258
- expect(onAfterLeave).toHaveBeenCalledTimes(1)
259
- })
260
-
261
- it('interrupts leave and starts entering when toggled back to show', () => {
262
- const show = signal(true)
263
- const onEnter = vi.fn()
264
- const onLeave = vi.fn()
265
-
266
- const { wrapperEl } = setupCollapse({
267
- show,
268
- onEnter,
269
- onLeave,
270
- children: h('div', null, 'Hello') as VNode,
271
- })
272
-
273
- // Start leaving
274
- show.set(false)
275
- expect(onLeave).toHaveBeenCalledTimes(1)
276
-
277
- // Toggle back
278
- show.set(true)
279
- expect(onEnter).toHaveBeenCalledTimes(1)
280
- expect(wrapperEl.style.height).toBe('200px')
281
- })
282
-
283
- it('interrupts entering and starts leaving when toggled back to hide', () => {
284
- const show = signal(false)
285
- const onEnter = vi.fn()
286
- const onLeave = vi.fn()
287
-
288
- setupCollapse({
289
- show,
290
- onEnter,
291
- onLeave,
292
- children: h('div', null, 'Hello') as VNode,
293
- })
294
-
295
- // Start entering
296
- show.set(true)
297
- expect(onEnter).toHaveBeenCalledTimes(1)
298
-
299
- // Toggle back before transitionend
300
- show.set(false)
301
- expect(onLeave).toHaveBeenCalledTimes(1)
302
- })
303
-
304
- it('does not re-trigger entering if already entered', () => {
305
- const show = signal(false)
306
- const onEnter = vi.fn()
307
-
308
- const { wrapperEl } = setupCollapse({
309
- show,
310
- onEnter,
311
- children: h('div', null, 'Hello') as VNode,
312
- })
313
-
314
- show.set(true)
315
- expect(onEnter).toHaveBeenCalledTimes(1)
316
-
317
- // Complete the transition
318
- fireTransitionEnd(wrapperEl)
319
-
320
- // Toggle show off and on again
321
- show.set(false)
322
- show.set(true)
323
- expect(onEnter).toHaveBeenCalledTimes(2)
324
- })
325
-
326
- it('does not re-trigger leaving if already hidden', () => {
327
- const show = signal(true)
328
- const onLeave = vi.fn()
329
-
330
- const { wrapperEl } = setupCollapse({
331
- show,
332
- onLeave,
333
- children: h('div', null, 'Hello') as VNode,
334
- })
335
-
336
- show.set(false)
337
- expect(onLeave).toHaveBeenCalledTimes(1)
338
-
339
- fireTransitionEnd(wrapperEl)
340
-
341
- // Already hidden, setting false again should not trigger leave
342
- show.set(false)
343
- expect(onLeave).toHaveBeenCalledTimes(1)
344
- })
345
-
346
- it('appear=true fires onAfterEnter after transitionend', async () => {
347
- const show = signal(true)
348
- const onAfterEnter = vi.fn()
349
-
350
- const { wrapperEl } = setupCollapse({
351
- show,
352
- appear: true,
353
- onAfterEnter,
354
- children: h('div', null, 'Hello') as VNode,
355
- })
356
-
357
- await Promise.resolve()
358
-
359
- expect(onAfterEnter).not.toHaveBeenCalled()
360
- fireTransitionEnd(wrapperEl)
361
- expect(onAfterEnter).toHaveBeenCalledTimes(1)
362
- })
363
-
364
- it('leave transition sets height to scrollHeight first then to 0', () => {
365
- const show = signal(true)
366
-
367
- const { wrapperEl } = setupCollapse({
368
- show,
369
- children: h('div', null, 'Hello') as VNode,
370
- })
371
-
372
- show.set(false)
373
-
374
- // After leaving, height should be 0
375
- expect(wrapperEl.style.height).toBe('0px')
376
- expect(wrapperEl.style.overflow).toBe('hidden')
377
- expect(wrapperEl.style.transition).toBe('height 300ms ease')
378
- })
379
- })
380
-
381
- // ─── CollapseRenderer (kinetic mode) ──────────────────────
382
-
383
- const makeCollapseConfig = (overrides: Partial<KineticConfig> = {}): KineticConfig => ({
384
- tag: 'div',
385
- mode: 'collapse',
386
- ...overrides,
387
- })
388
-
389
- /** Wire a ref (function or object) on a VNode's props to a given element. */
390
- const wireWrapperRef = (vnode: VNode | null, el: HTMLElement) => {
391
- if (!vnode?.props) return
392
- const vnodeProps = vnode.props as Record<string, unknown>
393
- if (typeof vnodeProps.ref === 'function') {
394
- ;(vnodeProps.ref as (el: HTMLElement | null) => void)(el)
395
- } else if (vnodeProps.ref && typeof vnodeProps.ref === 'object') {
396
- ;(vnodeProps.ref as { current: HTMLElement | null }).current = el
397
- }
398
- }
399
-
400
- /**
401
- * Find and wire contentRef on the inner div. Walks two shapes:
402
- *
403
- * 1. Initially-visible Collapse: outer → <Show>{<div ref=contentRef/>}</Show>
404
- * (the pre-SSR-fix shape, kept for the `wasInitiallyShown=true` branch
405
- * in CollapseRenderer)
406
- *
407
- * 2. Initially-hidden Collapse: outer → <div ref=contentRef/>
408
- * (the SSR-correct shape — children always rendered structurally so
409
- * the prerendered HTML carries content for SEO / social scrapers /
410
- * no-JS users; visual hiding via the outer wrapper's `height: 0;
411
- * overflow: hidden`. See CollapseRenderer's `wasInitiallyShown`
412
- * branch.)
413
- *
414
- * Tries the direct-div shape first; falls back to the Show-wrapped walk.
415
- */
416
- const wireContentRef = (vnode: VNode | null, contentEl: HTMLElement) => {
417
- if (!vnode?.children) return
418
- const vnodeChildren = Array.isArray(vnode.children) ? vnode.children : [vnode.children]
419
- for (const c of vnodeChildren) {
420
- if (!c || typeof c !== 'object' || !('props' in (c as object))) continue
421
- const directRef = (c as any).props?.ref
422
- if (directRef) {
423
- if (typeof directRef === 'function') directRef(contentEl)
424
- else if (typeof directRef === 'object') directRef.current = contentEl
425
- return
426
- }
427
- // Fall through to Show-wrapped walk.
428
- const showChildren = (c as any).props?.children ?? (c as any).children
429
- if (!showChildren) continue
430
- const sc = Array.isArray(showChildren) ? showChildren : [showChildren]
431
- for (const s of sc) {
432
- if (!s || typeof s !== 'object' || !('props' in s)) continue
433
- const ref = s.props?.ref
434
- if (ref && typeof ref === 'object') {
435
- ref.current = contentEl
436
- } else if (typeof ref === 'function') {
437
- ref(contentEl)
438
- }
439
- }
440
- }
441
- }
442
-
443
- /**
444
- * Helper: call CollapseRenderer and wire up mock elements to the refs.
445
- * CollapseRenderer uses h(config.tag, { ref: wrapperRef }) and an inner
446
- * <div ref={contentRef}> — same structure as Collapse but via config.tag.
447
- */
448
- const setupCollapseRenderer = (props: {
449
- config?: KineticConfig
450
- show: () => boolean
451
- appear?: boolean | undefined
452
- timeout?: number | undefined
453
- transition?: string | undefined
454
- callbacks?: Record<string, unknown>
455
- children?: VNode | VNode[]
456
- }) => {
457
- const wrapperEl = document.createElement('div')
458
- const contentEl = document.createElement('div')
459
-
460
- Object.defineProperty(wrapperEl, 'offsetHeight', {
461
- configurable: true,
462
- get() {
463
- return 0
464
- },
465
- })
466
-
467
- const config = props.config ?? makeCollapseConfig()
468
- const child = h('p', null, 'Content') as VNode
469
-
470
- const vnode = CollapseRenderer({
471
- config,
472
- htmlProps: {},
473
- show: props.show,
474
- appear: props.appear,
475
- timeout: props.timeout,
476
- transition: props.transition,
477
- callbacks: (props.callbacks ?? {}) as Record<string, () => void>,
478
- children: props.children ?? child,
479
- })
480
-
481
- wireWrapperRef(vnode, wrapperEl)
482
- wireContentRef(vnode, contentEl)
483
-
484
- return { vnode, wrapperEl, contentEl }
485
- }
486
-
487
- describe('CollapseRenderer', () => {
488
- beforeEach(() => {
489
- vi.useFakeTimers()
490
- mockScrollHeight(200)
491
- })
492
-
493
- afterEach(() => vi.useRealTimers())
494
-
495
- it('returns a VNode with the config.tag', () => {
496
- const show = signal(true)
497
- const config = makeCollapseConfig({ tag: 'section' })
498
- const child = h('p', null, 'Content') as VNode
499
-
500
- const vnode = CollapseRenderer({
501
- config,
502
- htmlProps: {},
503
- show: () => show(),
504
- callbacks: {},
505
- children: child,
506
- })
507
-
508
- expect(vnode).not.toBeNull()
509
- expect(vnode?.type).toBe('section')
510
- })
511
-
512
- it('fires onEnter and animates height on entering', () => {
513
- const show = signal(false)
514
- const onEnter = vi.fn()
515
-
516
- const { wrapperEl } = setupCollapseRenderer({
517
- show: () => show(),
518
- callbacks: { onEnter },
519
- })
520
-
521
- show.set(true)
522
- expect(onEnter).toHaveBeenCalledTimes(1)
523
- expect(wrapperEl.style.height).toBe('200px')
524
- expect(wrapperEl.style.transition).toBe('height 300ms ease')
525
- })
526
-
527
- it('fires onLeave and animates height to 0 on leaving', () => {
528
- const show = signal(true)
529
- const onLeave = vi.fn()
530
-
531
- const { wrapperEl } = setupCollapseRenderer({
532
- show: () => show(),
533
- callbacks: { onLeave },
534
- })
535
-
536
- show.set(false)
537
- expect(onLeave).toHaveBeenCalledTimes(1)
538
- expect(wrapperEl.style.height).toBe('0px')
539
- expect(wrapperEl.style.overflow).toBe('hidden')
540
- })
541
-
542
- it('fires onAfterEnter and sets height:auto after transitionend', () => {
543
- const show = signal(false)
544
- const onAfterEnter = vi.fn()
545
-
546
- const { wrapperEl } = setupCollapseRenderer({
547
- show: () => show(),
548
- callbacks: { onAfterEnter },
549
- })
550
-
551
- show.set(true)
552
- fireTransitionEnd(wrapperEl)
553
-
554
- expect(onAfterEnter).toHaveBeenCalledTimes(1)
555
- expect(wrapperEl.style.height).toBe('auto')
556
- expect(wrapperEl.style.overflow).toBe('')
557
- expect(wrapperEl.style.transition).toBe('')
558
- })
559
-
560
- it('fires onAfterLeave after leave transitionend', () => {
561
- const show = signal(true)
562
- const onAfterLeave = vi.fn()
563
-
564
- const { wrapperEl } = setupCollapseRenderer({
565
- show: () => show(),
566
- callbacks: { onAfterLeave },
567
- })
568
-
569
- show.set(false)
570
- fireTransitionEnd(wrapperEl)
571
-
572
- expect(onAfterLeave).toHaveBeenCalledTimes(1)
573
- })
574
-
575
- it('uses custom transition from prop', () => {
576
- const show = signal(false)
577
-
578
- const { wrapperEl } = setupCollapseRenderer({
579
- show: () => show(),
580
- transition: 'height 500ms ease-in-out',
581
- callbacks: {},
582
- })
583
-
584
- show.set(true)
585
- expect(wrapperEl.style.transition).toBe('height 500ms ease-in-out')
586
- })
587
-
588
- it('uses config.transition as fallback', () => {
589
- const show = signal(false)
590
- const config = makeCollapseConfig({ transition: 'height 700ms linear' })
591
-
592
- const { wrapperEl } = setupCollapseRenderer({
593
- config,
594
- show: () => show(),
595
- callbacks: {},
596
- })
597
-
598
- show.set(true)
599
- expect(wrapperEl.style.transition).toBe('height 700ms linear')
600
- })
601
-
602
- it('appear=true triggers entering via ref proxy on initial mount', async () => {
603
- const show = signal(true)
604
- const onEnter = vi.fn()
605
-
606
- const { wrapperEl } = setupCollapseRenderer({
607
- show: () => show(),
608
- appear: true,
609
- callbacks: { onEnter },
610
- })
611
-
612
- // appear defers via queueMicrotask
613
- await Promise.resolve()
614
-
615
- expect(onEnter).toHaveBeenCalledTimes(1)
616
- expect(wrapperEl.style.height).toBe('200px')
617
- })
618
-
619
- it('timeout fallback completes enter when transitionend never fires', () => {
620
- const show = signal(false)
621
- const onAfterEnter = vi.fn()
622
-
623
- setupCollapseRenderer({
624
- show: () => show(),
625
- timeout: 600,
626
- callbacks: { onAfterEnter },
627
- })
628
-
629
- show.set(true)
630
- expect(onAfterEnter).not.toHaveBeenCalled()
631
-
632
- vi.advanceTimersByTime(600)
633
- expect(onAfterEnter).toHaveBeenCalledTimes(1)
634
- })
635
-
636
- it('timeout fallback completes leave when transitionend never fires', () => {
637
- const show = signal(true)
638
- const onAfterLeave = vi.fn()
639
-
640
- setupCollapseRenderer({
641
- show: () => show(),
642
- timeout: 600,
643
- callbacks: { onAfterLeave },
644
- })
645
-
646
- show.set(false)
647
- expect(onAfterLeave).not.toHaveBeenCalled()
648
-
649
- vi.advanceTimersByTime(600)
650
- expect(onAfterLeave).toHaveBeenCalledTimes(1)
651
- })
652
-
653
- it('interrupts leave and re-enters when show toggles back', () => {
654
- const show = signal(true)
655
- const onEnter = vi.fn()
656
- const onLeave = vi.fn()
657
-
658
- const { wrapperEl } = setupCollapseRenderer({
659
- show: () => show(),
660
- callbacks: { onEnter, onLeave },
661
- })
662
-
663
- show.set(false)
664
- expect(onLeave).toHaveBeenCalledTimes(1)
665
-
666
- show.set(true)
667
- expect(onEnter).toHaveBeenCalledTimes(1)
668
- expect(wrapperEl.style.height).toBe('200px')
669
- })
670
-
671
- it('uses config.timeout as fallback', () => {
672
- const show = signal(false)
673
- const onAfterEnter = vi.fn()
674
- const config = makeCollapseConfig({ timeout: 400 })
675
-
676
- setupCollapseRenderer({
677
- config,
678
- show: () => show(),
679
- callbacks: { onAfterEnter },
680
- })
681
-
682
- show.set(true)
683
- vi.advanceTimersByTime(400)
684
- expect(onAfterEnter).toHaveBeenCalledTimes(1)
685
- })
686
-
687
- it('uses config.appear as fallback', async () => {
688
- const show = signal(true)
689
- const onEnter = vi.fn()
690
- const config = makeCollapseConfig({ appear: true })
691
-
692
- setupCollapseRenderer({
693
- config,
694
- show: () => show(),
695
- callbacks: { onEnter },
696
- })
697
-
698
- await Promise.resolve()
699
- expect(onEnter).toHaveBeenCalledTimes(1)
700
- })
701
- })
702
-
703
- describe('CollapseRenderer — reduced motion', () => {
704
- beforeEach(() => {
705
- vi.useFakeTimers()
706
- mockScrollHeight(200)
707
- _reducedMotion = true
708
- })
709
-
710
- afterEach(() => {
711
- vi.useRealTimers()
712
- _reducedMotion = false
713
- })
714
-
715
- it('reduced motion: entering skips animation and sets height:auto immediately', () => {
716
- const show = signal(false)
717
- const onEnter = vi.fn()
718
- const onAfterEnter = vi.fn()
719
-
720
- const { wrapperEl } = setupCollapseRenderer({
721
- show: () => show(),
722
- callbacks: { onEnter, onAfterEnter },
723
- })
724
-
725
- show.set(true)
726
-
727
- expect(onEnter).toHaveBeenCalledTimes(1)
728
- expect(onAfterEnter).toHaveBeenCalledTimes(1)
729
- expect(wrapperEl.style.height).toBe('auto')
730
- expect(wrapperEl.style.overflow).toBe('')
731
- })
732
-
733
- it('reduced motion: leaving skips animation and sets height:0 immediately', () => {
734
- const show = signal(true)
735
- const onLeave = vi.fn()
736
- const onAfterLeave = vi.fn()
737
-
738
- const { wrapperEl } = setupCollapseRenderer({
739
- show: () => show(),
740
- callbacks: { onLeave, onAfterLeave },
741
- })
742
-
743
- show.set(false)
744
-
745
- expect(onLeave).toHaveBeenCalledTimes(1)
746
- expect(onAfterLeave).toHaveBeenCalledTimes(1)
747
- expect(wrapperEl.style.height).toBe('0px')
748
- expect(wrapperEl.style.overflow).toBe('hidden')
749
- })
750
- })
751
-
752
- describe('Collapse — reduced motion', () => {
753
- beforeEach(() => {
754
- vi.useFakeTimers()
755
- mockScrollHeight(200)
756
- _reducedMotion = true
757
- })
758
-
759
- afterEach(() => {
760
- vi.useRealTimers()
761
- _reducedMotion = false
762
- })
763
-
764
- it('reduced motion: entering skips animation and fires both callbacks', () => {
765
- const show = signal(false)
766
- const onEnter = vi.fn()
767
- const onAfterEnter = vi.fn()
768
-
769
- const { wrapperEl } = setupCollapse({
770
- show,
771
- onEnter,
772
- onAfterEnter,
773
- children: h('div', null, 'Hello') as VNode,
774
- })
775
-
776
- show.set(true)
777
-
778
- expect(onEnter).toHaveBeenCalledTimes(1)
779
- expect(onAfterEnter).toHaveBeenCalledTimes(1)
780
- expect(wrapperEl.style.height).toBe('auto')
781
- expect(wrapperEl.style.overflow).toBe('')
782
- })
783
-
784
- it('reduced motion: leaving skips animation and fires both callbacks', () => {
785
- const show = signal(true)
786
- const onLeave = vi.fn()
787
- const onAfterLeave = vi.fn()
788
-
789
- const { wrapperEl } = setupCollapse({
790
- show,
791
- onLeave,
792
- onAfterLeave,
793
- children: h('div', null, 'Hello') as VNode,
794
- })
795
-
796
- show.set(false)
797
-
798
- expect(onLeave).toHaveBeenCalledTimes(1)
799
- expect(onAfterLeave).toHaveBeenCalledTimes(1)
800
- expect(wrapperEl.style.height).toBe('0px')
801
- expect(wrapperEl.style.overflow).toBe('hidden')
802
- })
803
- })