@pyreon/kinetic 0.24.5 → 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/Collapse.tsx +0 -166
- package/src/Stagger.tsx +0 -63
- package/src/Transition.tsx +0 -280
- package/src/TransitionGroup.tsx +0 -139
- package/src/__tests__/Collapse.test.tsx +0 -803
- package/src/__tests__/GroupRenderer.test.tsx +0 -434
- package/src/__tests__/StaggerRenderer.test.tsx +0 -523
- package/src/__tests__/Transition.ssr.test.tsx +0 -183
- package/src/__tests__/Transition.test.tsx +0 -403
- package/src/__tests__/TransitionItem.test.tsx +0 -514
- package/src/__tests__/kinetic-modes.ssr.test.tsx +0 -214
- package/src/__tests__/kinetic.browser.test.tsx +0 -327
- package/src/__tests__/kinetic.test.tsx +0 -565
- package/src/__tests__/presets.test.ts +0 -46
- package/src/__tests__/stagger-component-children-hydration.test.tsx +0 -191
- package/src/__tests__/top-level-transition-stagger-function-children.test.tsx +0 -141
- package/src/__tests__/useAnimationEnd.test.ts +0 -194
- package/src/__tests__/useReducedMotion.test.ts +0 -160
- package/src/__tests__/useTransitionState.test.ts +0 -132
- package/src/__tests__/utils.test.ts +0 -139
- package/src/index.ts +0 -15
- package/src/jsx-augment.d.ts +0 -12
- package/src/kinetic/CollapseRenderer.tsx +0 -216
- package/src/kinetic/GroupRenderer.tsx +0 -149
- package/src/kinetic/StaggerRenderer.tsx +0 -94
- package/src/kinetic/TransitionItem.tsx +0 -250
- package/src/kinetic/TransitionRenderer.tsx +0 -230
- package/src/kinetic/createKineticComponent.tsx +0 -224
- package/src/kinetic/types.ts +0 -149
- package/src/kinetic.ts +0 -25
- package/src/presets.ts +0 -66
- package/src/types.ts +0 -118
- package/src/useAnimationEnd.ts +0 -59
- package/src/useReducedMotion.ts +0 -28
- package/src/useTransitionState.ts +0 -62
- 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
|
-
})
|