@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,191 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource @pyreon/core */
|
|
2
|
-
/**
|
|
3
|
-
* Regression: `kinetic('div').stagger()` with `show={() => true}` +
|
|
4
|
-
* `appear` + multiple component-VNode children rendered `<undefined>`
|
|
5
|
-
* tags in place of the children's actual DOM post-hydrate.
|
|
6
|
-
*
|
|
7
|
-
* Real-app reporter (examples/bokisch.com Intro section): SSG'd HTML
|
|
8
|
-
* carried `<h1>Hello</h1>` + tagline + icons; client hydration produced
|
|
9
|
-
* `<undefined></undefined>` tags (literal HTML element with tagName
|
|
10
|
-
* "UNDEFINED") + `<!--pyreon-->` markers in place of every child.
|
|
11
|
-
*
|
|
12
|
-
* Bug class — Pyreon-compiler prop-inlining + cloneVNode-on-a-function:
|
|
13
|
-
*
|
|
14
|
-
* 1. The compiler rewrites local `const children = obj.x` then
|
|
15
|
-
* `<Comp>{children}</Comp>` as `Comp({..., children: () => obj.x})`.
|
|
16
|
-
* Component receives `props.children` as a FUNCTION, not an array.
|
|
17
|
-
*
|
|
18
|
-
* 2. StaggerRenderer iterated `(Array.isArray(children) ? children : [children])`
|
|
19
|
-
* directly. `[function].filter(isVNode)` collapsed to `[]` → the
|
|
20
|
-
* kinetic `<div>` rendered with zero children.
|
|
21
|
-
*
|
|
22
|
-
* 3. Even after StaggerRenderer's fix, TransitionItem's `cloneVNode(props.children, {ref})`
|
|
23
|
-
* tried to clone the same function-wrapped value (also auto-wrapped
|
|
24
|
-
* by the compiler one level down — `{cloneVNode(child, {style})}`
|
|
25
|
-
* became `() => cloneVNode(child, {style})`). Spreading a function
|
|
26
|
-
* via `{...fn, props: {...}}` yields `{props: {...}}` (no own
|
|
27
|
-
* enumerable properties on functions) — the resulting vnode had
|
|
28
|
-
* `type: undefined`. mountElement called `document.createElement(undefined)`
|
|
29
|
-
* → the browser produced literal `<undefined>` tags.
|
|
30
|
-
*
|
|
31
|
-
* Fix: `resolveChildren` helper in both StaggerRenderer (iteration) AND
|
|
32
|
-
* TransitionItem (cloning). Unwraps function-wrapped children eagerly
|
|
33
|
-
* since kinetic snapshots children at render time and does not observe
|
|
34
|
-
* children changes.
|
|
35
|
-
*
|
|
36
|
-
* Bisect-verified: reverting either `resolveChildren` call in this PR
|
|
37
|
-
* fails this spec — StaggerRenderer revert produces zero children in the
|
|
38
|
-
* kinetic `<div>`; TransitionItem revert produces `<undefined>` tags
|
|
39
|
-
* with the right cloned style.
|
|
40
|
-
*/
|
|
41
|
-
import type { VNode } from '@pyreon/core'
|
|
42
|
-
import { h } from '@pyreon/core'
|
|
43
|
-
import { mount } from '@pyreon/runtime-dom'
|
|
44
|
-
import { afterEach, describe, expect, it } from 'vitest'
|
|
45
|
-
import kinetic from '../kinetic'
|
|
46
|
-
import TransitionItem from '../kinetic/TransitionItem'
|
|
47
|
-
|
|
48
|
-
// Build a kinetic Component VNode whose `props.children` is a FUNCTION
|
|
49
|
-
// (not an array), mirroring what the Pyreon vite-plugin emits when JSX
|
|
50
|
-
// children are inlined back at the call site (`<Entrance>{children}</Entrance>`
|
|
51
|
-
// → `jsx(Entrance, { children: () => h.children })`). The kinetic test
|
|
52
|
-
// pipeline uses `vl_rolldown_build` which does NOT do Pyreon's
|
|
53
|
-
// prop-inlining, so we construct the shape directly.
|
|
54
|
-
const buildEntranceWithFunctionChildren = (
|
|
55
|
-
// Wider call signature — kinetic('div').stagger() returns a stagger-mode
|
|
56
|
-
// component whose precise typed shape (`KineticComponent<'div', 'stagger'>`)
|
|
57
|
-
// is narrower than what `kinetic()`'s default returns. The function-
|
|
58
|
-
// children wrapper bypasses the strict children-required typing.
|
|
59
|
-
Entrance: (props: Record<string, unknown>) => VNode | null,
|
|
60
|
-
childArray: VNode[],
|
|
61
|
-
): VNode => {
|
|
62
|
-
// h() puts children in vnode.children (rest args). For mountComponent's
|
|
63
|
-
// merge to leave props.children alone, set it explicitly here.
|
|
64
|
-
return h(Entrance, {
|
|
65
|
-
show: () => true,
|
|
66
|
-
appear: true,
|
|
67
|
-
children: (() => childArray) as unknown as VNode[],
|
|
68
|
-
})
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
let containers: HTMLElement[] = []
|
|
72
|
-
afterEach(() => {
|
|
73
|
-
for (const c of containers) c.remove()
|
|
74
|
-
containers = []
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
describe('kinetic("div").stagger() — function-wrapped children survive render', () => {
|
|
78
|
-
it('iterates function-wrapped children correctly (no <undefined> tags)', () => {
|
|
79
|
-
const Entrance = kinetic('div')
|
|
80
|
-
.enter({ opacity: '0' })
|
|
81
|
-
.enterTo({ opacity: '1' })
|
|
82
|
-
.stagger({ interval: 20 })
|
|
83
|
-
|
|
84
|
-
const tree = buildEntranceWithFunctionChildren(Entrance as never, [
|
|
85
|
-
h('h1', { 'data-id': 'heading' }, 'Hello'),
|
|
86
|
-
h('p', { 'data-id': 'tagline' }, 'tagline'),
|
|
87
|
-
h('ul', { 'data-id': 'icons' }, h('li', null, 'icon-a')),
|
|
88
|
-
])
|
|
89
|
-
|
|
90
|
-
const container = document.createElement('div')
|
|
91
|
-
document.body.appendChild(container)
|
|
92
|
-
containers.push(container)
|
|
93
|
-
|
|
94
|
-
const dispose = mount(tree as VNode, container)
|
|
95
|
-
|
|
96
|
-
// Children are rendered with proper element tags — NOT <undefined>
|
|
97
|
-
const heading = container.querySelector('[data-id="heading"]')
|
|
98
|
-
const tagline = container.querySelector('[data-id="tagline"]')
|
|
99
|
-
const icons = container.querySelector('[data-id="icons"]')
|
|
100
|
-
|
|
101
|
-
expect(
|
|
102
|
-
heading,
|
|
103
|
-
`heading missing — pre-fix the function-wrapped child got mounted as <undefined>. ` +
|
|
104
|
-
`container.innerHTML=${container.innerHTML.slice(0, 600)}`,
|
|
105
|
-
).not.toBeNull()
|
|
106
|
-
expect(heading?.tagName).toBe('H1')
|
|
107
|
-
expect(heading?.textContent).toBe('Hello')
|
|
108
|
-
|
|
109
|
-
expect(tagline).not.toBeNull()
|
|
110
|
-
expect(tagline?.tagName).toBe('P')
|
|
111
|
-
|
|
112
|
-
expect(icons).not.toBeNull()
|
|
113
|
-
expect(icons?.tagName).toBe('UL')
|
|
114
|
-
|
|
115
|
-
// Sanity: no `<undefined>` tags should exist anywhere (pre-fix
|
|
116
|
-
// TransitionItem's cloneVNode(props.children, {ref}) on a function
|
|
117
|
-
// produced `{type: undefined, props: {ref}}` → mountElement called
|
|
118
|
-
// document.createElement(undefined) → `<undefined>` element).
|
|
119
|
-
expect(container.querySelector('undefined')).toBeNull()
|
|
120
|
-
|
|
121
|
-
dispose()
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
it('TransitionItem resolves function-wrapped children before cloneVNode (no <undefined> tag)', () => {
|
|
125
|
-
// Direct test for the SECOND fix-site — TransitionItem's
|
|
126
|
-
// `cloneVNode(props.children, {ref})`. Pre-fix, when the parent
|
|
127
|
-
// (StaggerRenderer/GroupRenderer) emits `<TransitionItem>{cloneVNode(c, {style})}</TransitionItem>`
|
|
128
|
-
// under the Pyreon vite-plugin, the compiler wraps the JSX child as
|
|
129
|
-
// `() => cloneVNode(c, {style})`. TransitionItem then receives
|
|
130
|
-
// `props.children = function`. `cloneVNode(function, {ref})` spreads
|
|
131
|
-
// the function (no own enumerable properties) → produces
|
|
132
|
-
// `{type: undefined, props: {ref}}` → mountElement creates literal
|
|
133
|
-
// `<undefined>` tag.
|
|
134
|
-
const childVNode = h('h1', { 'data-id': 'ti-heading' }, 'Hello')
|
|
135
|
-
const tree = h(TransitionItem, {
|
|
136
|
-
show: () => true,
|
|
137
|
-
appear: false,
|
|
138
|
-
timeout: 100,
|
|
139
|
-
enterStyle: { opacity: '0' },
|
|
140
|
-
enterToStyle: { opacity: '1' },
|
|
141
|
-
enterTransition: 'opacity 50ms ease',
|
|
142
|
-
// Function-wrapped children, mirroring the compiler's emit.
|
|
143
|
-
children: (() => childVNode) as unknown as VNode,
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
const container = document.createElement('div')
|
|
147
|
-
document.body.appendChild(container)
|
|
148
|
-
containers.push(container)
|
|
149
|
-
|
|
150
|
-
const dispose = mount(tree as VNode, container)
|
|
151
|
-
|
|
152
|
-
const heading = container.querySelector('[data-id="ti-heading"]')
|
|
153
|
-
expect(
|
|
154
|
-
heading,
|
|
155
|
-
`heading missing — pre-fix TransitionItem cloned the function, ` +
|
|
156
|
-
`producing <undefined>. container.innerHTML=${container.innerHTML.slice(0, 400)}`,
|
|
157
|
-
).not.toBeNull()
|
|
158
|
-
expect(heading?.tagName).toBe('H1')
|
|
159
|
-
expect(heading?.textContent).toBe('Hello')
|
|
160
|
-
expect(container.querySelector('undefined')).toBeNull()
|
|
161
|
-
|
|
162
|
-
dispose()
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
it('iterates static-array children correctly (control — was always working)', () => {
|
|
166
|
-
const Entrance = kinetic('div')
|
|
167
|
-
.enter({ opacity: '0' })
|
|
168
|
-
.enterTo({ opacity: '1' })
|
|
169
|
-
.stagger({ interval: 20 })
|
|
170
|
-
|
|
171
|
-
// No compiler wrap — children as a plain array.
|
|
172
|
-
const tree = h(
|
|
173
|
-
Entrance,
|
|
174
|
-
{ show: () => true, appear: true },
|
|
175
|
-
h('h1', { 'data-id': 'heading-static' }, 'Static'),
|
|
176
|
-
h('p', { 'data-id': 'tagline-static' }, 't'),
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
const container = document.createElement('div')
|
|
180
|
-
document.body.appendChild(container)
|
|
181
|
-
containers.push(container)
|
|
182
|
-
|
|
183
|
-
const dispose = mount(tree as VNode, container)
|
|
184
|
-
|
|
185
|
-
expect(container.querySelector('[data-id="heading-static"]')?.tagName).toBe('H1')
|
|
186
|
-
expect(container.querySelector('[data-id="tagline-static"]')?.tagName).toBe('P')
|
|
187
|
-
expect(container.querySelector('undefined')).toBeNull()
|
|
188
|
-
|
|
189
|
-
dispose()
|
|
190
|
-
})
|
|
191
|
-
})
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource @pyreon/core */
|
|
2
|
-
/**
|
|
3
|
-
* Regression: PR #731 fixed the kinetic-mode renderers (StaggerRenderer +
|
|
4
|
-
* TransitionItem under `src/kinetic/`) but missed the parallel TOP-LEVEL
|
|
5
|
-
* `<Transition>` and `<Stagger>` components in `src/Transition.tsx` and
|
|
6
|
-
* `src/Stagger.tsx`. They have the SAME iteration + cloneVNode shape and
|
|
7
|
-
* the SAME bug when the Pyreon compiler wraps the children prop in
|
|
8
|
-
* `() => x` (the prop-inlining pass).
|
|
9
|
-
*
|
|
10
|
-
* The Pyreon vite-plugin auto-wraps `<Comp>{x}</Comp>` JSX child
|
|
11
|
-
* expressions in `() => x` for stable prop-derived references; downstream
|
|
12
|
-
* libraries that iterate `props.children` directly at the VNode level or
|
|
13
|
-
* `cloneVNode` them silently break — the function spread produces
|
|
14
|
-
* `{type: undefined}` → `<undefined>` DOM tags. PR #732 added the
|
|
15
|
-
* compiler carve-out for stable references; library-side `resolveChildren`
|
|
16
|
-
* is still needed for the CallExpression-inside-JSX-child shape that the
|
|
17
|
-
* compiler (correctly) doesn't optimize.
|
|
18
|
-
*
|
|
19
|
-
* Bisect-verified: reverting the `resolveChildren` call in `Stagger.tsx`
|
|
20
|
-
* fails the Stagger spec (no children rendered); reverting in
|
|
21
|
-
* `Transition.tsx` fails the Transition spec (`<undefined>` tag rendered
|
|
22
|
-
* instead of the cloned child).
|
|
23
|
-
*/
|
|
24
|
-
import type { VNode } from '@pyreon/core'
|
|
25
|
-
import { h } from '@pyreon/core'
|
|
26
|
-
import { mount } from '@pyreon/runtime-dom'
|
|
27
|
-
import { afterEach, describe, expect, it } from 'vitest'
|
|
28
|
-
import Stagger from '../Stagger'
|
|
29
|
-
import Transition from '../Transition'
|
|
30
|
-
|
|
31
|
-
let containers: HTMLElement[] = []
|
|
32
|
-
afterEach(() => {
|
|
33
|
-
for (const c of containers) c.remove()
|
|
34
|
-
containers = []
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
describe('top-level <Stagger> — function-wrapped children survive render', () => {
|
|
38
|
-
it('iterates function-wrapped children correctly (no empty render)', () => {
|
|
39
|
-
const childArray: VNode[] = [
|
|
40
|
-
h('h1', { 'data-id': 'st-h1' }, 'Hello'),
|
|
41
|
-
h('p', { 'data-id': 'st-p' }, 'tagline'),
|
|
42
|
-
h('ul', { 'data-id': 'st-ul' }, h('li', null, 'a')),
|
|
43
|
-
]
|
|
44
|
-
|
|
45
|
-
const tree = h(Stagger, {
|
|
46
|
-
show: () => true,
|
|
47
|
-
appear: true,
|
|
48
|
-
interval: 20,
|
|
49
|
-
// Compiler-emitted shape: children is a function returning the array.
|
|
50
|
-
children: (() => childArray) as unknown as VNode[],
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
const container = document.createElement('div')
|
|
54
|
-
document.body.appendChild(container)
|
|
55
|
-
containers.push(container)
|
|
56
|
-
|
|
57
|
-
const dispose = mount(tree as VNode, container)
|
|
58
|
-
|
|
59
|
-
const h1 = container.querySelector('[data-id="st-h1"]')
|
|
60
|
-
const p = container.querySelector('[data-id="st-p"]')
|
|
61
|
-
const ul = container.querySelector('[data-id="st-ul"]')
|
|
62
|
-
|
|
63
|
-
expect(
|
|
64
|
-
h1,
|
|
65
|
-
`Stagger collapsed when children is a function — html=${container.innerHTML.slice(0, 400)}`,
|
|
66
|
-
).not.toBeNull()
|
|
67
|
-
expect(h1?.tagName).toBe('H1')
|
|
68
|
-
expect(h1?.textContent).toBe('Hello')
|
|
69
|
-
expect(p?.tagName).toBe('P')
|
|
70
|
-
expect(ul?.tagName).toBe('UL')
|
|
71
|
-
expect(container.querySelector('undefined')).toBeNull()
|
|
72
|
-
|
|
73
|
-
dispose()
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
it('static-array children control — was always working', () => {
|
|
77
|
-
const tree = h(
|
|
78
|
-
Stagger,
|
|
79
|
-
{ show: () => true, appear: true, interval: 20 },
|
|
80
|
-
h('h1', { 'data-id': 'st-static' }, 'Static'),
|
|
81
|
-
h('p', { 'data-id': 'st-static-p' }, 't'),
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
const container = document.createElement('div')
|
|
85
|
-
document.body.appendChild(container)
|
|
86
|
-
containers.push(container)
|
|
87
|
-
const dispose = mount(tree as VNode, container)
|
|
88
|
-
|
|
89
|
-
expect(container.querySelector('[data-id="st-static"]')?.tagName).toBe('H1')
|
|
90
|
-
expect(container.querySelector('[data-id="st-static-p"]')?.tagName).toBe('P')
|
|
91
|
-
|
|
92
|
-
dispose()
|
|
93
|
-
})
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
describe('top-level <Transition> — function-wrapped children survive render', () => {
|
|
97
|
-
it('resolves function-wrapped children before cloneVNode (no <undefined> tag)', () => {
|
|
98
|
-
const childVNode = h('h1', { 'data-id': 'tn-h1' }, 'Hello')
|
|
99
|
-
|
|
100
|
-
const tree = h(Transition, {
|
|
101
|
-
show: () => true,
|
|
102
|
-
appear: false,
|
|
103
|
-
// Compiler-emitted shape.
|
|
104
|
-
children: (() => childVNode) as unknown as VNode,
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
const container = document.createElement('div')
|
|
108
|
-
document.body.appendChild(container)
|
|
109
|
-
containers.push(container)
|
|
110
|
-
|
|
111
|
-
const dispose = mount(tree as VNode, container)
|
|
112
|
-
|
|
113
|
-
const h1 = container.querySelector('[data-id="tn-h1"]')
|
|
114
|
-
expect(
|
|
115
|
-
h1,
|
|
116
|
-
`Transition produced <undefined> — html=${container.innerHTML.slice(0, 400)}`,
|
|
117
|
-
).not.toBeNull()
|
|
118
|
-
expect(h1?.tagName).toBe('H1')
|
|
119
|
-
expect(h1?.textContent).toBe('Hello')
|
|
120
|
-
expect(container.querySelector('undefined')).toBeNull()
|
|
121
|
-
|
|
122
|
-
dispose()
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
it('static-VNode child control — was always working', () => {
|
|
126
|
-
const tree = h(
|
|
127
|
-
Transition,
|
|
128
|
-
{ show: () => true, appear: false },
|
|
129
|
-
h('h1', { 'data-id': 'tn-static' }, 'Static'),
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
const container = document.createElement('div')
|
|
133
|
-
document.body.appendChild(container)
|
|
134
|
-
containers.push(container)
|
|
135
|
-
const dispose = mount(tree as VNode, container)
|
|
136
|
-
|
|
137
|
-
expect(container.querySelector('[data-id="tn-static"]')?.tagName).toBe('H1')
|
|
138
|
-
|
|
139
|
-
dispose()
|
|
140
|
-
})
|
|
141
|
-
})
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
import { signal } from '@pyreon/reactivity'
|
|
2
|
-
import useAnimationEnd from '../useAnimationEnd'
|
|
3
|
-
|
|
4
|
-
const createMockRef = () => {
|
|
5
|
-
const el = document.createElement('div')
|
|
6
|
-
return { current: el }
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
describe('useAnimationEnd', () => {
|
|
10
|
-
beforeEach(() => vi.useFakeTimers())
|
|
11
|
-
afterEach(() => vi.useRealTimers())
|
|
12
|
-
|
|
13
|
-
it('calls onEnd when transitionend fires on the element', () => {
|
|
14
|
-
const onEnd = vi.fn()
|
|
15
|
-
const ref = createMockRef()
|
|
16
|
-
const active = signal(true)
|
|
17
|
-
|
|
18
|
-
useAnimationEnd({ ref, onEnd, active })
|
|
19
|
-
|
|
20
|
-
const event = new Event('transitionend', { bubbles: true })
|
|
21
|
-
Object.defineProperty(event, 'target', { value: ref.current })
|
|
22
|
-
ref.current.dispatchEvent(event)
|
|
23
|
-
|
|
24
|
-
expect(onEnd).toHaveBeenCalledTimes(1)
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
it('calls onEnd when animationend fires on the element', () => {
|
|
28
|
-
const onEnd = vi.fn()
|
|
29
|
-
const ref = createMockRef()
|
|
30
|
-
const active = signal(true)
|
|
31
|
-
|
|
32
|
-
useAnimationEnd({ ref, onEnd, active })
|
|
33
|
-
|
|
34
|
-
const event = new Event('animationend', { bubbles: true })
|
|
35
|
-
Object.defineProperty(event, 'target', { value: ref.current })
|
|
36
|
-
ref.current.dispatchEvent(event)
|
|
37
|
-
|
|
38
|
-
expect(onEnd).toHaveBeenCalledTimes(1)
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('ignores bubbled events from children', () => {
|
|
42
|
-
const onEnd = vi.fn()
|
|
43
|
-
const ref = createMockRef()
|
|
44
|
-
const child = document.createElement('span')
|
|
45
|
-
ref.current.appendChild(child)
|
|
46
|
-
const active = signal(true)
|
|
47
|
-
|
|
48
|
-
useAnimationEnd({ ref, onEnd, active })
|
|
49
|
-
|
|
50
|
-
const event = new Event('transitionend', { bubbles: true })
|
|
51
|
-
child.dispatchEvent(event)
|
|
52
|
-
|
|
53
|
-
expect(onEnd).not.toHaveBeenCalled()
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it('fires timeout fallback when no event fires', () => {
|
|
57
|
-
const onEnd = vi.fn()
|
|
58
|
-
const ref = createMockRef()
|
|
59
|
-
const active = signal(true)
|
|
60
|
-
|
|
61
|
-
useAnimationEnd({ ref, onEnd, active, timeout: 1000 })
|
|
62
|
-
|
|
63
|
-
expect(onEnd).not.toHaveBeenCalled()
|
|
64
|
-
|
|
65
|
-
vi.advanceTimersByTime(1000)
|
|
66
|
-
|
|
67
|
-
expect(onEnd).toHaveBeenCalledTimes(1)
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it('uses default timeout of 5000ms', () => {
|
|
71
|
-
const onEnd = vi.fn()
|
|
72
|
-
const ref = createMockRef()
|
|
73
|
-
const active = signal(true)
|
|
74
|
-
|
|
75
|
-
useAnimationEnd({ ref, onEnd, active })
|
|
76
|
-
|
|
77
|
-
vi.advanceTimersByTime(4999)
|
|
78
|
-
expect(onEnd).not.toHaveBeenCalled()
|
|
79
|
-
|
|
80
|
-
vi.advanceTimersByTime(1)
|
|
81
|
-
expect(onEnd).toHaveBeenCalledTimes(1)
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
it('only fires onEnd once even if multiple events fire', () => {
|
|
85
|
-
const onEnd = vi.fn()
|
|
86
|
-
const ref = createMockRef()
|
|
87
|
-
const active = signal(true)
|
|
88
|
-
|
|
89
|
-
useAnimationEnd({ ref, onEnd, active })
|
|
90
|
-
|
|
91
|
-
const event1 = new Event('transitionend', { bubbles: true })
|
|
92
|
-
Object.defineProperty(event1, 'target', { value: ref.current })
|
|
93
|
-
ref.current.dispatchEvent(event1)
|
|
94
|
-
|
|
95
|
-
const event2 = new Event('animationend', { bubbles: true })
|
|
96
|
-
Object.defineProperty(event2, 'target', { value: ref.current })
|
|
97
|
-
ref.current.dispatchEvent(event2)
|
|
98
|
-
|
|
99
|
-
expect(onEnd).toHaveBeenCalledTimes(1)
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
it('does not fire when active is false', () => {
|
|
103
|
-
const onEnd = vi.fn()
|
|
104
|
-
const ref = createMockRef()
|
|
105
|
-
const active = signal(false)
|
|
106
|
-
|
|
107
|
-
useAnimationEnd({ ref, onEnd, active, timeout: 100 })
|
|
108
|
-
|
|
109
|
-
vi.advanceTimersByTime(200)
|
|
110
|
-
|
|
111
|
-
expect(onEnd).not.toHaveBeenCalled()
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
it('does not fire when active=true but ref.current is null', () => {
|
|
115
|
-
const onEnd = vi.fn()
|
|
116
|
-
const ref = { current: null } as { current: HTMLElement | null }
|
|
117
|
-
const active = signal(true)
|
|
118
|
-
|
|
119
|
-
useAnimationEnd({ ref, onEnd, active, timeout: 100 })
|
|
120
|
-
|
|
121
|
-
// No timer should be set when ref is null
|
|
122
|
-
vi.advanceTimersByTime(200)
|
|
123
|
-
|
|
124
|
-
expect(onEnd).not.toHaveBeenCalled()
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('does not call onEnd twice when transitionend fires and then timeout fires', () => {
|
|
128
|
-
const onEnd = vi.fn()
|
|
129
|
-
const ref = createMockRef()
|
|
130
|
-
const active = signal(true)
|
|
131
|
-
|
|
132
|
-
useAnimationEnd({ ref, onEnd, active, timeout: 1000 })
|
|
133
|
-
|
|
134
|
-
// First: transitionend fires — calls done()
|
|
135
|
-
const event = new Event('transitionend', { bubbles: true })
|
|
136
|
-
Object.defineProperty(event, 'target', { value: ref.current })
|
|
137
|
-
ref.current.dispatchEvent(event)
|
|
138
|
-
|
|
139
|
-
expect(onEnd).toHaveBeenCalledTimes(1)
|
|
140
|
-
|
|
141
|
-
// Second: timeout fires — should be no-op because called is true
|
|
142
|
-
vi.advanceTimersByTime(1000)
|
|
143
|
-
|
|
144
|
-
expect(onEnd).toHaveBeenCalledTimes(1)
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
it('does not call onEnd twice when timeout fires and then transitionend fires', () => {
|
|
148
|
-
const onEnd = vi.fn()
|
|
149
|
-
const ref = createMockRef()
|
|
150
|
-
const active = signal(true)
|
|
151
|
-
|
|
152
|
-
useAnimationEnd({ ref, onEnd, active, timeout: 500 })
|
|
153
|
-
|
|
154
|
-
// First: timeout fires — calls done()
|
|
155
|
-
vi.advanceTimersByTime(500)
|
|
156
|
-
|
|
157
|
-
expect(onEnd).toHaveBeenCalledTimes(1)
|
|
158
|
-
|
|
159
|
-
// Second: transitionend fires — should be no-op via called guard
|
|
160
|
-
const event = new Event('transitionend', { bubbles: true })
|
|
161
|
-
Object.defineProperty(event, 'target', { value: ref.current })
|
|
162
|
-
ref.current.dispatchEvent(event)
|
|
163
|
-
|
|
164
|
-
expect(onEnd).toHaveBeenCalledTimes(1)
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
it('resets called when active transitions from true to false', () => {
|
|
168
|
-
const onEnd = vi.fn()
|
|
169
|
-
const ref = createMockRef()
|
|
170
|
-
const active = signal(true)
|
|
171
|
-
|
|
172
|
-
useAnimationEnd({ ref, onEnd, active, timeout: 1000 })
|
|
173
|
-
|
|
174
|
-
// Fire to set called = true
|
|
175
|
-
const event = new Event('transitionend', { bubbles: true })
|
|
176
|
-
Object.defineProperty(event, 'target', { value: ref.current })
|
|
177
|
-
ref.current.dispatchEvent(event)
|
|
178
|
-
|
|
179
|
-
expect(onEnd).toHaveBeenCalledTimes(1)
|
|
180
|
-
|
|
181
|
-
// Deactivate — resets called
|
|
182
|
-
active.set(false)
|
|
183
|
-
|
|
184
|
-
// Re-activate
|
|
185
|
-
active.set(true)
|
|
186
|
-
|
|
187
|
-
// Should be able to fire again
|
|
188
|
-
const event2 = new Event('transitionend', { bubbles: true })
|
|
189
|
-
Object.defineProperty(event2, 'target', { value: ref.current })
|
|
190
|
-
ref.current.dispatchEvent(event2)
|
|
191
|
-
|
|
192
|
-
expect(onEnd).toHaveBeenCalledTimes(2)
|
|
193
|
-
})
|
|
194
|
-
})
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
// Track lifecycle callbacks
|
|
2
|
-
let mountCallbacks: Array<() => undefined | (() => void)> = []
|
|
3
|
-
let unmountCallbacks: Array<() => void> = []
|
|
4
|
-
|
|
5
|
-
vi.mock('@pyreon/core', () => ({
|
|
6
|
-
onMount: vi.fn((cb: () => undefined | (() => void)) => {
|
|
7
|
-
mountCallbacks.push(cb)
|
|
8
|
-
}),
|
|
9
|
-
onUnmount: vi.fn((cb: () => void) => {
|
|
10
|
-
unmountCallbacks.push(cb)
|
|
11
|
-
}),
|
|
12
|
-
}))
|
|
13
|
-
|
|
14
|
-
vi.mock('@pyreon/reactivity', () => {
|
|
15
|
-
const signal = <T>(initial: T) => {
|
|
16
|
-
let value = initial
|
|
17
|
-
const s = (() => value) as (() => T) & {
|
|
18
|
-
set: (v: T) => void
|
|
19
|
-
update: (fn: (c: T) => T) => void
|
|
20
|
-
peek: () => T
|
|
21
|
-
subscribe: () => () => void
|
|
22
|
-
direct: () => () => void
|
|
23
|
-
label: string | undefined
|
|
24
|
-
debug: () => { name: string | undefined; value: T; subscriberCount: number }
|
|
25
|
-
}
|
|
26
|
-
s.set = (v: T) => {
|
|
27
|
-
value = v
|
|
28
|
-
}
|
|
29
|
-
s.update = (fn: (c: T) => T) => {
|
|
30
|
-
value = fn(value)
|
|
31
|
-
}
|
|
32
|
-
s.peek = () => value
|
|
33
|
-
s.subscribe = () => () => undefined
|
|
34
|
-
s.direct = () => () => undefined
|
|
35
|
-
s.label = undefined
|
|
36
|
-
s.debug = () => ({ name: undefined, value, subscriberCount: 0 })
|
|
37
|
-
return s
|
|
38
|
-
}
|
|
39
|
-
// No-op stub for the DI hook `@pyreon/core/context.ts` calls at module
|
|
40
|
-
// load. See sibling test mocks for the full rationale.
|
|
41
|
-
const setSnapshotCapture = () => {}
|
|
42
|
-
return { signal, setSnapshotCapture }
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
import { useReducedMotion } from '../useReducedMotion'
|
|
46
|
-
|
|
47
|
-
describe('useReducedMotion', () => {
|
|
48
|
-
let changeHandlers: Array<(e: any) => void>
|
|
49
|
-
let removedHandlers: Array<(e: any) => void>
|
|
50
|
-
|
|
51
|
-
const createMockMQL = (matches: boolean) => ({
|
|
52
|
-
matches,
|
|
53
|
-
media: '(prefers-reduced-motion: reduce)',
|
|
54
|
-
addEventListener: vi.fn((event: string, handler: (e: any) => void) => {
|
|
55
|
-
if (event === 'change') changeHandlers.push(handler)
|
|
56
|
-
}),
|
|
57
|
-
removeEventListener: vi.fn((event: string, handler: (e: any) => void) => {
|
|
58
|
-
if (event === 'change') removedHandlers.push(handler)
|
|
59
|
-
}),
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
beforeEach(() => {
|
|
63
|
-
mountCallbacks = []
|
|
64
|
-
unmountCallbacks = []
|
|
65
|
-
changeHandlers = []
|
|
66
|
-
removedHandlers = []
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
afterEach(() => {
|
|
70
|
-
vi.restoreAllMocks()
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('returns false initially', () => {
|
|
74
|
-
vi.stubGlobal(
|
|
75
|
-
'matchMedia',
|
|
76
|
-
vi.fn(() => createMockMQL(false)),
|
|
77
|
-
)
|
|
78
|
-
const result = useReducedMotion()
|
|
79
|
-
expect(result()).toBe(false)
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it('reads matchMedia state on mount (true)', () => {
|
|
83
|
-
vi.stubGlobal(
|
|
84
|
-
'matchMedia',
|
|
85
|
-
vi.fn(() => createMockMQL(true)),
|
|
86
|
-
)
|
|
87
|
-
const result = useReducedMotion()
|
|
88
|
-
|
|
89
|
-
// Fire mount callback
|
|
90
|
-
for (const cb of mountCallbacks) cb()
|
|
91
|
-
|
|
92
|
-
expect(result()).toBe(true)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it('reads matchMedia state on mount (false)', () => {
|
|
96
|
-
vi.stubGlobal(
|
|
97
|
-
'matchMedia',
|
|
98
|
-
vi.fn(() => createMockMQL(false)),
|
|
99
|
-
)
|
|
100
|
-
const result = useReducedMotion()
|
|
101
|
-
|
|
102
|
-
for (const cb of mountCallbacks) cb()
|
|
103
|
-
|
|
104
|
-
expect(result()).toBe(false)
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it('reacts to change events', () => {
|
|
108
|
-
vi.stubGlobal(
|
|
109
|
-
'matchMedia',
|
|
110
|
-
vi.fn(() => createMockMQL(false)),
|
|
111
|
-
)
|
|
112
|
-
const result = useReducedMotion()
|
|
113
|
-
|
|
114
|
-
for (const cb of mountCallbacks) cb()
|
|
115
|
-
expect(result()).toBe(false)
|
|
116
|
-
|
|
117
|
-
// Simulate preference change
|
|
118
|
-
for (const handler of changeHandlers) {
|
|
119
|
-
handler({ matches: true })
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
expect(result()).toBe(true)
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
it('queries the correct media string', () => {
|
|
126
|
-
const mockMatchMedia = vi.fn(() => createMockMQL(false))
|
|
127
|
-
vi.stubGlobal('matchMedia', mockMatchMedia)
|
|
128
|
-
|
|
129
|
-
useReducedMotion()
|
|
130
|
-
for (const cb of mountCallbacks) cb()
|
|
131
|
-
|
|
132
|
-
expect(mockMatchMedia).toHaveBeenCalledWith('(prefers-reduced-motion: reduce)')
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
it('registers a change listener on mount', () => {
|
|
136
|
-
vi.stubGlobal(
|
|
137
|
-
'matchMedia',
|
|
138
|
-
vi.fn(() => createMockMQL(false)),
|
|
139
|
-
)
|
|
140
|
-
useReducedMotion()
|
|
141
|
-
|
|
142
|
-
for (const cb of mountCallbacks) cb()
|
|
143
|
-
|
|
144
|
-
expect(changeHandlers).toHaveLength(1)
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
it('removes the change listener on unmount', () => {
|
|
148
|
-
vi.stubGlobal(
|
|
149
|
-
'matchMedia',
|
|
150
|
-
vi.fn(() => createMockMQL(false)),
|
|
151
|
-
)
|
|
152
|
-
useReducedMotion()
|
|
153
|
-
|
|
154
|
-
for (const cb of mountCallbacks) cb()
|
|
155
|
-
expect(changeHandlers).toHaveLength(1)
|
|
156
|
-
|
|
157
|
-
for (const cb of unmountCallbacks) cb()
|
|
158
|
-
expect(removedHandlers).toHaveLength(1)
|
|
159
|
-
})
|
|
160
|
-
})
|