@hanzogui/animate-presence 2.0.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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/LICENSE.md +9 -0
  3. package/README.md +1 -0
  4. package/dist/cjs/AnimatePresence.cjs +111 -0
  5. package/dist/cjs/AnimatePresence.native.js +129 -0
  6. package/dist/cjs/AnimatePresence.native.js.map +1 -0
  7. package/dist/cjs/LayoutGroupContext.cjs +38 -0
  8. package/dist/cjs/LayoutGroupContext.native.js +41 -0
  9. package/dist/cjs/LayoutGroupContext.native.js.map +1 -0
  10. package/dist/cjs/PresenceChild.cjs +88 -0
  11. package/dist/cjs/PresenceChild.native.js +116 -0
  12. package/dist/cjs/PresenceChild.native.js.map +1 -0
  13. package/dist/cjs/index.cjs +21 -0
  14. package/dist/cjs/index.native.js +24 -0
  15. package/dist/cjs/index.native.js.map +1 -0
  16. package/dist/cjs/types.cjs +16 -0
  17. package/dist/cjs/types.native.js +19 -0
  18. package/dist/cjs/types.native.js.map +1 -0
  19. package/dist/esm/AnimatePresence.mjs +88 -0
  20. package/dist/esm/AnimatePresence.mjs.map +1 -0
  21. package/dist/esm/AnimatePresence.native.js +103 -0
  22. package/dist/esm/AnimatePresence.native.js.map +1 -0
  23. package/dist/esm/LayoutGroupContext.mjs +4 -0
  24. package/dist/esm/LayoutGroupContext.mjs.map +1 -0
  25. package/dist/esm/LayoutGroupContext.native.js +4 -0
  26. package/dist/esm/LayoutGroupContext.native.js.map +1 -0
  27. package/dist/esm/PresenceChild.mjs +54 -0
  28. package/dist/esm/PresenceChild.mjs.map +1 -0
  29. package/dist/esm/PresenceChild.native.js +79 -0
  30. package/dist/esm/PresenceChild.native.js.map +1 -0
  31. package/dist/esm/index.js +5 -0
  32. package/dist/esm/index.js.map +1 -0
  33. package/dist/esm/index.mjs +5 -0
  34. package/dist/esm/index.mjs.map +1 -0
  35. package/dist/esm/index.native.js +5 -0
  36. package/dist/esm/index.native.js.map +1 -0
  37. package/dist/esm/types.mjs +2 -0
  38. package/dist/esm/types.mjs.map +1 -0
  39. package/dist/esm/types.native.js +2 -0
  40. package/dist/esm/types.native.js.map +1 -0
  41. package/package.json +55 -0
  42. package/src/AnimatePresence.tsx +219 -0
  43. package/src/LayoutGroupContext.tsx +8 -0
  44. package/src/PresenceChild.tsx +90 -0
  45. package/src/index.ts +4 -0
  46. package/src/types.ts +107 -0
  47. package/types/AnimatePresence.d.ts +4 -0
  48. package/types/LayoutGroupContext.d.ts +7 -0
  49. package/types/PresenceChild.d.ts +16 -0
  50. package/types/index.d.ts +5 -0
  51. package/types/types.d.ts +96 -0
@@ -0,0 +1,219 @@
1
+ import { useInsertionEffect } from 'react'
2
+ import { useConstant } from '@hanzogui/use-constant'
3
+ import { useForceUpdate } from '@hanzogui/use-force-update'
4
+ import type { FunctionComponent, PropsWithChildren, ReactElement, ReactNode } from 'react'
5
+ import { Children, isValidElement, useContext, useMemo, useRef, useState } from 'react'
6
+ import { LayoutGroupContext } from './LayoutGroupContext'
7
+ import { PresenceChild } from './PresenceChild'
8
+ import type { AnimatePresenceProps } from './types'
9
+
10
+ type ComponentKey = string | number
11
+
12
+ const getChildKey = (child: ReactElement<any>): ComponentKey => {
13
+ return (
14
+ child.key ||
15
+ (() => {
16
+ // we can help a bit by falling back to gui name or component name
17
+ const ct = child.type
18
+ const defaultName = ct['displayName'] || ct['name'] || ''
19
+ if (ct && typeof ct === 'object' && 'staticConfig' in ct) {
20
+ // @ts-expect-error
21
+ return ct.staticConfig.componentName || defaultName
22
+ }
23
+ return defaultName
24
+ })()
25
+ )
26
+ }
27
+
28
+ function onlyElements(children: ReactNode): ReactElement<any>[] {
29
+ const filtered: ReactElement<any>[] = []
30
+ // We use forEach here instead of map as map mutates the component key by preprending `.$`
31
+ Children.forEach(children, (child) => {
32
+ if (isValidElement(child)) filtered.push(child)
33
+ })
34
+ return filtered
35
+ }
36
+
37
+ export const AnimatePresence: FunctionComponent<
38
+ PropsWithChildren<AnimatePresenceProps>
39
+ > = ({
40
+ children,
41
+ enterVariant,
42
+ exitVariant,
43
+ enterExitVariant,
44
+ initial = true,
45
+ onExitComplete,
46
+ exitBeforeEnter,
47
+ mode,
48
+ presenceAffectsLayout = true,
49
+ custom,
50
+ passThrough,
51
+ }) => {
52
+ // Determine effective mode: mode prop takes precedence, then exitBeforeEnter for backwards compatibility
53
+ const effectiveMode = mode ?? (exitBeforeEnter ? 'wait' : 'sync')
54
+
55
+ /**
56
+ * Filter any children that aren't ReactElements. We can only track components
57
+ * between renders with a props.key.
58
+ * IMPORTANT: useMemo ensures reference stability for the comparison below
59
+ */
60
+ const presentChildren = useMemo(() => onlyElements(children), [children])
61
+
62
+ /**
63
+ * Track the keys of the currently rendered children.
64
+ */
65
+ const presentKeys = presentChildren.map(getChildKey)
66
+
67
+ /**
68
+ * If `initial={false}` we only want to pass this to components in the first render.
69
+ */
70
+ const isInitialRender = useRef(true)
71
+
72
+ /**
73
+ * Freeze custom prop for exiting children so direction doesn't reverse mid-exit.
74
+ */
75
+ const frozenCustomRef = useRef(new Map<ComponentKey, any>())
76
+
77
+ /**
78
+ * A ref containing the currently present children. When all exit animations
79
+ * are complete, we use this to re-render with the latest children *committed*
80
+ * rather than the latest children *rendered*.
81
+ */
82
+ const pendingPresentChildren = useRef(presentChildren)
83
+
84
+ /**
85
+ * Track which exiting children have finished animating out.
86
+ */
87
+ const exitComplete = useConstant(() => new Map<ComponentKey, boolean>())
88
+
89
+ /**
90
+ * Save children to render as React state. To ensure this component is concurrent-safe,
91
+ * we check for exiting children via an effect.
92
+ */
93
+ const [diffedChildren, setDiffedChildren] = useState(presentChildren)
94
+ const [renderedChildren, setRenderedChildren] = useState(presentChildren)
95
+
96
+ /**
97
+ * If we've been provided a forceRender function by the LayoutGroupContext,
98
+ * we can use it to force a re-render amongst all surrounding components once
99
+ * all components have finished animating out.
100
+ */
101
+ const forceRender = useContext(LayoutGroupContext).forceRender ?? useForceUpdate()
102
+
103
+ if (passThrough) {
104
+ return <>{children}</>
105
+ }
106
+
107
+ // useInsertionEffect runs before ALL useLayoutEffects (including children's)
108
+ // this ensures pendingPresentChildren and exitComplete are set before
109
+ // animation drivers call sendExitComplete() in their layout effects
110
+ // (critical for immediate completions like animateOnly=[])
111
+ useInsertionEffect(() => {
112
+ isInitialRender.current = false
113
+ pendingPresentChildren.current = presentChildren
114
+
115
+ /**
116
+ * Update complete status of exiting children.
117
+ */
118
+ for (let i = 0; i < renderedChildren.length; i++) {
119
+ const key = getChildKey(renderedChildren[i])
120
+
121
+ if (!presentKeys.includes(key)) {
122
+ if (exitComplete.get(key) !== true) {
123
+ exitComplete.set(key, false)
124
+ }
125
+ } else {
126
+ exitComplete.delete(key)
127
+ frozenCustomRef.current.delete(key)
128
+ }
129
+ }
130
+ }, [renderedChildren, presentKeys.length, presentKeys.join('-')])
131
+
132
+ if (presentChildren !== diffedChildren) {
133
+ let nextChildren = [...presentChildren]
134
+
135
+ /**
136
+ * Loop through all the currently rendered components and decide which
137
+ * are exiting.
138
+ */
139
+ for (let i = 0; i < renderedChildren.length; i++) {
140
+ const child = renderedChildren[i]
141
+ const key = getChildKey(child)
142
+
143
+ if (!presentKeys.includes(key)) {
144
+ nextChildren.splice(i, 0, child)
145
+ // freeze custom at the moment of exit so direction doesn't reverse
146
+ if (!frozenCustomRef.current.has(key)) {
147
+ frozenCustomRef.current.set(key, custom)
148
+ }
149
+ }
150
+ }
151
+
152
+ /**
153
+ * If we're in "wait" mode, and we have exiting children, we want to
154
+ * only render these until they've all exited.
155
+ */
156
+ const exitingChildren = renderedChildren.filter(
157
+ (child) => !presentKeys.includes(getChildKey(child))
158
+ )
159
+ if (effectiveMode === 'wait' && exitingChildren.length) {
160
+ nextChildren = exitingChildren
161
+ }
162
+
163
+ setRenderedChildren(onlyElements(nextChildren))
164
+ setDiffedChildren(presentChildren)
165
+
166
+ /**
167
+ * Early return to ensure once we've set state with the latest diffed
168
+ * children, we can immediately re-render.
169
+ */
170
+ return null
171
+ }
172
+
173
+ return (
174
+ <>
175
+ {renderedChildren.map((child) => {
176
+ const key = getChildKey(child)
177
+ const isPresent =
178
+ presentChildren === renderedChildren || presentKeys.includes(key)
179
+
180
+ const onExit = () => {
181
+ if (exitComplete.has(key)) {
182
+ exitComplete.set(key, true)
183
+ } else {
184
+ return
185
+ }
186
+
187
+ let isEveryExitComplete = true
188
+ exitComplete.forEach((isExitComplete) => {
189
+ if (!isExitComplete) isEveryExitComplete = false
190
+ })
191
+
192
+ if (isEveryExitComplete) {
193
+ forceRender?.()
194
+ setRenderedChildren(pendingPresentChildren.current)
195
+ onExitComplete?.()
196
+ }
197
+ }
198
+
199
+ return (
200
+ <PresenceChild
201
+ key={key}
202
+ isPresent={isPresent}
203
+ initial={!isInitialRender.current || initial ? undefined : false}
204
+ custom={isPresent ? custom : (frozenCustomRef.current.get(key) ?? custom)}
205
+ presenceAffectsLayout={presenceAffectsLayout}
206
+ enterExitVariant={enterExitVariant}
207
+ enterVariant={enterVariant}
208
+ exitVariant={exitVariant}
209
+ onExitComplete={isPresent ? undefined : onExit}
210
+ >
211
+ {child}
212
+ </PresenceChild>
213
+ )
214
+ })}
215
+ </>
216
+ )
217
+ }
218
+
219
+ AnimatePresence.displayName = 'AnimatePresence'
@@ -0,0 +1,8 @@
1
+ import React from 'react'
2
+
3
+ export interface LayoutGroupContextProps {
4
+ id?: string
5
+ forceRender?: VoidFunction
6
+ }
7
+
8
+ export const LayoutGroupContext = React.createContext<LayoutGroupContextProps>({})
@@ -0,0 +1,90 @@
1
+ import { useConstant } from '@hanzogui/use-constant'
2
+ import { PresenceContext } from '@hanzogui/use-presence'
3
+ import type { PresenceContextProps } from '@hanzogui/web'
4
+ import * as React from 'react'
5
+ import { useId } from 'react'
6
+
7
+ import type { VariantLabels } from './types'
8
+
9
+ interface PresenceChildProps {
10
+ children: React.ReactElement<any>
11
+ isPresent: boolean
12
+ onExitComplete?: () => void
13
+ initial?: false | VariantLabels
14
+ custom?: any
15
+ presenceAffectsLayout?: boolean
16
+ exitVariant?: string | null
17
+ enterVariant?: string | null
18
+ enterExitVariant?: string | null
19
+ }
20
+
21
+ // this memo seems to help PopoverContent from continuously re-rendering when open
22
+ export const PresenceChild = React.memo(
23
+ ({
24
+ children,
25
+ initial,
26
+ isPresent,
27
+ onExitComplete,
28
+ exitVariant,
29
+ enterVariant,
30
+ enterExitVariant,
31
+ presenceAffectsLayout,
32
+ custom,
33
+ }: PresenceChildProps) => {
34
+ const presenceChildren = useConstant(newChildrenMap)
35
+ const id = useId() || ''
36
+
37
+ const context = React.useMemo(
38
+ (): PresenceContextProps => {
39
+ return {
40
+ id,
41
+ initial,
42
+ isPresent,
43
+ custom,
44
+ exitVariant,
45
+ enterVariant,
46
+ enterExitVariant,
47
+ onExitComplete: () => {
48
+ presenceChildren.set(id, true)
49
+ for (const isComplete of presenceChildren.values()) {
50
+ if (!isComplete) {
51
+ return // can stop searching when any is incomplete
52
+ }
53
+ }
54
+ onExitComplete?.()
55
+ },
56
+ register: () => {
57
+ presenceChildren.set(id, false)
58
+ return () => presenceChildren.delete(id)
59
+ },
60
+ }
61
+ },
62
+ /**
63
+ * If the presence of a child affects the layout of the components around it,
64
+ * we want to make a new context value to ensure they get re-rendered
65
+ * so they can detect that layout change.
66
+ */
67
+
68
+ // @ts-expect-error its ok
69
+ presenceAffectsLayout ? undefined : [isPresent, exitVariant, enterVariant]
70
+ )
71
+
72
+ React.useMemo(() => {
73
+ presenceChildren.forEach((_, key) => presenceChildren.set(key, false))
74
+ }, [isPresent])
75
+
76
+ /**
77
+ * If there's no animated components to fire exit animations, we want to remove this
78
+ * component immediately.
79
+ */
80
+ React.useEffect(() => {
81
+ !isPresent && !presenceChildren.size && onExitComplete?.()
82
+ }, [isPresent])
83
+
84
+ return <PresenceContext.Provider value={context}>{children}</PresenceContext.Provider>
85
+ }
86
+ )
87
+
88
+ function newChildrenMap(): Map<string, boolean> {
89
+ return new Map()
90
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './AnimatePresence'
2
+ export * from '@hanzogui/use-presence'
3
+ export * from './types'
4
+ export * from './PresenceChild'
package/src/types.ts ADDED
@@ -0,0 +1,107 @@
1
+ export interface Axis {
2
+ min: number
3
+ max: number
4
+ }
5
+
6
+ export interface Box {
7
+ x: Axis
8
+ y: Axis
9
+ }
10
+
11
+ export type VariantLabels = string | string[]
12
+
13
+ /**
14
+ * @public
15
+ */
16
+ export interface AnimatePresenceProps {
17
+ /**
18
+ * Determines how to handle entering and exiting elements.
19
+ * - "sync": Default. Elements animate in and out simultaneously.
20
+ * - "wait": Wait for exit to finish before entering.
21
+ *
22
+ * @public
23
+ */
24
+ mode?: 'sync' | 'wait'
25
+
26
+ /**
27
+ * By passing `initial={false}`, `AnimatePresence` will disable any initial animations on children
28
+ * that are present when the component is first rendered.
29
+ *
30
+ * ```jsx
31
+ * <AnimatePresence initial={false}>
32
+ * {isVisible && (
33
+ * <motion.div
34
+ * key="modal"
35
+ * initial={{ opacity: 0 }}
36
+ * animate={{ opacity: 1 }}
37
+ * exit={{ opacity: 0 }}
38
+ * />
39
+ * )}
40
+ * </AnimatePresence>
41
+ * ```
42
+ *
43
+ * @public
44
+ */
45
+ initial?: boolean
46
+
47
+ /**
48
+ * When a component is removed, there's no longer a chance to update its props directly. So if you need to update
49
+ * a variant for that component as it renders for the exit animation, set `custom` to an object value that
50
+ * is applied to the animating childrens props before they render once more to determine their final exit styles.
51
+ *
52
+ * The custom value must be JSON.stringify-able!
53
+ * This lets us memoize it and use it across workers for some animationd drivers.
54
+ *
55
+ * ```tsx
56
+ * <AnimatePresence custom={{ direction: 10 }}>
57
+ * {isVisible && <Child />}
58
+ * </AnimatePresence>
59
+ *
60
+ * const Child = styled(View, { variants: { direction: ... } })
61
+ * ```
62
+ *
63
+ * @public
64
+ */
65
+ custom?: object
66
+
67
+ /**
68
+ * Fires when all exiting nodes have completed animating out.
69
+ *
70
+ * @public
71
+ */
72
+ onExitComplete?: () => void
73
+
74
+ /**
75
+ * If set to `true`, `AnimatePresence` will only render one component at a time. The exiting component
76
+ * will finish its exit animation before the entering component is rendered.
77
+ *
78
+ * @deprecated Use `mode="wait"` instead.
79
+ */
80
+ exitBeforeEnter?: boolean
81
+
82
+ /**
83
+ * Used in Framer to flag that sibling children *shouldn't* re-render as a result of a
84
+ * child being removed.
85
+ *
86
+ * @internal
87
+ */
88
+ presenceAffectsLayout?: boolean
89
+
90
+ /**
91
+ * @deprecated use `custom` passing it an Object instead
92
+ */
93
+ exitVariant?: string | null
94
+
95
+ /**
96
+ * @deprecated use `custom` passing it an Object instead
97
+ */
98
+ enterVariant?: string | null
99
+
100
+ /**
101
+ * Will use a variant on the child component and apply the true styles for when its entering, false styles for when its exiting
102
+ * @deprecated use `custom` passing it an Object instead
103
+ */
104
+ enterExitVariant?: string | null
105
+
106
+ passThrough?: boolean
107
+ }
@@ -0,0 +1,4 @@
1
+ import type { FunctionComponent, PropsWithChildren } from 'react';
2
+ import type { AnimatePresenceProps } from './types';
3
+ export declare const AnimatePresence: FunctionComponent<PropsWithChildren<AnimatePresenceProps>>;
4
+ //# sourceMappingURL=AnimatePresence.d.ts.map
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ export interface LayoutGroupContextProps {
3
+ id?: string;
4
+ forceRender?: VoidFunction;
5
+ }
6
+ export declare const LayoutGroupContext: React.Context<LayoutGroupContextProps>;
7
+ //# sourceMappingURL=LayoutGroupContext.d.ts.map
@@ -0,0 +1,16 @@
1
+ import * as React from 'react';
2
+ import type { VariantLabels } from './types';
3
+ interface PresenceChildProps {
4
+ children: React.ReactElement<any>;
5
+ isPresent: boolean;
6
+ onExitComplete?: () => void;
7
+ initial?: false | VariantLabels;
8
+ custom?: any;
9
+ presenceAffectsLayout?: boolean;
10
+ exitVariant?: string | null;
11
+ enterVariant?: string | null;
12
+ enterExitVariant?: string | null;
13
+ }
14
+ export declare const PresenceChild: React.MemoExoticComponent<({ children, initial, isPresent, onExitComplete, exitVariant, enterVariant, enterExitVariant, presenceAffectsLayout, custom, }: PresenceChildProps) => import("react/jsx-runtime").JSX.Element>;
15
+ export {};
16
+ //# sourceMappingURL=PresenceChild.d.ts.map
@@ -0,0 +1,5 @@
1
+ export * from './AnimatePresence';
2
+ export * from '@gui/use-presence';
3
+ export * from './types';
4
+ export * from './PresenceChild';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,96 @@
1
+ export interface Axis {
2
+ min: number;
3
+ max: number;
4
+ }
5
+ export interface Box {
6
+ x: Axis;
7
+ y: Axis;
8
+ }
9
+ export type VariantLabels = string | string[];
10
+ /**
11
+ * @public
12
+ */
13
+ export interface AnimatePresenceProps {
14
+ /**
15
+ * Determines how to handle entering and exiting elements.
16
+ * - "sync": Default. Elements animate in and out simultaneously.
17
+ * - "wait": Wait for exit to finish before entering.
18
+ *
19
+ * @public
20
+ */
21
+ mode?: 'sync' | 'wait';
22
+ /**
23
+ * By passing `initial={false}`, `AnimatePresence` will disable any initial animations on children
24
+ * that are present when the component is first rendered.
25
+ *
26
+ * ```jsx
27
+ * <AnimatePresence initial={false}>
28
+ * {isVisible && (
29
+ * <motion.div
30
+ * key="modal"
31
+ * initial={{ opacity: 0 }}
32
+ * animate={{ opacity: 1 }}
33
+ * exit={{ opacity: 0 }}
34
+ * />
35
+ * )}
36
+ * </AnimatePresence>
37
+ * ```
38
+ *
39
+ * @public
40
+ */
41
+ initial?: boolean;
42
+ /**
43
+ * When a component is removed, there's no longer a chance to update its props directly. So if you need to update
44
+ * a variant for that component as it renders for the exit animation, set `custom` to an object value that
45
+ * is applied to the animating childrens props before they render once more to determine their final exit styles.
46
+ *
47
+ * The custom value must be JSON.stringify-able!
48
+ * This lets us memoize it and use it across workers for some animationd drivers.
49
+ *
50
+ * ```tsx
51
+ * <AnimatePresence custom={{ direction: 10 }}>
52
+ * {isVisible && <Child />}
53
+ * </AnimatePresence>
54
+ *
55
+ * const Child = styled(View, { variants: { direction: ... } })
56
+ * ```
57
+ *
58
+ * @public
59
+ */
60
+ custom?: object;
61
+ /**
62
+ * Fires when all exiting nodes have completed animating out.
63
+ *
64
+ * @public
65
+ */
66
+ onExitComplete?: () => void;
67
+ /**
68
+ * If set to `true`, `AnimatePresence` will only render one component at a time. The exiting component
69
+ * will finish its exit animation before the entering component is rendered.
70
+ *
71
+ * @deprecated Use `mode="wait"` instead.
72
+ */
73
+ exitBeforeEnter?: boolean;
74
+ /**
75
+ * Used in Framer to flag that sibling children *shouldn't* re-render as a result of a
76
+ * child being removed.
77
+ *
78
+ * @internal
79
+ */
80
+ presenceAffectsLayout?: boolean;
81
+ /**
82
+ * @deprecated use `custom` passing it an Object instead
83
+ */
84
+ exitVariant?: string | null;
85
+ /**
86
+ * @deprecated use `custom` passing it an Object instead
87
+ */
88
+ enterVariant?: string | null;
89
+ /**
90
+ * Will use a variant on the child component and apply the true styles for when its entering, false styles for when its exiting
91
+ * @deprecated use `custom` passing it an Object instead
92
+ */
93
+ enterExitVariant?: string | null;
94
+ passThrough?: boolean;
95
+ }
96
+ //# sourceMappingURL=types.d.ts.map