@remix-run/ui 0.1.1 → 0.2.0
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/README.md +33 -10
- package/dist/animation/animate-layout-mixin.js +66 -66
- package/dist/animation/animate-layout-mixin.js.map +1 -1
- package/dist/animation/spring.d.ts +6 -35
- package/dist/animation/spring.js +9 -8
- package/dist/animation/spring.js.map +1 -1
- package/dist/components/accordion/accordion.d.ts +7 -7
- package/dist/components/accordion/accordion.js +0 -2
- package/dist/components/accordion/accordion.js.map +1 -1
- package/dist/components/accordion/demos/card.demo.d.ts +6 -0
- package/dist/components/accordion/demos/card.demo.js +58 -0
- package/dist/components/accordion/demos/card.demo.js.map +1 -0
- package/dist/components/accordion/demos/multiple.demo.d.ts +6 -0
- package/dist/components/accordion/demos/multiple.demo.js +19 -0
- package/dist/components/accordion/demos/multiple.demo.js.map +1 -0
- package/dist/components/accordion/demos/overview.demo.d.ts +6 -0
- package/dist/components/accordion/demos/overview.demo.js +19 -0
- package/dist/components/accordion/demos/overview.demo.js.map +1 -0
- package/dist/components/breadcrumbs/breadcrumbs.d.ts +1 -1
- package/dist/components/breadcrumbs/breadcrumbs.js +0 -2
- package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -1
- package/dist/components/breadcrumbs/demos/basic.demo.d.ts +5 -0
- package/dist/components/breadcrumbs/demos/basic.demo.js +14 -0
- package/dist/components/breadcrumbs/demos/basic.demo.js.map +1 -0
- package/dist/components/breadcrumbs/demos/separator.demo.d.ts +5 -0
- package/dist/components/breadcrumbs/demos/separator.demo.js +14 -0
- package/dist/components/breadcrumbs/demos/separator.demo.js.map +1 -0
- package/dist/components/button/button.d.ts +86 -4
- package/dist/components/button/button.js +63 -3
- package/dist/components/button/button.js.map +1 -1
- package/dist/components/button/demos/aliases.demo.d.ts +6 -0
- package/dist/components/button/demos/aliases.demo.js +18 -0
- package/dist/components/button/demos/aliases.demo.js.map +1 -0
- package/dist/components/button/demos/basic.demo.d.ts +6 -0
- package/dist/components/button/demos/basic.demo.js +19 -0
- package/dist/components/button/demos/basic.demo.js.map +1 -0
- package/dist/components/button/demos/states.demo.d.ts +6 -0
- package/dist/components/button/demos/states.demo.js +29 -0
- package/dist/components/button/demos/states.demo.js.map +1 -0
- package/dist/components/combobox/combobox.d.ts +11 -11
- package/dist/components/combobox/combobox.js +0 -2
- package/dist/components/combobox/combobox.js.map +1 -1
- package/dist/components/combobox/demos/overview.demo.d.ts +6 -0
- package/dist/components/combobox/demos/overview.demo.js +153 -0
- package/dist/components/combobox/demos/overview.demo.js.map +1 -0
- package/dist/components/listbox/demos/overview.demo.d.ts +6 -0
- package/dist/components/listbox/demos/overview.demo.js +64 -0
- package/dist/components/listbox/demos/overview.demo.js.map +1 -0
- package/dist/components/listbox/listbox.d.ts +4 -9
- package/dist/components/listbox/listbox.js +21 -33
- package/dist/components/listbox/listbox.js.map +1 -1
- package/dist/components/menu/demos/bubbling.demo.d.ts +5 -0
- package/dist/components/menu/demos/bubbling.demo.js +15 -0
- package/dist/components/menu/demos/bubbling.demo.js.map +1 -0
- package/dist/components/menu/demos/overview.demo.d.ts +6 -0
- package/dist/components/menu/demos/overview.demo.js +32 -0
- package/dist/components/menu/demos/overview.demo.js.map +1 -0
- package/dist/components/menu/menu.d.ts +15 -24
- package/dist/components/menu/menu.js +23 -38
- package/dist/components/menu/menu.js.map +1 -1
- package/dist/components/popover/demos/overview.demo.d.ts +6 -0
- package/dist/components/popover/demos/overview.demo.js +64 -0
- package/dist/components/popover/demos/overview.demo.js.map +1 -0
- package/dist/components/popover/popover.d.ts +5 -5
- package/dist/components/popover/popover.js.map +1 -1
- package/dist/components/select/demos/deconstructed.demo.d.ts +6 -0
- package/dist/components/select/demos/deconstructed.demo.js +53 -0
- package/dist/components/select/demos/deconstructed.demo.js.map +1 -0
- package/dist/components/select/demos/overview.demo.d.ts +5 -0
- package/dist/components/select/demos/overview.demo.js +27 -0
- package/dist/components/select/demos/overview.demo.js.map +1 -0
- package/dist/components/select/select.d.ts +11 -11
- package/dist/components/select/select.js +17 -26
- package/dist/components/select/select.js.map +1 -1
- package/dist/components/tabs/tabs.d.ts +6 -6
- package/dist/components/tabs/tabs.js +0 -2
- package/dist/components/tabs/tabs.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/interactions/keydown/keydown.d.ts +1 -1
- package/dist/interactions/outside-click/outside-click-mixin.d.ts +2 -1
- package/dist/interactions/typeahead/typeahead-mixin.d.ts +3 -1
- package/dist/runtime/component.d.ts +18 -12
- package/dist/runtime/component.js +93 -80
- package/dist/runtime/component.js.map +1 -1
- package/dist/runtime/core/attributes.d.ts +14 -0
- package/dist/runtime/core/attributes.js +103 -0
- package/dist/runtime/core/attributes.js.map +1 -0
- package/dist/runtime/core/children.d.ts +7 -0
- package/dist/runtime/core/children.js +30 -0
- package/dist/runtime/core/children.js.map +1 -0
- package/dist/runtime/core/props.d.ts +9 -0
- package/dist/runtime/core/props.js +170 -0
- package/dist/runtime/core/props.js.map +1 -0
- package/dist/runtime/core/vnode.d.ts +3 -0
- package/dist/runtime/core/vnode.js +39 -0
- package/dist/runtime/core/vnode.js.map +1 -0
- package/dist/runtime/create-element.js +2 -1
- package/dist/runtime/create-element.js.map +1 -1
- package/dist/runtime/diff-dom.js +47 -14
- package/dist/runtime/diff-dom.js.map +1 -1
- package/dist/runtime/diff-props.d.ts +1 -2
- package/dist/runtime/diff-props.js +2 -181
- package/dist/runtime/diff-props.js.map +1 -1
- package/dist/runtime/dom.d.ts +20 -9
- package/dist/runtime/frame.d.ts +23 -2
- package/dist/runtime/frame.js +41 -47
- package/dist/runtime/frame.js.map +1 -1
- package/dist/runtime/jsx.d.ts +1 -1
- package/dist/runtime/jsx.js +2 -28
- package/dist/runtime/jsx.js.map +1 -1
- package/dist/runtime/mixins/link-mixin.d.ts +5 -1
- package/dist/runtime/mixins/link-mixin.js.map +1 -1
- package/dist/runtime/mixins/mixin.d.ts +2 -1
- package/dist/runtime/mixins/mixin.js +9 -4
- package/dist/runtime/mixins/mixin.js.map +1 -1
- package/dist/runtime/mixins/on-mixin.d.ts +15 -4
- package/dist/runtime/mixins/on-mixin.js +9 -2
- package/dist/runtime/mixins/on-mixin.js.map +1 -1
- package/dist/runtime/mixins/ref-mixin.d.ts +2 -1
- package/dist/runtime/mixins/ref-mixin.js.map +1 -1
- package/dist/runtime/navigation.js +9 -1
- package/dist/runtime/navigation.js.map +1 -1
- package/dist/runtime/reconcile.d.ts +1 -1
- package/dist/runtime/reconcile.js +452 -197
- package/dist/runtime/reconcile.js.map +1 -1
- package/dist/runtime/run.d.ts +17 -2
- package/dist/runtime/run.js.map +1 -1
- package/dist/runtime/scheduler.d.ts +10 -10
- package/dist/runtime/scheduler.js +1 -1
- package/dist/runtime/scheduler.js.map +1 -1
- package/dist/runtime/stream-protocol.d.ts +8 -0
- package/dist/runtime/stream-protocol.js +20 -0
- package/dist/runtime/stream-protocol.js.map +1 -0
- package/dist/runtime/svg-attributes.js +1 -0
- package/dist/runtime/svg-attributes.js.map +1 -1
- package/dist/runtime/to-vnode.js +7 -11
- package/dist/runtime/to-vnode.js.map +1 -1
- package/dist/runtime/vnode.d.ts +11 -5
- package/dist/runtime/vnode.js +6 -3
- package/dist/runtime/vnode.js.map +1 -1
- package/dist/server/stream.d.ts +2 -0
- package/dist/server/stream.js +154 -114
- package/dist/server/stream.js.map +1 -1
- package/dist/style/css-mixin.d.ts +2 -1
- package/dist/style/css-mixin.js.map +1 -1
- package/dist/style/index.d.ts +1 -1
- package/dist/style/layers.d.ts +2 -0
- package/dist/style/layers.js +3 -0
- package/dist/style/layers.js.map +1 -0
- package/dist/style/stylesheet.d.ts +13 -11
- package/dist/style/stylesheet.js +69 -8
- package/dist/style/stylesheet.js.map +1 -1
- package/dist/theme/contract.d.ts +24 -222
- package/dist/theme/contract.js +2 -2
- package/dist/theme/contract.js.map +1 -1
- package/dist/theme/glyph-contract.d.ts +1 -3
- package/dist/theme/glyph-contract.js.map +1 -1
- package/dist/theme/presets/rmx-01/index.d.ts +4 -2
- package/dist/theme/presets/rmx-01/index.js.map +1 -1
- package/dist/theme/runtime.js +4 -1
- package/dist/theme/runtime.js.map +1 -1
- package/dist/utils/scroll-lock.d.ts +3 -3
- package/dist/utils/scroll-lock.js.map +1 -1
- package/package.json +3 -3
- package/src/animation/README.md +316 -0
- package/src/animation/animate-layout-mixin.ts +79 -71
- package/src/animation/spring.ts +21 -9
- package/src/components/accordion/README.md +166 -0
- package/src/components/accordion/accordion.tsx +14 -14
- package/src/components/accordion/demos/card.demo.tsx +113 -0
- package/src/components/accordion/demos/multiple.demo.tsx +56 -0
- package/src/components/accordion/demos/overview.demo.tsx +56 -0
- package/src/components/anchor/README.md +153 -0
- package/src/components/anchor/anchor.ts +1 -1
- package/src/components/breadcrumbs/README.md +55 -0
- package/src/components/breadcrumbs/breadcrumbs.tsx +1 -3
- package/src/components/breadcrumbs/demos/basic.demo.tsx +17 -0
- package/src/components/breadcrumbs/demos/separator.demo.tsx +18 -0
- package/src/components/button/README.md +4 -4
- package/src/components/button/button.tsx +118 -20
- package/src/components/button/demos/aliases.demo.tsx +27 -0
- package/src/components/button/demos/basic.demo.tsx +30 -0
- package/src/components/button/demos/states.demo.tsx +52 -0
- package/src/components/combobox/README.md +1 -1
- package/src/components/combobox/combobox.tsx +37 -21
- package/src/components/combobox/demos/overview.demo.tsx +199 -0
- package/src/components/glyph/README.md +72 -0
- package/src/components/glyph/glyph.tsx +2 -2
- package/src/components/listbox/README.md +115 -0
- package/src/components/listbox/demos/overview.demo.tsx +101 -0
- package/src/components/listbox/listbox.ts +106 -110
- package/src/components/menu/README.md +96 -0
- package/src/components/menu/demos/bubbling.demo.tsx +34 -0
- package/src/components/menu/demos/overview.demo.tsx +76 -0
- package/src/components/menu/menu.tsx +61 -55
- package/src/components/popover/README.md +3 -3
- package/src/components/popover/demos/overview.demo.tsx +143 -0
- package/src/components/popover/popover.ts +90 -79
- package/src/components/select/README.md +107 -0
- package/src/components/select/demos/deconstructed.demo.tsx +95 -0
- package/src/components/select/demos/overview.demo.tsx +51 -0
- package/src/components/select/select.tsx +53 -36
- package/src/components/tabs/tabs.tsx +10 -5
- package/src/index.ts +2 -1
- package/src/runtime/component.ts +122 -88
- package/src/runtime/core/attributes.ts +118 -0
- package/src/runtime/core/children.ts +42 -0
- package/src/runtime/core/props.ts +187 -0
- package/src/runtime/core/vnode.ts +48 -0
- package/src/runtime/create-element.ts +2 -1
- package/src/runtime/diff-dom.ts +59 -12
- package/src/runtime/diff-props.ts +2 -187
- package/src/runtime/dom.ts +20 -9
- package/src/runtime/frame.ts +71 -55
- package/src/runtime/jsx.ts +3 -30
- package/src/runtime/mixins/link-mixin.ts +88 -86
- package/src/runtime/mixins/mixin.ts +18 -5
- package/src/runtime/mixins/on-mixin.ts +20 -12
- package/src/runtime/mixins/ref-mixin.ts +4 -5
- package/src/runtime/navigation.ts +10 -1
- package/src/runtime/reconcile.ts +563 -223
- package/src/runtime/run.ts +17 -1
- package/src/runtime/scheduler.ts +20 -20
- package/src/runtime/stream-protocol.ts +27 -0
- package/src/runtime/svg-attributes.ts +1 -0
- package/src/runtime/to-vnode.ts +7 -10
- package/src/runtime/vnode.ts +16 -9
- package/src/server/README.md +90 -0
- package/src/server/stream.ts +176 -124
- package/src/style/css-mixin.ts +6 -1
- package/src/style/index.ts +1 -2
- package/src/style/layers.ts +2 -0
- package/src/style/stylesheet.ts +88 -11
- package/src/test/README.md +107 -0
- package/src/test/utils.ts +3 -1
- package/src/theme/README.md +103 -0
- package/src/theme/contract.ts +37 -5
- package/src/theme/glyph-contract.ts +3 -1
- package/src/theme/presets/rmx-01/index.ts +4 -2
- package/src/theme/runtime.ts +4 -1
- package/src/utils/scroll-lock/README.md +33 -0
- package/src/utils/scroll-lock.ts +7 -3
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
# animation
|
|
2
|
+
|
|
3
|
+
`animation` provides small primitives for entrance, exit, layout, spring, and tween animation. Use these helpers with Remix UI mixins, CSS transitions, the Web Animations API, and imperative `requestAnimationFrame` loops.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { animateEntrance, animateExit, animateLayout, spring } from 'remix/ui/animation'
|
|
9
|
+
|
|
10
|
+
let panelTransition = spring.transition(['opacity', 'transform'], 'snappy')
|
|
11
|
+
|
|
12
|
+
function Panel() {
|
|
13
|
+
return () => (
|
|
14
|
+
<div
|
|
15
|
+
style={{ transition: panelTransition }}
|
|
16
|
+
mix={[
|
|
17
|
+
animateEntrance({ opacity: 0, duration: 120 }),
|
|
18
|
+
animateExit({ opacity: 0, duration: 120 }),
|
|
19
|
+
animateLayout(),
|
|
20
|
+
]}
|
|
21
|
+
>
|
|
22
|
+
Saved filters
|
|
23
|
+
</div>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Entrance And Exit
|
|
29
|
+
|
|
30
|
+
`animateEntrance` animates an element from the provided keyframe into its natural styles when the element is inserted.
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { animateEntrance, spring } from 'remix/ui/animation'
|
|
34
|
+
|
|
35
|
+
function Toast() {
|
|
36
|
+
return () => (
|
|
37
|
+
<div
|
|
38
|
+
mix={[
|
|
39
|
+
animateEntrance({
|
|
40
|
+
opacity: 0,
|
|
41
|
+
transform: 'translateY(8px)',
|
|
42
|
+
...spring('snappy'),
|
|
43
|
+
}),
|
|
44
|
+
]}
|
|
45
|
+
>
|
|
46
|
+
Saved
|
|
47
|
+
</div>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`animateExit` keeps a removed keyed element in the DOM long enough to animate from its natural styles to the provided keyframe.
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import type { Handle } from 'remix/ui'
|
|
56
|
+
import { animateExit } from 'remix/ui/animation'
|
|
57
|
+
|
|
58
|
+
function Item(handle: Handle<{ id: string; label: string }>) {
|
|
59
|
+
return () => (
|
|
60
|
+
<li
|
|
61
|
+
key={handle.props.id}
|
|
62
|
+
mix={[
|
|
63
|
+
animateExit({
|
|
64
|
+
opacity: 0,
|
|
65
|
+
transform: 'scale(0.96)',
|
|
66
|
+
duration: 120,
|
|
67
|
+
easing: 'ease-in',
|
|
68
|
+
}),
|
|
69
|
+
]}
|
|
70
|
+
>
|
|
71
|
+
{handle.props.label}
|
|
72
|
+
</li>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Passing `true` uses the default opacity animation. Passing `false`, `null`, or `undefined` disables the animation.
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
<div mix={[animateEntrance(true), animateExit(false)]} />
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Animation configs combine WAAPI timing options with style properties for the animated keyframe.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
type AnimateMixinConfig = {
|
|
87
|
+
duration: number
|
|
88
|
+
easing?: string
|
|
89
|
+
delay?: number
|
|
90
|
+
composite?: CompositeOperation
|
|
91
|
+
initial?: boolean
|
|
92
|
+
[property: string]: unknown
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Pass `initial: false` to skip only the first keyed entrance for an element within a parent. Later insertions for the same key can still animate.
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
<div key={id} mix={[animateEntrance({ opacity: 0, duration: 150, initial: false })]} />
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Exit animations can reclaim a removed keyed node if the same keyed element is rendered again before the exit finishes. The reclaimed node retargets toward its natural styles instead of simply reversing the exit animation.
|
|
103
|
+
|
|
104
|
+
## Layout Animation
|
|
105
|
+
|
|
106
|
+
`animateLayout` animates layout changes with a FLIP-style transform projection. Use it on elements whose position or size can change between renders.
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
import type { Handle } from 'remix/ui'
|
|
110
|
+
import { animateLayout, spring } from 'remix/ui/animation'
|
|
111
|
+
|
|
112
|
+
function Card(handle: Handle<{ expanded: boolean }>) {
|
|
113
|
+
return () => (
|
|
114
|
+
<section
|
|
115
|
+
class={handle.props.expanded ? 'card expanded' : 'card'}
|
|
116
|
+
mix={[
|
|
117
|
+
animateLayout({
|
|
118
|
+
...spring('smooth'),
|
|
119
|
+
}),
|
|
120
|
+
]}
|
|
121
|
+
>
|
|
122
|
+
Details
|
|
123
|
+
</section>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Pass `size: false` when the element should animate position only and avoid scale projection.
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
<div mix={[animateLayout({ duration: 180, easing: 'ease-out', size: false })]} />
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Passing `true` or no argument enables the default layout animation. Passing `false`, `null`, or `undefined` disables it. Layout animation skips work when geometry does not change, keeps in-flight animations running when their target geometry has not changed, and continues from the current visual transform when a new layout change interrupts an active animation.
|
|
135
|
+
|
|
136
|
+
## Spring
|
|
137
|
+
|
|
138
|
+
`spring` returns a decorated iterator. It can be iterated for JavaScript animation, spread into Web Animations API options, or stringified for CSS transition syntax.
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
import { spring } from 'remix/ui/animation'
|
|
142
|
+
|
|
143
|
+
spring('bouncy')
|
|
144
|
+
spring('snappy')
|
|
145
|
+
spring('smooth')
|
|
146
|
+
spring({ duration: 400, bounce: 0.3 })
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
interface SpringIterator extends IterableIterator<number> {
|
|
151
|
+
duration: number
|
|
152
|
+
easing: string
|
|
153
|
+
toString(): string
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Use `spring.transition` to build CSS transition entries.
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
let transition = spring.transition(['opacity', 'transform'], 'bouncy')
|
|
161
|
+
|
|
162
|
+
function Button() {
|
|
163
|
+
return () => <button style={{ transition }}>Save</button>
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Spread a spring into animation mixin or WAAPI options.
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
<div
|
|
171
|
+
mix={[
|
|
172
|
+
animateEntrance({
|
|
173
|
+
opacity: 0,
|
|
174
|
+
transform: 'scale(0.92)',
|
|
175
|
+
...spring('bouncy'),
|
|
176
|
+
}),
|
|
177
|
+
]}
|
|
178
|
+
/>
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
element.animate(
|
|
183
|
+
[
|
|
184
|
+
{ opacity: 0, transform: 'scale(0.92)' },
|
|
185
|
+
{ opacity: 1, transform: 'scale(1)' },
|
|
186
|
+
],
|
|
187
|
+
{ ...spring('snappy') },
|
|
188
|
+
)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Use the iterator values as progress from `0` to `1` for imperative animation.
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
let from = 0
|
|
195
|
+
let to = 200
|
|
196
|
+
|
|
197
|
+
for (let progress of spring('bouncy')) {
|
|
198
|
+
let x = from + (to - from) * progress
|
|
199
|
+
element.style.transform = `translateX(${x}px)`
|
|
200
|
+
await nextFrame()
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The built-in presets are:
|
|
205
|
+
|
|
206
|
+
| Preset | Bounce | Duration | Character |
|
|
207
|
+
| -------- | ------ | -------- | ------------------------ |
|
|
208
|
+
| `smooth` | -0.3 | 400ms | Overdamped, no overshoot |
|
|
209
|
+
| `snappy` | 0 | 200ms | Quick, no overshoot |
|
|
210
|
+
| `bouncy` | 0.3 | 400ms | Underdamped bounce |
|
|
211
|
+
|
|
212
|
+
Override preset duration or velocity with the second argument.
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
spring('bouncy', { duration: 300 })
|
|
216
|
+
spring('snappy', { velocity: 2 })
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Use explicit options when you need full control.
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
spring({
|
|
223
|
+
duration: 500,
|
|
224
|
+
bounce: 0.35,
|
|
225
|
+
velocity: 0,
|
|
226
|
+
})
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Tween
|
|
230
|
+
|
|
231
|
+
`tween` creates a generator that interpolates a numeric value over time with a cubic-bezier curve. Call `next()` once to initialize the generator, then pass `requestAnimationFrame` timestamps into `next(timestamp)`.
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
import { easings, tween } from 'remix/ui/animation'
|
|
235
|
+
|
|
236
|
+
let animation = tween({
|
|
237
|
+
from: 0,
|
|
238
|
+
to: 100,
|
|
239
|
+
duration: 300,
|
|
240
|
+
curve: easings.easeOut,
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
animation.next()
|
|
244
|
+
|
|
245
|
+
function tick(timestamp: number) {
|
|
246
|
+
let { value, done } = animation.next(timestamp)
|
|
247
|
+
element.style.transform = `translateX(${value}px)`
|
|
248
|
+
if (!done) requestAnimationFrame(tick)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
requestAnimationFrame(tick)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Animate multiple values with separate tweens.
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
let xAnimation = tween({ from: 0, to: 100, duration: 500, curve: easings.easeOut })
|
|
258
|
+
let scaleAnimation = tween({ from: 1, to: 1.2, duration: 500, curve: easings.easeOut })
|
|
259
|
+
|
|
260
|
+
xAnimation.next()
|
|
261
|
+
scaleAnimation.next()
|
|
262
|
+
|
|
263
|
+
function tick(timestamp: number) {
|
|
264
|
+
let x = xAnimation.next(timestamp)
|
|
265
|
+
let scale = scaleAnimation.next(timestamp)
|
|
266
|
+
|
|
267
|
+
element.style.transform = `translateX(${x.value}px) scale(${scale.value})`
|
|
268
|
+
|
|
269
|
+
if (!x.done || !scale.done) {
|
|
270
|
+
requestAnimationFrame(tick)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The built-in easing presets are cubic-bezier control points matching common CSS timing functions.
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
easings.linear
|
|
279
|
+
easings.ease
|
|
280
|
+
easings.easeIn
|
|
281
|
+
easings.easeOut
|
|
282
|
+
easings.easeInOut
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Custom curves use the same control points as CSS `cubic-bezier(x1, y1, x2, y2)`.
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
let animation = tween({
|
|
289
|
+
from: 0,
|
|
290
|
+
to: 100,
|
|
291
|
+
duration: 500,
|
|
292
|
+
curve: { x1: 0.68, y1: -0.55, x2: 0.265, y2: 1.55 },
|
|
293
|
+
})
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## API
|
|
297
|
+
|
|
298
|
+
- `animateEntrance(config?)`: mixin that animates an element when it enters the DOM.
|
|
299
|
+
- `animateExit(config?)`: mixin that persists a removed keyed element long enough to run its exit animation.
|
|
300
|
+
- `animateLayout(config?)`: mixin that animates layout changes by comparing geometry between renders.
|
|
301
|
+
- `spring(preset?, overrides?)`: creates a `SpringIterator` from a named preset.
|
|
302
|
+
- `spring(options?)`: creates a `SpringIterator` from explicit spring options.
|
|
303
|
+
- `spring.transition(property, presetOrOptions?, overrides?)`: builds one or more CSS transition entries from a spring.
|
|
304
|
+
- `spring.presets`: named `smooth`, `snappy`, and `bouncy` spring defaults.
|
|
305
|
+
- `tween(options)`: generator that interpolates numeric values over time with a cubic-bezier curve.
|
|
306
|
+
- `easings`: common cubic-bezier presets for `tween`.
|
|
307
|
+
- `SpringIterator`, `SpringPreset`, `SpringOptions`, `TweenOptions`, and `BezierCurve`: public TypeScript types for spring and tween configuration.
|
|
308
|
+
|
|
309
|
+
## Behavior Notes
|
|
310
|
+
|
|
311
|
+
- Animation mixin style properties are copied into WAAPI keyframes; `duration`, `easing`, `delay`, `composite`, and `initial` are treated as options.
|
|
312
|
+
- `animateEntrance({ initial: false })` only skips the first keyed entrance tracked for the parent node.
|
|
313
|
+
- `animateExit` needs keyed elements when removed nodes may be reclaimed or persisted across list updates.
|
|
314
|
+
- `animateLayout({ size: false })` animates translation without scale projection.
|
|
315
|
+
- `spring()` yields progress values from `0` to `1`; its `duration` and `easing` properties are enumerable so `{ ...spring() }` works with WAAPI options.
|
|
316
|
+
- `tween(...)` yields the initial value first; advance the generator with frame timestamps via `next(timestamp)` and read `done` to detect completion.
|
|
@@ -60,30 +60,6 @@ function calcBoxDelta(delta: Delta, source: Box, target: Box, layoutConfig: Layo
|
|
|
60
60
|
calcAxisDelta(delta.y, source.y, target.y, origin)
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
function mixAxisDelta(output: AxisDelta, delta: AxisDelta, progress: number): void {
|
|
64
|
-
output.translate = mix(delta.translate, 0, progress)
|
|
65
|
-
output.scale = mix(delta.scale, 1, progress)
|
|
66
|
-
output.origin = delta.origin
|
|
67
|
-
output.originPoint = delta.originPoint
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function mixDelta(output: Delta, delta: Delta, progress: number): void {
|
|
71
|
-
mixAxisDelta(output.x, delta.x, progress)
|
|
72
|
-
mixAxisDelta(output.y, delta.y, progress)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function copyAxisDeltaInto(target: AxisDelta, source: AxisDelta): void {
|
|
76
|
-
target.translate = source.translate
|
|
77
|
-
target.scale = source.scale
|
|
78
|
-
target.origin = source.origin
|
|
79
|
-
target.originPoint = source.originPoint
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function copyDeltaInto(target: Delta, source: Delta): void {
|
|
83
|
-
copyAxisDeltaInto(target.x, source.x)
|
|
84
|
-
copyAxisDeltaInto(target.y, source.y)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
63
|
function buildProjectionTransform(delta: Delta, layoutConfig: LayoutAnimationConfig): string {
|
|
88
64
|
let transform = ''
|
|
89
65
|
if (delta.x.translate || delta.y.translate) {
|
|
@@ -133,23 +109,51 @@ function isVisualDeltaZero(delta: Delta, layoutConfig: LayoutAnimationConfig): b
|
|
|
133
109
|
)
|
|
134
110
|
}
|
|
135
111
|
|
|
112
|
+
function isTargetBoxSame(source: Box, target: Box, layoutConfig: LayoutAnimationConfig): boolean {
|
|
113
|
+
return (
|
|
114
|
+
isNear(source.x.min, target.x.min, TRANSLATE_PRECISION) &&
|
|
115
|
+
isNear(source.y.min, target.y.min, TRANSLATE_PRECISION) &&
|
|
116
|
+
(layoutConfig.size === false ||
|
|
117
|
+
(isNear(source.x.max, target.x.max, TRANSLATE_PRECISION) &&
|
|
118
|
+
isNear(source.y.max, target.y.max, TRANSLATE_PRECISION)))
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function measureAnimationTargetBox(
|
|
123
|
+
node: HTMLElement,
|
|
124
|
+
animation: Animation,
|
|
125
|
+
animationEndTime: number,
|
|
126
|
+
): Box {
|
|
127
|
+
let currentTime = animation.currentTime
|
|
128
|
+
let wasRunning = animation.playState === 'running'
|
|
129
|
+
|
|
130
|
+
animation.currentTime = animationEndTime
|
|
131
|
+
let box = measureNaturalBox(node)
|
|
132
|
+
animation.currentTime = currentTime
|
|
133
|
+
|
|
134
|
+
if (wasRunning && animation.playState !== 'running') {
|
|
135
|
+
animation.play()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return box
|
|
139
|
+
}
|
|
140
|
+
|
|
136
141
|
const animateLayoutMixin = createMixin<Element, [config?: LayoutConfig], ElementProps>((handle) => {
|
|
137
142
|
let snapshot: Box | null = null
|
|
138
143
|
let currentConfig: LayoutConfig = true
|
|
139
|
-
let currentDelta: Delta | null = null
|
|
140
|
-
let animationProgress = 0
|
|
141
144
|
let animation: Animation | null = null
|
|
145
|
+
let animationTarget: Box | null = null
|
|
146
|
+
let animationEndTime = 0
|
|
142
147
|
|
|
143
|
-
let
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
requestAnimationFrame(tick)
|
|
148
|
+
let clearActiveAnimationState = () => {
|
|
149
|
+
animation = null
|
|
150
|
+
animationTarget = null
|
|
151
|
+
animationEndTime = 0
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let clearLayoutState = () => {
|
|
155
|
+
clearActiveAnimationState()
|
|
156
|
+
snapshot = null
|
|
153
157
|
}
|
|
154
158
|
|
|
155
159
|
let clearProjectionStyles = (node: HTMLElement) => {
|
|
@@ -157,33 +161,48 @@ const animateLayoutMixin = createMixin<Element, [config?: LayoutConfig], Element
|
|
|
157
161
|
node.style.transformOrigin = ''
|
|
158
162
|
}
|
|
159
163
|
|
|
160
|
-
let resetAnimation = () => {
|
|
161
|
-
animation = null
|
|
162
|
-
currentDelta = null
|
|
163
|
-
animationProgress = 0
|
|
164
|
-
}
|
|
165
|
-
|
|
166
164
|
handle.addEventListener('beforeUpdate', (event) => {
|
|
167
165
|
let layoutConfig = resolveLayoutConfig(currentConfig)
|
|
168
166
|
if (!layoutConfig) return
|
|
169
|
-
|
|
167
|
+
let htmlNode = event.node as HTMLElement
|
|
168
|
+
// Capture the live on-screen position so the next animation can pick up
|
|
169
|
+
// where this one was interrupted if this update changes the target box.
|
|
170
|
+
if (animation && animation.playState === 'running') {
|
|
171
|
+
snapshot = rectToBox(htmlNode.getBoundingClientRect())
|
|
172
|
+
} else {
|
|
173
|
+
snapshot = measureNaturalBox(htmlNode)
|
|
174
|
+
}
|
|
170
175
|
})
|
|
171
176
|
|
|
172
177
|
handle.addEventListener('commit', (event) => {
|
|
173
178
|
let layoutConfig = resolveLayoutConfig(currentConfig)
|
|
174
179
|
let htmlNode = event.node as HTMLElement
|
|
175
|
-
let
|
|
180
|
+
let runningAnimation = animation?.playState === 'running' ? animation : null
|
|
181
|
+
let latest: Box | null = null
|
|
182
|
+
|
|
183
|
+
if (runningAnimation && animationTarget && layoutConfig) {
|
|
184
|
+
latest = measureAnimationTargetBox(htmlNode, runningAnimation, animationEndTime)
|
|
185
|
+
if (isTargetBoxSame(latest, animationTarget, layoutConfig)) {
|
|
186
|
+
snapshot = null
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Defensive cleanup for cases where beforeUpdate didn't run (e.g. the
|
|
192
|
+
// mixin was just attached, layoutConfig was disabled this render, or an
|
|
193
|
+
// in-flight animation needs to retarget to a new final box).
|
|
194
|
+
animation?.cancel()
|
|
195
|
+
clearActiveAnimationState()
|
|
196
|
+
clearProjectionStyles(htmlNode)
|
|
197
|
+
latest ??= measureNaturalBox(htmlNode)
|
|
176
198
|
|
|
177
199
|
if (!layoutConfig) {
|
|
178
|
-
|
|
179
|
-
clearProjectionStyles(htmlNode)
|
|
180
|
-
resetAnimation()
|
|
181
|
-
snapshot = latest
|
|
200
|
+
clearLayoutState()
|
|
182
201
|
return
|
|
183
202
|
}
|
|
184
203
|
|
|
185
204
|
if (!snapshot) {
|
|
186
|
-
snapshot =
|
|
205
|
+
snapshot = null
|
|
187
206
|
return
|
|
188
207
|
}
|
|
189
208
|
|
|
@@ -191,26 +210,10 @@ const animateLayoutMixin = createMixin<Element, [config?: LayoutConfig], Element
|
|
|
191
210
|
calcBoxDelta(targetDelta, latest, snapshot, layoutConfig)
|
|
192
211
|
|
|
193
212
|
if (isVisualDeltaZero(targetDelta, layoutConfig)) {
|
|
194
|
-
snapshot =
|
|
213
|
+
snapshot = null
|
|
195
214
|
return
|
|
196
215
|
}
|
|
197
216
|
|
|
198
|
-
if (animation && animation.playState === 'running') {
|
|
199
|
-
animation.cancel()
|
|
200
|
-
if (currentDelta && animationProgress > 0 && animationProgress < 1) {
|
|
201
|
-
let visual = createDelta()
|
|
202
|
-
mixDelta(visual, currentDelta, animationProgress)
|
|
203
|
-
targetDelta.x.translate += visual.x.translate
|
|
204
|
-
targetDelta.y.translate += visual.y.translate
|
|
205
|
-
targetDelta.x.scale *= visual.x.scale
|
|
206
|
-
targetDelta.y.scale *= visual.y.scale
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (!currentDelta) currentDelta = createDelta()
|
|
211
|
-
copyDeltaInto(currentDelta, targetDelta)
|
|
212
|
-
animationProgress = 0
|
|
213
|
-
|
|
214
217
|
let invert = buildProjectionTransform(targetDelta, layoutConfig)
|
|
215
218
|
let origin = buildTransformOrigin(targetDelta)
|
|
216
219
|
htmlNode.style.transform = invert
|
|
@@ -226,21 +229,26 @@ const animateLayoutMixin = createMixin<Element, [config?: LayoutConfig], Element
|
|
|
226
229
|
{ duration, easing, fill: 'forwards' },
|
|
227
230
|
)
|
|
228
231
|
animation = active
|
|
229
|
-
|
|
232
|
+
animationTarget = latest
|
|
233
|
+
animationEndTime = duration
|
|
234
|
+
snapshot = null
|
|
230
235
|
active.finished
|
|
231
236
|
.then(() => {
|
|
232
237
|
if (animation !== active) return
|
|
238
|
+
// Cancel even though the animation finished naturally. A fill:forwards
|
|
239
|
+
// animation in 'finished' state remains an effective animation on
|
|
240
|
+
// transform, which makes commitStyles() throw on the next interrupted
|
|
241
|
+
// animation because more than one effect targets the same property.
|
|
242
|
+
active.cancel()
|
|
233
243
|
clearProjectionStyles(htmlNode)
|
|
234
|
-
|
|
235
|
-
snapshot = rectToBox(htmlNode.getBoundingClientRect())
|
|
244
|
+
clearLayoutState()
|
|
236
245
|
})
|
|
237
246
|
.catch(() => {})
|
|
238
247
|
})
|
|
239
248
|
|
|
240
249
|
handle.addEventListener('remove', () => {
|
|
241
250
|
animation?.cancel()
|
|
242
|
-
|
|
243
|
-
snapshot = null
|
|
251
|
+
clearLayoutState()
|
|
244
252
|
})
|
|
245
253
|
|
|
246
254
|
return (config = true) => {
|
package/src/animation/spring.ts
CHANGED
|
@@ -38,6 +38,17 @@ export interface SpringIterator extends IterableIterator<number> {
|
|
|
38
38
|
toString(): string
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
export interface SpringFunction {
|
|
42
|
+
(preset: SpringPreset, overrides?: Omit<SpringOptions, 'bounce'>): SpringIterator
|
|
43
|
+
(options?: SpringOptions): SpringIterator
|
|
44
|
+
transition(
|
|
45
|
+
property: string | string[],
|
|
46
|
+
presetOrOptions?: SpringPreset | SpringOptions,
|
|
47
|
+
overrides?: Omit<SpringOptions, 'bounce'>,
|
|
48
|
+
): string
|
|
49
|
+
presets: Record<SpringPreset, { duration: number; bounce: number }>
|
|
50
|
+
}
|
|
51
|
+
|
|
41
52
|
const presets: Record<SpringPreset, { duration: number; bounce: number }> = {
|
|
42
53
|
smooth: { duration: 400, bounce: -0.3 },
|
|
43
54
|
snappy: { duration: 200, bounce: 0 },
|
|
@@ -74,7 +85,7 @@ const frameMs = 1000 / 60 // ~16.67ms per frame
|
|
|
74
85
|
* @param overrides Optional preset overrides.
|
|
75
86
|
* @returns A spring iterator.
|
|
76
87
|
*/
|
|
77
|
-
|
|
88
|
+
function createSpring(
|
|
78
89
|
preset: SpringPreset,
|
|
79
90
|
overrides?: Omit<SpringOptions, 'bounce'>,
|
|
80
91
|
): SpringIterator
|
|
@@ -84,8 +95,8 @@ export function spring(
|
|
|
84
95
|
* @param options Spring parameters.
|
|
85
96
|
* @returns A spring iterator.
|
|
86
97
|
*/
|
|
87
|
-
|
|
88
|
-
|
|
98
|
+
function createSpring(options?: SpringOptions): SpringIterator
|
|
99
|
+
function createSpring(
|
|
89
100
|
presetOrOptions?: SpringPreset | SpringOptions,
|
|
90
101
|
overrides?: Omit<SpringOptions, 'bounce'>,
|
|
91
102
|
): SpringIterator {
|
|
@@ -119,23 +130,24 @@ export function spring(
|
|
|
119
130
|
return iter as unknown as SpringIterator
|
|
120
131
|
}
|
|
121
132
|
|
|
122
|
-
|
|
123
|
-
spring.transition = function transition(
|
|
133
|
+
function transition(
|
|
124
134
|
property: string | string[],
|
|
125
135
|
presetOrOptions?: SpringPreset | SpringOptions,
|
|
126
136
|
overrides?: Omit<SpringOptions, 'bounce'>,
|
|
127
137
|
): string {
|
|
128
138
|
let s =
|
|
129
139
|
typeof presetOrOptions === 'string'
|
|
130
|
-
?
|
|
131
|
-
:
|
|
140
|
+
? createSpring(presetOrOptions, overrides)
|
|
141
|
+
: createSpring(presetOrOptions)
|
|
132
142
|
|
|
133
143
|
let properties = Array.isArray(property) ? property : [property]
|
|
134
144
|
return properties.map((p) => `${p} ${s}`).join(', ')
|
|
135
145
|
}
|
|
136
146
|
|
|
137
|
-
|
|
138
|
-
|
|
147
|
+
export const spring: SpringFunction = Object.assign(createSpring, {
|
|
148
|
+
transition,
|
|
149
|
+
presets,
|
|
150
|
+
})
|
|
139
151
|
|
|
140
152
|
function resolveOptions(
|
|
141
153
|
presetOrOptions?: SpringPreset | SpringOptions,
|