@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,514 +0,0 @@
1
- import type { VNode } from '@pyreon/core'
2
- import { h } from '@pyreon/core'
3
- import { signal } from '@pyreon/reactivity'
4
-
5
- let _reducedMotion = false
6
-
7
- vi.mock('../useReducedMotion', () => ({
8
- useReducedMotion: () => () => _reducedMotion,
9
- }))
10
-
11
- import TransitionItem from '../kinetic/TransitionItem'
12
-
13
- // Mock rAF for deterministic double-rAF testing
14
- let rafCallbacks: (() => void)[] = []
15
- const originalRaf = globalThis.requestAnimationFrame
16
- const originalCaf = globalThis.cancelAnimationFrame
17
-
18
- beforeEach(() => {
19
- vi.useFakeTimers()
20
- rafCallbacks = []
21
-
22
- vi.stubGlobal(
23
- 'requestAnimationFrame',
24
- vi.fn((cb: () => void) => {
25
- rafCallbacks.push(cb)
26
- return rafCallbacks.length
27
- }),
28
- )
29
-
30
- vi.stubGlobal('cancelAnimationFrame', vi.fn())
31
- })
32
-
33
- afterEach(() => {
34
- vi.useRealTimers()
35
- vi.stubGlobal('requestAnimationFrame', originalRaf)
36
- vi.stubGlobal('cancelAnimationFrame', originalCaf)
37
- })
38
-
39
- const flushRaf = () => {
40
- const cbs = [...rafCallbacks]
41
- rafCallbacks = []
42
- for (const cb of cbs) cb()
43
- }
44
-
45
- const fireTransitionEnd = (el: HTMLElement) => {
46
- const event = new Event('transitionend', { bubbles: true })
47
- Object.defineProperty(event, 'target', { value: el })
48
- el.dispatchEvent(event)
49
- }
50
-
51
- /**
52
- * Recursively finds and invokes all refs in a VNode tree,
53
- * wiring them to the given element.
54
- */
55
- const wireRef = (vnode: VNode | null, el: HTMLElement) => {
56
- if (!vnode) return
57
- const visitNode = (node: VNode) => {
58
- const nodeProps = node.props as Record<string, unknown>
59
- if (typeof nodeProps?.ref === 'function') {
60
- ;(nodeProps.ref as (element: HTMLElement | null) => void)(el)
61
- } else if (nodeProps?.ref && typeof nodeProps.ref === 'object') {
62
- ;(nodeProps.ref as { current: HTMLElement | null }).current = el
63
- }
64
- if (node.children) {
65
- const ch = Array.isArray(node.children) ? node.children : [node.children]
66
- for (const c of ch) {
67
- if (c && typeof c === 'object' && 'type' in (c as object)) visitNode(c as VNode)
68
- }
69
- }
70
- if (nodeProps?.children) {
71
- const pc = Array.isArray(nodeProps.children) ? nodeProps.children : [nodeProps.children]
72
- for (const c of pc) {
73
- if (c && typeof c === 'object' && 'type' in (c as object)) visitNode(c as VNode)
74
- }
75
- }
76
- if (
77
- nodeProps?.fallback &&
78
- typeof nodeProps.fallback === 'object' &&
79
- 'type' in (nodeProps.fallback as object)
80
- ) {
81
- visitNode(nodeProps.fallback as VNode)
82
- }
83
- }
84
- visitNode(vnode)
85
- }
86
-
87
- /**
88
- * Helper: call TransitionItem and wire a mock element to refs.
89
- */
90
- const setupTransitionItem = (props: Record<string, unknown>) => {
91
- const el = document.createElement('div')
92
- // Real h() instead of a mock literal — same VNode shape as production.
93
- const child = h('div', { 'data-testid': 'child' }, 'Hello') as VNode
94
-
95
- const vnode = TransitionItem({
96
- ...props,
97
- children: child,
98
- } as any)
99
-
100
- wireRef(vnode, el)
101
-
102
- return { vnode, el }
103
- }
104
-
105
- describe('TransitionItem', () => {
106
- it('returns a VNode when show returns true', () => {
107
- const show = () => true
108
- const child = h('div', null, 'Hello') as VNode
109
- const vnode = TransitionItem({ show, children: child })
110
- expect(vnode).not.toBeNull()
111
- })
112
-
113
- it('wraps child in a Show component', () => {
114
- const show = () => true
115
- const child = h('div', null, 'Hello') as VNode
116
- const vnode = TransitionItem({ show, children: child })
117
- expect(vnode).not.toBeNull()
118
- expect(typeof vnode?.type).toBe('function')
119
- })
120
-
121
- it('clones child VNode with merged ref', () => {
122
- const show = () => true
123
- const child = h('div', null, 'Hello') as VNode
124
- const vnode = TransitionItem({ show, children: child })
125
-
126
- // The Show component's children (or fallback) should have a ref prop
127
- const showProps = vnode?.props as Record<string, unknown>
128
- const showChildren = showProps?.children as VNode | undefined
129
- if (showChildren) {
130
- const childProps = showChildren.props as Record<string, unknown>
131
- expect(childProps?.ref).toBeDefined()
132
- expect(typeof childProps?.ref).toBe('function')
133
- }
134
- })
135
-
136
- it('fires onEnter callback when entering', () => {
137
- const show = signal(false)
138
- const onEnter = vi.fn()
139
-
140
- setupTransitionItem({ show: () => show(), onEnter })
141
-
142
- show.set(true)
143
- expect(onEnter).toHaveBeenCalledTimes(1)
144
- })
145
-
146
- it('fires onLeave callback when leaving', () => {
147
- const show = signal(true)
148
- const onLeave = vi.fn()
149
-
150
- setupTransitionItem({ show: () => show(), onLeave })
151
-
152
- show.set(false)
153
- expect(onLeave).toHaveBeenCalledTimes(1)
154
- })
155
-
156
- it('applies enter classes when entering', () => {
157
- const show = signal(false)
158
- const { el } = setupTransitionItem({
159
- show: () => show(),
160
- enter: 'ti-enter',
161
- enterFrom: 'ti-enter-from',
162
- enterTo: 'ti-enter-to',
163
- })
164
-
165
- show.set(true)
166
-
167
- expect(el.classList.contains('ti-enter')).toBe(true)
168
- expect(el.classList.contains('ti-enter-from')).toBe(true)
169
- expect(el.classList.contains('ti-enter-to')).toBe(false)
170
- })
171
-
172
- it('swaps enterFrom to enterTo after double rAF', () => {
173
- const show = signal(false)
174
- const { el } = setupTransitionItem({
175
- show: () => show(),
176
- enter: 'ti-enter',
177
- enterFrom: 'ti-enter-from',
178
- enterTo: 'ti-enter-to',
179
- })
180
-
181
- show.set(true)
182
-
183
- flushRaf()
184
- flushRaf()
185
-
186
- expect(el.classList.contains('ti-enter')).toBe(true)
187
- expect(el.classList.contains('ti-enter-from')).toBe(false)
188
- expect(el.classList.contains('ti-enter-to')).toBe(true)
189
- })
190
-
191
- it('applies leave classes when leaving', () => {
192
- const show = signal(true)
193
- const { el } = setupTransitionItem({
194
- show: () => show(),
195
- leave: 'ti-leave',
196
- leaveFrom: 'ti-leave-from',
197
- leaveTo: 'ti-leave-to',
198
- })
199
-
200
- show.set(false)
201
-
202
- expect(el.classList.contains('ti-leave')).toBe(true)
203
- expect(el.classList.contains('ti-leave-from')).toBe(true)
204
-
205
- flushRaf()
206
- flushRaf()
207
-
208
- expect(el.classList.contains('ti-leave-from')).toBe(false)
209
- expect(el.classList.contains('ti-leave-to')).toBe(true)
210
- })
211
-
212
- it('applies enter style transitions', () => {
213
- const show = signal(false)
214
- const { el } = setupTransitionItem({
215
- show: () => show(),
216
- enterStyle: { opacity: 0 },
217
- enterToStyle: { opacity: 1 },
218
- enterTransition: 'opacity 300ms ease',
219
- })
220
-
221
- show.set(true)
222
-
223
- expect(el.style.opacity).toBe('0')
224
- expect(el.style.transition).toBe('opacity 300ms ease')
225
-
226
- flushRaf()
227
- flushRaf()
228
-
229
- expect(el.style.opacity).toBe('1')
230
- })
231
-
232
- it('applies leave style transitions', () => {
233
- const show = signal(true)
234
- const { el } = setupTransitionItem({
235
- show: () => show(),
236
- leaveStyle: { opacity: 1 },
237
- leaveToStyle: { opacity: 0 },
238
- leaveTransition: 'opacity 200ms ease-in',
239
- })
240
-
241
- show.set(false)
242
-
243
- expect(el.style.opacity).toBe('1')
244
- expect(el.style.transition).toBe('opacity 200ms ease-in')
245
-
246
- flushRaf()
247
- flushRaf()
248
-
249
- expect(el.style.opacity).toBe('0')
250
- })
251
-
252
- it('fires onAfterEnter after transitionend', () => {
253
- const show = signal(false)
254
- const onAfterEnter = vi.fn()
255
-
256
- const { el } = setupTransitionItem({ show: () => show(), onAfterEnter })
257
-
258
- show.set(true)
259
- expect(onAfterEnter).not.toHaveBeenCalled()
260
-
261
- flushRaf()
262
- flushRaf()
263
- fireTransitionEnd(el)
264
-
265
- expect(onAfterEnter).toHaveBeenCalledTimes(1)
266
- })
267
-
268
- it('fires onAfterLeave after transitionend', () => {
269
- const show = signal(true)
270
- const onAfterLeave = vi.fn()
271
-
272
- const { el } = setupTransitionItem({ show: () => show(), onAfterLeave })
273
-
274
- show.set(false)
275
- expect(onAfterLeave).not.toHaveBeenCalled()
276
-
277
- flushRaf()
278
- flushRaf()
279
- fireTransitionEnd(el)
280
-
281
- expect(onAfterLeave).toHaveBeenCalledTimes(1)
282
- })
283
-
284
- it('cleans up enter classes after transitionend', () => {
285
- const show = signal(false)
286
- const { el } = setupTransitionItem({
287
- show: () => show(),
288
- enter: 'ti-enter',
289
- enterFrom: 'ti-enter-from',
290
- enterTo: 'ti-enter-to',
291
- })
292
-
293
- show.set(true)
294
- flushRaf()
295
- flushRaf()
296
- fireTransitionEnd(el)
297
-
298
- // enter class should be removed on entered stage
299
- expect(el.classList.contains('ti-enter')).toBe(false)
300
- })
301
-
302
- it('cleans up transition style on entered stage', () => {
303
- const show = signal(false)
304
- const { el } = setupTransitionItem({
305
- show: () => show(),
306
- enter: 'ti-enter',
307
- enterTransition: 'opacity 300ms ease',
308
- enterStyle: { opacity: 0 },
309
- enterToStyle: { opacity: 1 },
310
- })
311
-
312
- show.set(true)
313
- expect(el.style.transition).toBe('opacity 300ms ease')
314
- expect(el.classList.contains('ti-enter')).toBe(true)
315
-
316
- flushRaf()
317
- flushRaf()
318
- fireTransitionEnd(el)
319
-
320
- expect(el.style.transition).toBe('')
321
- expect(el.classList.contains('ti-enter')).toBe(false)
322
- })
323
-
324
- it('appear=true fires onEnter on initial mount', () => {
325
- const show = signal(true)
326
- const onEnter = vi.fn()
327
-
328
- setupTransitionItem({ show: () => show(), appear: true, onEnter })
329
-
330
- expect(onEnter).toHaveBeenCalledTimes(1)
331
- })
332
-
333
- it('appear=false does not fire onEnter on initial mount when show is true', () => {
334
- const show = signal(true)
335
- const onEnter = vi.fn()
336
-
337
- setupTransitionItem({ show: () => show(), appear: false, onEnter })
338
-
339
- expect(onEnter).not.toHaveBeenCalled()
340
- })
341
-
342
- it('timeout fallback completes transition when transitionend never fires', () => {
343
- const show = signal(false)
344
- const onAfterEnter = vi.fn()
345
-
346
- setupTransitionItem({ show: () => show(), timeout: 1000, onAfterEnter })
347
-
348
- show.set(true)
349
- expect(onAfterEnter).not.toHaveBeenCalled()
350
-
351
- vi.advanceTimersByTime(1000)
352
-
353
- expect(onAfterEnter).toHaveBeenCalledTimes(1)
354
- })
355
-
356
- it('unmount=false keeps element with display:none when hidden', () => {
357
- const show = () => false
358
- const child = h('div', null, 'Hello') as VNode
359
- const vnode = TransitionItem({ show, unmount: false, children: child })
360
-
361
- expect(vnode).not.toBeNull()
362
- // With unmount=false, the fallback should contain a cloned VNode with display:none
363
- const showProps = vnode?.props as Record<string, unknown>
364
- if (showProps?.fallback && typeof showProps.fallback === 'object') {
365
- const fallbackNode = showProps.fallback as VNode
366
- const fallbackProps = fallbackNode.props as Record<string, unknown>
367
- const style = fallbackProps?.style as Record<string, unknown> | undefined
368
- expect(style?.display).toBe('none')
369
- }
370
- })
371
-
372
- it('unmount=false fallback has a merged ref', () => {
373
- const show = () => false
374
- const child = h('div', null, 'Hello') as VNode
375
- const vnode = TransitionItem({ show, unmount: false, children: child })
376
-
377
- const showProps = vnode?.props as Record<string, unknown>
378
- if (showProps?.fallback && typeof showProps.fallback === 'object') {
379
- const fallbackNode = showProps.fallback as VNode
380
- const fallbackProps = fallbackNode.props as Record<string, unknown>
381
- expect(fallbackProps?.ref).toBeDefined()
382
- expect(typeof fallbackProps?.ref).toBe('function')
383
- }
384
- })
385
-
386
- it('unmount=false merges existing child style with display:none', () => {
387
- const show = () => false
388
- const child = h('div', { style: { color: 'red', opacity: 1 } }, 'Hello') as VNode
389
- const vnode = TransitionItem({ show, unmount: false, children: child })
390
-
391
- const showProps = vnode?.props as Record<string, unknown>
392
- if (showProps?.fallback && typeof showProps.fallback === 'object') {
393
- const fallbackNode = showProps.fallback as VNode
394
- const fallbackProps = fallbackNode.props as Record<string, unknown>
395
- const style = fallbackProps?.style as Record<string, unknown> | undefined
396
- expect(style?.color).toBe('red')
397
- expect(style?.opacity).toBe(1)
398
- expect(style?.display).toBe('none')
399
- }
400
- })
401
-
402
- it('appear=true applies enter classes on initial mount when show is true', () => {
403
- const show = signal(true)
404
- const { el } = setupTransitionItem({
405
- show: () => show(),
406
- appear: true,
407
- enter: 'ti-enter',
408
- enterFrom: 'ti-enter-from',
409
- enterTo: 'ti-enter-to',
410
- })
411
-
412
- // After appear, entering classes should be applied
413
- expect(el.classList.contains('ti-enter')).toBe(true)
414
- expect(el.classList.contains('ti-enter-from')).toBe(true)
415
-
416
- flushRaf()
417
- flushRaf()
418
-
419
- expect(el.classList.contains('ti-enter-from')).toBe(false)
420
- expect(el.classList.contains('ti-enter-to')).toBe(true)
421
- })
422
-
423
- it('appear=true completes full enter lifecycle', () => {
424
- const show = signal(true)
425
- const onEnter = vi.fn()
426
- const onAfterEnter = vi.fn()
427
-
428
- const { el } = setupTransitionItem({
429
- show: () => show(),
430
- appear: true,
431
- onEnter,
432
- onAfterEnter,
433
- enter: 'ti-enter',
434
- })
435
-
436
- expect(onEnter).toHaveBeenCalledTimes(1)
437
-
438
- flushRaf()
439
- flushRaf()
440
- fireTransitionEnd(el)
441
-
442
- expect(onAfterEnter).toHaveBeenCalledTimes(1)
443
- // After entered stage, enter class should be cleaned up
444
- expect(el.classList.contains('ti-enter')).toBe(false)
445
- expect(el.style.transition).toBe('')
446
- })
447
- })
448
-
449
- describe('TransitionItem — reduced motion', () => {
450
- beforeEach(() => {
451
- vi.useFakeTimers()
452
- rafCallbacks = []
453
- _reducedMotion = true
454
-
455
- vi.stubGlobal(
456
- 'requestAnimationFrame',
457
- vi.fn((cb: () => void) => {
458
- rafCallbacks.push(cb)
459
- return rafCallbacks.length
460
- }),
461
- )
462
- vi.stubGlobal('cancelAnimationFrame', vi.fn())
463
- })
464
-
465
- afterEach(() => {
466
- vi.useRealTimers()
467
- vi.stubGlobal('requestAnimationFrame', originalRaf)
468
- vi.stubGlobal('cancelAnimationFrame', originalCaf)
469
- _reducedMotion = false
470
- })
471
-
472
- it('reduced motion: entering fires onEnter and onAfterEnter immediately', () => {
473
- const show = signal(false)
474
- const onEnter = vi.fn()
475
- const onAfterEnter = vi.fn()
476
-
477
- setupTransitionItem({ show: () => show(), onEnter, onAfterEnter })
478
-
479
- show.set(true)
480
-
481
- expect(onEnter).toHaveBeenCalledTimes(1)
482
- expect(onAfterEnter).toHaveBeenCalledTimes(1)
483
- })
484
-
485
- it('reduced motion: leaving fires onLeave and onAfterLeave immediately', () => {
486
- const show = signal(true)
487
- const onLeave = vi.fn()
488
- const onAfterLeave = vi.fn()
489
-
490
- setupTransitionItem({ show: () => show(), onLeave, onAfterLeave })
491
-
492
- show.set(false)
493
-
494
- expect(onLeave).toHaveBeenCalledTimes(1)
495
- expect(onAfterLeave).toHaveBeenCalledTimes(1)
496
- })
497
-
498
- it('reduced motion: does not apply CSS classes or rAF', () => {
499
- const show = signal(false)
500
- const { el } = setupTransitionItem({
501
- show: () => show(),
502
- enter: 'ti-enter',
503
- enterFrom: 'ti-enter-from',
504
- enterTo: 'ti-enter-to',
505
- })
506
-
507
- show.set(true)
508
-
509
- // No classes should be applied — reduced motion skips CSS transitions
510
- expect(el.classList.contains('ti-enter')).toBe(false)
511
- expect(el.classList.contains('ti-enter-from')).toBe(false)
512
- expect(rafCallbacks.length).toBe(0)
513
- })
514
- })