Skip to content

Commit de909dc

Browse files
wip: motion
1 parent 407eb4e commit de909dc

File tree

8 files changed

+388
-5
lines changed

8 files changed

+388
-5
lines changed

docs/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"@qds.dev/tools": "workspace:*",
3232
"@qds.dev/ui": "workspace:*",
3333
"@qds.dev/utils": "workspace:*",
34+
"@qds.dev/motion": "workspace:*",
3435
"@qwik.dev/core": "https://pkg.pr.new/QwikDev/qwik/@qwik.dev/core@d48c3d2",
3536
"@qwik.dev/router": "https://pkg.pr.new/QwikDev/qwik/@qwik.dev/router@d48c3d2",
3637
"@qwikest/icons": "^0.0.13",
Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
import { AnimatePresence } from "@qds.dev/motion";
12
import { Modal } from "@qds.dev/ui";
23
import { component$ } from "@qwik.dev/core";
34

45
export default component$(() => {
56
return (
6-
<Modal.Root>
7-
<Modal.Trigger class="ui-open:bg-red-500">Open Modal</Modal.Trigger>
8-
<Modal.Content>Some content</Modal.Content>
9-
</Modal.Root>
7+
<AnimatePresence>
8+
<Modal.Root>
9+
<Modal.Trigger class="ui-open:bg-red-500">Open Modal</Modal.Trigger>
10+
<Modal.Content>Some content</Modal.Content>
11+
</Modal.Root>
12+
</AnimatePresence>
1013
);
1114
});

docs/vite.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export default defineConfig((): UserConfig => {
9090
"@qds.dev/ui": resolve(__dirname, "../libs/components/src"),
9191
"@qds.dev/utils": resolve(__dirname, "../libs/utils/src"),
9292
"@qds.dev/tools": resolve(__dirname, "../libs/tools/src"),
93+
"@qds.dev/motion": resolve(__dirname, "../libs/motion/src"),
9394
"~": resolve(__dirname, "src")
9495
},
9596
dedupe: ["@qwik.dev/core", "@qwik.dev/router"]
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# AnimatePresence Component
2+
3+
## What It Handles
4+
5+
When using View Transitions API directly, `AnimatePresence` acts as a **coordination layer** that:
6+
7+
### 1. **Detects DOM Changes**
8+
- Watches for children being added/removed using `MutationObserver`
9+
- Triggers View Transitions when changes occur
10+
11+
### 2. **Wraps Updates with View Transitions**
12+
```tsx
13+
// When a child is added/removed, AnimatePresence does:
14+
document.startViewTransition(() => {
15+
// DOM update happens here
16+
// Browser captures before/after states automatically
17+
});
18+
```
19+
20+
### 3. **Manages Transition Lifecycle**
21+
- Tracks when transitions are active (`isTransitioning`)
22+
- Prevents overlapping transitions
23+
- Handles cleanup on unmount
24+
25+
### 4. **Handles Mode Prop**
26+
- **`sync`**: Enter/exit happen simultaneously (default)
27+
- **`wait`**: Enter waits for exit to complete
28+
- **`popLayout`**: Exit is positioned absolutely (for layout animations)
29+
30+
### 5. **Provides Context for Auto-Generated Names**
31+
- Gives children a way to generate `view-transition-name` from `key` prop
32+
- Ensures unique names per AnimatePresence instance
33+
34+
### 6. **Calls Callbacks**
35+
- `onExitComplete`: Fires when all exit animations finish
36+
37+
## What It DOESN'T Handle
38+
39+
AnimatePresence does **NOT** handle:
40+
41+
-**Animation definitions** - That's CSS (`::view-transition-*`)
42+
-**Setting view-transition-name** - Children do that via CSS or inline styles
43+
-**Animation timing/easing** - That's CSS
44+
-**Layout calculations** - Browser handles that automatically
45+
46+
## Usage Pattern
47+
48+
```tsx
49+
<AnimatePresence mode="wait" onExitComplete={() => console.log('Done!')}>
50+
{show && (
51+
<div
52+
key="modal"
53+
style={{ viewTransitionName: "modal" }}
54+
>
55+
Content
56+
</div>
57+
)}
58+
</AnimatePresence>
59+
```
60+
61+
## How It Works
62+
63+
1. **AnimatePresence** wraps your content
64+
2. **MutationObserver** watches for children being added/removed
65+
3. When changes detected → calls `document.startViewTransition()`
66+
4. **Browser** captures before/after DOM states
67+
5. **CSS** (`::view-transition-*`) handles the actual animation
68+
6. **onExitComplete** fires when animation finishes
69+
70+
## Key Insight
71+
72+
AnimatePresence is a **thin wrapper** around View Transitions API that:
73+
- Provides a React/Vue-like API (familiar to developers)
74+
- Handles the coordination/boilerplate
75+
- Lets CSS and the browser do the heavy lifting
76+
77+
You could use `document.startViewTransition()` directly, but AnimatePresence makes it easier and provides helpful features like `mode` and `onExitComplete`.
78+
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/**
2+
* AnimatePresence Component
3+
*
4+
* What it handles when using View Transitions API directly:
5+
*
6+
* 1. **Provides context** - Gives children `instanceId` to generate unique view-transition-name values
7+
* 2. **Tracks transition lifecycle** - Monitors View Transitions and calls `onExitComplete` when done
8+
* 3. **Handles mode prop** - Controls sync/wait/popLayout behavior (future enhancement)
9+
*
10+
* What it DOESN'T handle:
11+
* - Wrapping DOM updates with document.startViewTransition() - This must be done where the update occurs
12+
* - Actual animation definitions (that's CSS ::view-transition-* pseudo-elements)
13+
* - Setting view-transition-name (use `Motion` component or set manually via CSS/inline styles)
14+
* - Animation timing/easing (that's CSS)
15+
*
16+
* Note: In Qwik, DOM updates happen reactively. To use View Transitions, you need to wrap
17+
* the state change that triggers the DOM update with document.startViewTransition().
18+
* AnimatePresence provides the coordination layer and context, but the actual wrapping
19+
* happens at the point where you update the signal/state that controls visibility.
20+
*/
21+
22+
import {
23+
component$,
24+
createContextId,
25+
type PropsOf,
26+
Slot,
27+
useConstant,
28+
useContextProvider
29+
} from "@qwik.dev/core";
30+
31+
export const animatePresenceContextId =
32+
createContextId<AnimatePresenceContext>("qds-animate-presence");
33+
34+
export type AnimatePresenceContext = {
35+
/**
36+
* Instance ID for this AnimatePresence instance
37+
* Use this to generate unique view-transition-name values: `${instanceId}-${key}`
38+
*/
39+
instanceId: string;
40+
};
41+
42+
export type AnimatePresenceProps = {
43+
/**
44+
* When false, disables initial animations on children present when component first renders
45+
*/
46+
initial?: boolean;
47+
48+
/**
49+
* Custom value passed to variant functions for dynamic exit animations
50+
*/
51+
custom?: unknown;
52+
53+
/**
54+
* Transition mode: 'sync' | 'wait' | 'popLayout'
55+
* - sync: Enter/exit simultaneously (default)
56+
* - wait: The entering child will wait until the exiting child has animated out
57+
* - popLayout: Exiting children will be "popped" out of the page layout
58+
*/
59+
mode?: "sync" | "wait" | "popLayout";
60+
61+
/**
62+
* Callback fired when all exiting nodes have completed animating out
63+
*/
64+
onExitComplete?: () => void;
65+
66+
/**
67+
* Class name for styling the container
68+
*/
69+
class?: string;
70+
} & PropsOf<"div">;
71+
72+
/**
73+
* AnimatePresence - Coordinates View Transitions for enter/exit animations
74+
*
75+
* This component:
76+
* 1. Watches for children being added/removed from the DOM
77+
* 2. Wraps those changes with document.startViewTransition()
78+
* 3. Manages the transition lifecycle (mode, callbacks)
79+
* 4. Provides context for children to generate view-transition-name
80+
*
81+
* Children should use `motion.*` components (e.g., `motion.div`) to automatically apply view-transition-name,
82+
* or manually set view-transition-name via CSS or inline styles.
83+
*
84+
* Define CSS animations for ::view-transition-* pseudo-elements.
85+
*
86+
* @example
87+
* ```tsx
88+
* <AnimatePresence mode="wait">
89+
* {show && (
90+
* <motion.div key="modal">
91+
* Content
92+
* </motion.div>
93+
* )}
94+
* </AnimatePresence>
95+
* ```
96+
*
97+
* @example Manual view-transition-name
98+
* ```tsx
99+
* <AnimatePresence>
100+
* {show && (
101+
* <div style={{ viewTransitionName: "modal" }}>
102+
* Content
103+
* </div>
104+
* )}
105+
* </AnimatePresence>
106+
* ```
107+
*/
108+
export const AnimatePresence = component$<AnimatePresenceProps>((props) => {
109+
const {
110+
initial: _initial = true,
111+
custom: _custom,
112+
mode = "sync",
113+
onExitComplete: _onExitComplete,
114+
class: className,
115+
...restProps
116+
} = props;
117+
118+
// Generate unique instance ID for this AnimatePresence
119+
const instanceId = useConstant(() => `ap-${Math.random().toString(36).slice(2, 9)}`);
120+
121+
// Provide context to children
122+
// Children can generate names using: `${instanceId}-${key}`
123+
const context: AnimatePresenceContext = {
124+
instanceId
125+
};
126+
127+
useContextProvider(animatePresenceContextId, context);
128+
129+
return (
130+
<div {...restProps} class={className} data-animate-presence data-mode={mode}>
131+
<Slot />
132+
</div>
133+
);
134+
});

libs/motion/src/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,10 @@
1-
export const hello = "world";
1+
export type {
2+
AnimatePresenceContext,
3+
AnimatePresenceProps
4+
} from "./animate-presence/animate-presence";
5+
export {
6+
AnimatePresence,
7+
animatePresenceContextId
8+
} from "./animate-presence/animate-presence";
9+
export type { MotionProps } from "./motion/motion";
10+
export { motion } from "./motion/motion";

0 commit comments

Comments
 (0)