|  | 
|  | 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 | +}); | 
0 commit comments