@huin-core/react-presence 1.0.2 → 1.0.3
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/dist/index.d.mts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +158 -0
- package/dist/index.js.map +7 -0
- package/dist/index.mjs +126 -0
- package/dist/index.mjs.map +7 -0
- package/package.json +1 -1
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
interface PresenceProps {
|
|
4
|
+
children: React.ReactElement | ((props: {
|
|
5
|
+
present: boolean;
|
|
6
|
+
}) => React.ReactElement);
|
|
7
|
+
present: boolean;
|
|
8
|
+
}
|
|
9
|
+
declare const Presence: React.FC<PresenceProps>;
|
|
10
|
+
|
|
11
|
+
export { Presence, type PresenceProps };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
interface PresenceProps {
|
|
4
|
+
children: React.ReactElement | ((props: {
|
|
5
|
+
present: boolean;
|
|
6
|
+
}) => React.ReactElement);
|
|
7
|
+
present: boolean;
|
|
8
|
+
}
|
|
9
|
+
declare const Presence: React.FC<PresenceProps>;
|
|
10
|
+
|
|
11
|
+
export { Presence, type PresenceProps };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
"use client";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
|
|
31
|
+
// packages/react/presence/src/index.ts
|
|
32
|
+
var src_exports = {};
|
|
33
|
+
__export(src_exports, {
|
|
34
|
+
Presence: () => Presence
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(src_exports);
|
|
37
|
+
|
|
38
|
+
// packages/react/presence/src/Presence.tsx
|
|
39
|
+
var React2 = __toESM(require("react"));
|
|
40
|
+
var ReactDOM = __toESM(require("react-dom"));
|
|
41
|
+
var import_react_compose_refs = require("@huin-core/react-compose-refs");
|
|
42
|
+
var import_react_use_layout_effect = require("@huin-core/react-use-layout-effect");
|
|
43
|
+
|
|
44
|
+
// packages/react/presence/src/useStateMachine.tsx
|
|
45
|
+
var React = __toESM(require("react"));
|
|
46
|
+
function useStateMachine(initialState, machine) {
|
|
47
|
+
return React.useReducer((state, event) => {
|
|
48
|
+
const nextState = machine[state][event];
|
|
49
|
+
return nextState ?? state;
|
|
50
|
+
}, initialState);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// packages/react/presence/src/Presence.tsx
|
|
54
|
+
var Presence = (props) => {
|
|
55
|
+
const { present, children } = props;
|
|
56
|
+
const presence = usePresence(present);
|
|
57
|
+
const child = typeof children === "function" ? children({ present: presence.isPresent }) : React2.Children.only(children);
|
|
58
|
+
const ref = (0, import_react_compose_refs.useComposedRefs)(presence.ref, getElementRef(child));
|
|
59
|
+
const forceMount = typeof children === "function";
|
|
60
|
+
return forceMount || presence.isPresent ? React2.cloneElement(child, { ref }) : null;
|
|
61
|
+
};
|
|
62
|
+
Presence.displayName = "Presence";
|
|
63
|
+
function usePresence(present) {
|
|
64
|
+
const [node, setNode] = React2.useState();
|
|
65
|
+
const stylesRef = React2.useRef({});
|
|
66
|
+
const prevPresentRef = React2.useRef(present);
|
|
67
|
+
const prevAnimationNameRef = React2.useRef("none");
|
|
68
|
+
const initialState = present ? "mounted" : "unmounted";
|
|
69
|
+
const [state, send] = useStateMachine(initialState, {
|
|
70
|
+
mounted: {
|
|
71
|
+
UNMOUNT: "unmounted",
|
|
72
|
+
ANIMATION_OUT: "unmountSuspended"
|
|
73
|
+
},
|
|
74
|
+
unmountSuspended: {
|
|
75
|
+
MOUNT: "mounted",
|
|
76
|
+
ANIMATION_END: "unmounted"
|
|
77
|
+
},
|
|
78
|
+
unmounted: {
|
|
79
|
+
MOUNT: "mounted"
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
React2.useEffect(() => {
|
|
83
|
+
const currentAnimationName = getAnimationName(stylesRef.current);
|
|
84
|
+
prevAnimationNameRef.current = state === "mounted" ? currentAnimationName : "none";
|
|
85
|
+
}, [state]);
|
|
86
|
+
(0, import_react_use_layout_effect.useLayoutEffect)(() => {
|
|
87
|
+
const styles = stylesRef.current;
|
|
88
|
+
const wasPresent = prevPresentRef.current;
|
|
89
|
+
const hasPresentChanged = wasPresent !== present;
|
|
90
|
+
if (hasPresentChanged) {
|
|
91
|
+
const prevAnimationName = prevAnimationNameRef.current;
|
|
92
|
+
const currentAnimationName = getAnimationName(styles);
|
|
93
|
+
if (present) {
|
|
94
|
+
send("MOUNT");
|
|
95
|
+
} else if (currentAnimationName === "none" || styles?.display === "none") {
|
|
96
|
+
send("UNMOUNT");
|
|
97
|
+
} else {
|
|
98
|
+
const isAnimating = prevAnimationName !== currentAnimationName;
|
|
99
|
+
if (wasPresent && isAnimating) {
|
|
100
|
+
send("ANIMATION_OUT");
|
|
101
|
+
} else {
|
|
102
|
+
send("UNMOUNT");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
prevPresentRef.current = present;
|
|
106
|
+
}
|
|
107
|
+
}, [present, send]);
|
|
108
|
+
(0, import_react_use_layout_effect.useLayoutEffect)(() => {
|
|
109
|
+
if (node) {
|
|
110
|
+
const handleAnimationEnd = (event) => {
|
|
111
|
+
const currentAnimationName = getAnimationName(stylesRef.current);
|
|
112
|
+
const isCurrentAnimation = currentAnimationName.includes(event.animationName);
|
|
113
|
+
if (event.target === node && isCurrentAnimation) {
|
|
114
|
+
ReactDOM.flushSync(() => send("ANIMATION_END"));
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const handleAnimationStart = (event) => {
|
|
118
|
+
if (event.target === node) {
|
|
119
|
+
prevAnimationNameRef.current = getAnimationName(stylesRef.current);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
node.addEventListener("animationstart", handleAnimationStart);
|
|
123
|
+
node.addEventListener("animationcancel", handleAnimationEnd);
|
|
124
|
+
node.addEventListener("animationend", handleAnimationEnd);
|
|
125
|
+
return () => {
|
|
126
|
+
node.removeEventListener("animationstart", handleAnimationStart);
|
|
127
|
+
node.removeEventListener("animationcancel", handleAnimationEnd);
|
|
128
|
+
node.removeEventListener("animationend", handleAnimationEnd);
|
|
129
|
+
};
|
|
130
|
+
} else {
|
|
131
|
+
send("ANIMATION_END");
|
|
132
|
+
}
|
|
133
|
+
}, [node, send]);
|
|
134
|
+
return {
|
|
135
|
+
isPresent: ["mounted", "unmountSuspended"].includes(state),
|
|
136
|
+
ref: React2.useCallback((node2) => {
|
|
137
|
+
if (node2) stylesRef.current = getComputedStyle(node2);
|
|
138
|
+
setNode(node2);
|
|
139
|
+
}, [])
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function getAnimationName(styles) {
|
|
143
|
+
return styles?.animationName || "none";
|
|
144
|
+
}
|
|
145
|
+
function getElementRef(element) {
|
|
146
|
+
let getter = Object.getOwnPropertyDescriptor(element.props, "ref")?.get;
|
|
147
|
+
let mayWarn = getter && "isReactWarning" in getter && getter.isReactWarning;
|
|
148
|
+
if (mayWarn) {
|
|
149
|
+
return element.ref;
|
|
150
|
+
}
|
|
151
|
+
getter = Object.getOwnPropertyDescriptor(element, "ref")?.get;
|
|
152
|
+
mayWarn = getter && "isReactWarning" in getter && getter.isReactWarning;
|
|
153
|
+
if (mayWarn) {
|
|
154
|
+
return element.props.ref;
|
|
155
|
+
}
|
|
156
|
+
return element.props.ref || element.ref;
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/index.ts", "../src/Presence.tsx", "../src/useStateMachine.tsx"],
|
|
4
|
+
"sourcesContent": ["'use client';\nexport { Presence } from './Presence';\nexport type { PresenceProps } from './Presence';\n", "import * as React from 'react';\nimport * as ReactDOM from 'react-dom';\nimport { useComposedRefs } from '@huin-core/react-compose-refs';\nimport { useLayoutEffect } from '@huin-core/react-use-layout-effect';\nimport { useStateMachine } from './useStateMachine';\n\ninterface PresenceProps {\n children: React.ReactElement | ((props: { present: boolean }) => React.ReactElement);\n present: boolean;\n}\n\nconst Presence: React.FC<PresenceProps> = (props) => {\n const { present, children } = props;\n const presence = usePresence(present);\n\n const child = (\n typeof children === 'function'\n ? children({ present: presence.isPresent })\n : React.Children.only(children)\n ) as React.ReactElement;\n\n const ref = useComposedRefs(presence.ref, getElementRef(child));\n const forceMount = typeof children === 'function';\n return forceMount || presence.isPresent ? React.cloneElement(child, { ref }) : null;\n};\n\nPresence.displayName = 'Presence';\n\n/* -------------------------------------------------------------------------------------------------\n * usePresence\n * -----------------------------------------------------------------------------------------------*/\n\nfunction usePresence(present: boolean) {\n const [node, setNode] = React.useState<HTMLElement>();\n const stylesRef = React.useRef<CSSStyleDeclaration>({} as any);\n const prevPresentRef = React.useRef(present);\n const prevAnimationNameRef = React.useRef<string>('none');\n const initialState = present ? 'mounted' : 'unmounted';\n const [state, send] = useStateMachine(initialState, {\n mounted: {\n UNMOUNT: 'unmounted',\n ANIMATION_OUT: 'unmountSuspended',\n },\n unmountSuspended: {\n MOUNT: 'mounted',\n ANIMATION_END: 'unmounted',\n },\n unmounted: {\n MOUNT: 'mounted',\n },\n });\n\n React.useEffect(() => {\n const currentAnimationName = getAnimationName(stylesRef.current);\n prevAnimationNameRef.current = state === 'mounted' ? currentAnimationName : 'none';\n }, [state]);\n\n useLayoutEffect(() => {\n const styles = stylesRef.current;\n const wasPresent = prevPresentRef.current;\n const hasPresentChanged = wasPresent !== present;\n\n if (hasPresentChanged) {\n const prevAnimationName = prevAnimationNameRef.current;\n const currentAnimationName = getAnimationName(styles);\n\n if (present) {\n send('MOUNT');\n } else if (currentAnimationName === 'none' || styles?.display === 'none') {\n // If there is no exit animation or the element is hidden, animations won't run\n // so we unmount instantly\n send('UNMOUNT');\n } else {\n /**\n * When `present` changes to `false`, we check changes to animation-name to\n * determine whether an animation has started. We chose this approach (reading\n * computed styles) because there is no `animationrun` event and `animationstart`\n * fires after `animation-delay` has expired which would be too late.\n */\n const isAnimating = prevAnimationName !== currentAnimationName;\n\n if (wasPresent && isAnimating) {\n send('ANIMATION_OUT');\n } else {\n send('UNMOUNT');\n }\n }\n\n prevPresentRef.current = present;\n }\n }, [present, send]);\n\n useLayoutEffect(() => {\n if (node) {\n /**\n * Triggering an ANIMATION_OUT during an ANIMATION_IN will fire an `animationcancel`\n * event for ANIMATION_IN after we have entered `unmountSuspended` state. So, we\n * make sure we only trigger ANIMATION_END for the currently active animation.\n */\n const handleAnimationEnd = (event: AnimationEvent) => {\n const currentAnimationName = getAnimationName(stylesRef.current);\n const isCurrentAnimation = currentAnimationName.includes(event.animationName);\n if (event.target === node && isCurrentAnimation) {\n // With React 18 concurrency this update is applied\n // a frame after the animation ends, creating a flash of visible content.\n // By manually flushing we ensure they sync within a frame, removing the flash.\n ReactDOM.flushSync(() => send('ANIMATION_END'));\n }\n };\n const handleAnimationStart = (event: AnimationEvent) => {\n if (event.target === node) {\n // if animation occurred, store its name as the previous animation.\n prevAnimationNameRef.current = getAnimationName(stylesRef.current);\n }\n };\n node.addEventListener('animationstart', handleAnimationStart);\n node.addEventListener('animationcancel', handleAnimationEnd);\n node.addEventListener('animationend', handleAnimationEnd);\n return () => {\n node.removeEventListener('animationstart', handleAnimationStart);\n node.removeEventListener('animationcancel', handleAnimationEnd);\n node.removeEventListener('animationend', handleAnimationEnd);\n };\n } else {\n // Transition to the unmounted state if the node is removed prematurely.\n // We avoid doing so during cleanup as the node may change but still exist.\n send('ANIMATION_END');\n }\n }, [node, send]);\n\n return {\n isPresent: ['mounted', 'unmountSuspended'].includes(state),\n ref: React.useCallback((node: HTMLElement) => {\n if (node) stylesRef.current = getComputedStyle(node);\n setNode(node);\n }, []),\n };\n}\n\n/* -----------------------------------------------------------------------------------------------*/\n\nfunction getAnimationName(styles?: CSSStyleDeclaration) {\n return styles?.animationName || 'none';\n}\n\n// Before React 19 accessing `element.props.ref` will throw a warning and suggest using `element.ref`\n// After React 19 accessing `element.ref` does the opposite.\n// https://github.com/facebook/react/pull/28348\n//\n// Access the ref using the method that doesn't yield a warning.\nfunction getElementRef(element: React.ReactElement) {\n // React <=18 in DEV\n let getter = Object.getOwnPropertyDescriptor(element.props, 'ref')?.get;\n let mayWarn = getter && 'isReactWarning' in getter && getter.isReactWarning;\n if (mayWarn) {\n return (element as any).ref;\n }\n\n // React 19 in DEV\n getter = Object.getOwnPropertyDescriptor(element, 'ref')?.get;\n mayWarn = getter && 'isReactWarning' in getter && getter.isReactWarning;\n if (mayWarn) {\n return element.props.ref;\n }\n\n // Not DEV\n return element.props.ref || (element as any).ref;\n}\n\nexport { Presence };\nexport type { PresenceProps };\n", "import * as React from 'react';\n\ntype Machine<S> = { [k: string]: { [k: string]: S } };\ntype MachineState<T> = keyof T;\ntype MachineEvent<T> = keyof UnionToIntersection<T[keyof T]>;\n\n// \uD83E\uDD2F https://fettblog.eu/typescript-union-to-intersection/\ntype UnionToIntersection<T> = (T extends any ? (x: T) => any : never) extends (x: infer R) => any\n ? R\n : never;\n\nexport function useStateMachine<M>(\n initialState: MachineState<M>,\n machine: M & Machine<MachineState<M>>\n) {\n return React.useReducer((state: MachineState<M>, event: MachineEvent<M>): MachineState<M> => {\n const nextState = (machine[state] as any)[event];\n return nextState ?? state;\n }, initialState);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["React", "node"]
|
|
7
|
+
}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// packages/react/presence/src/Presence.tsx
|
|
4
|
+
import * as React2 from "react";
|
|
5
|
+
import * as ReactDOM from "react-dom";
|
|
6
|
+
import { useComposedRefs } from "@huin-core/react-compose-refs";
|
|
7
|
+
import { useLayoutEffect } from "@huin-core/react-use-layout-effect";
|
|
8
|
+
|
|
9
|
+
// packages/react/presence/src/useStateMachine.tsx
|
|
10
|
+
import * as React from "react";
|
|
11
|
+
function useStateMachine(initialState, machine) {
|
|
12
|
+
return React.useReducer((state, event) => {
|
|
13
|
+
const nextState = machine[state][event];
|
|
14
|
+
return nextState ?? state;
|
|
15
|
+
}, initialState);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// packages/react/presence/src/Presence.tsx
|
|
19
|
+
var Presence = (props) => {
|
|
20
|
+
const { present, children } = props;
|
|
21
|
+
const presence = usePresence(present);
|
|
22
|
+
const child = typeof children === "function" ? children({ present: presence.isPresent }) : React2.Children.only(children);
|
|
23
|
+
const ref = useComposedRefs(presence.ref, getElementRef(child));
|
|
24
|
+
const forceMount = typeof children === "function";
|
|
25
|
+
return forceMount || presence.isPresent ? React2.cloneElement(child, { ref }) : null;
|
|
26
|
+
};
|
|
27
|
+
Presence.displayName = "Presence";
|
|
28
|
+
function usePresence(present) {
|
|
29
|
+
const [node, setNode] = React2.useState();
|
|
30
|
+
const stylesRef = React2.useRef({});
|
|
31
|
+
const prevPresentRef = React2.useRef(present);
|
|
32
|
+
const prevAnimationNameRef = React2.useRef("none");
|
|
33
|
+
const initialState = present ? "mounted" : "unmounted";
|
|
34
|
+
const [state, send] = useStateMachine(initialState, {
|
|
35
|
+
mounted: {
|
|
36
|
+
UNMOUNT: "unmounted",
|
|
37
|
+
ANIMATION_OUT: "unmountSuspended"
|
|
38
|
+
},
|
|
39
|
+
unmountSuspended: {
|
|
40
|
+
MOUNT: "mounted",
|
|
41
|
+
ANIMATION_END: "unmounted"
|
|
42
|
+
},
|
|
43
|
+
unmounted: {
|
|
44
|
+
MOUNT: "mounted"
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
React2.useEffect(() => {
|
|
48
|
+
const currentAnimationName = getAnimationName(stylesRef.current);
|
|
49
|
+
prevAnimationNameRef.current = state === "mounted" ? currentAnimationName : "none";
|
|
50
|
+
}, [state]);
|
|
51
|
+
useLayoutEffect(() => {
|
|
52
|
+
const styles = stylesRef.current;
|
|
53
|
+
const wasPresent = prevPresentRef.current;
|
|
54
|
+
const hasPresentChanged = wasPresent !== present;
|
|
55
|
+
if (hasPresentChanged) {
|
|
56
|
+
const prevAnimationName = prevAnimationNameRef.current;
|
|
57
|
+
const currentAnimationName = getAnimationName(styles);
|
|
58
|
+
if (present) {
|
|
59
|
+
send("MOUNT");
|
|
60
|
+
} else if (currentAnimationName === "none" || styles?.display === "none") {
|
|
61
|
+
send("UNMOUNT");
|
|
62
|
+
} else {
|
|
63
|
+
const isAnimating = prevAnimationName !== currentAnimationName;
|
|
64
|
+
if (wasPresent && isAnimating) {
|
|
65
|
+
send("ANIMATION_OUT");
|
|
66
|
+
} else {
|
|
67
|
+
send("UNMOUNT");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
prevPresentRef.current = present;
|
|
71
|
+
}
|
|
72
|
+
}, [present, send]);
|
|
73
|
+
useLayoutEffect(() => {
|
|
74
|
+
if (node) {
|
|
75
|
+
const handleAnimationEnd = (event) => {
|
|
76
|
+
const currentAnimationName = getAnimationName(stylesRef.current);
|
|
77
|
+
const isCurrentAnimation = currentAnimationName.includes(event.animationName);
|
|
78
|
+
if (event.target === node && isCurrentAnimation) {
|
|
79
|
+
ReactDOM.flushSync(() => send("ANIMATION_END"));
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const handleAnimationStart = (event) => {
|
|
83
|
+
if (event.target === node) {
|
|
84
|
+
prevAnimationNameRef.current = getAnimationName(stylesRef.current);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
node.addEventListener("animationstart", handleAnimationStart);
|
|
88
|
+
node.addEventListener("animationcancel", handleAnimationEnd);
|
|
89
|
+
node.addEventListener("animationend", handleAnimationEnd);
|
|
90
|
+
return () => {
|
|
91
|
+
node.removeEventListener("animationstart", handleAnimationStart);
|
|
92
|
+
node.removeEventListener("animationcancel", handleAnimationEnd);
|
|
93
|
+
node.removeEventListener("animationend", handleAnimationEnd);
|
|
94
|
+
};
|
|
95
|
+
} else {
|
|
96
|
+
send("ANIMATION_END");
|
|
97
|
+
}
|
|
98
|
+
}, [node, send]);
|
|
99
|
+
return {
|
|
100
|
+
isPresent: ["mounted", "unmountSuspended"].includes(state),
|
|
101
|
+
ref: React2.useCallback((node2) => {
|
|
102
|
+
if (node2) stylesRef.current = getComputedStyle(node2);
|
|
103
|
+
setNode(node2);
|
|
104
|
+
}, [])
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function getAnimationName(styles) {
|
|
108
|
+
return styles?.animationName || "none";
|
|
109
|
+
}
|
|
110
|
+
function getElementRef(element) {
|
|
111
|
+
let getter = Object.getOwnPropertyDescriptor(element.props, "ref")?.get;
|
|
112
|
+
let mayWarn = getter && "isReactWarning" in getter && getter.isReactWarning;
|
|
113
|
+
if (mayWarn) {
|
|
114
|
+
return element.ref;
|
|
115
|
+
}
|
|
116
|
+
getter = Object.getOwnPropertyDescriptor(element, "ref")?.get;
|
|
117
|
+
mayWarn = getter && "isReactWarning" in getter && getter.isReactWarning;
|
|
118
|
+
if (mayWarn) {
|
|
119
|
+
return element.props.ref;
|
|
120
|
+
}
|
|
121
|
+
return element.props.ref || element.ref;
|
|
122
|
+
}
|
|
123
|
+
export {
|
|
124
|
+
Presence
|
|
125
|
+
};
|
|
126
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/Presence.tsx", "../src/useStateMachine.tsx"],
|
|
4
|
+
"sourcesContent": ["import * as React from 'react';\nimport * as ReactDOM from 'react-dom';\nimport { useComposedRefs } from '@huin-core/react-compose-refs';\nimport { useLayoutEffect } from '@huin-core/react-use-layout-effect';\nimport { useStateMachine } from './useStateMachine';\n\ninterface PresenceProps {\n children: React.ReactElement | ((props: { present: boolean }) => React.ReactElement);\n present: boolean;\n}\n\nconst Presence: React.FC<PresenceProps> = (props) => {\n const { present, children } = props;\n const presence = usePresence(present);\n\n const child = (\n typeof children === 'function'\n ? children({ present: presence.isPresent })\n : React.Children.only(children)\n ) as React.ReactElement;\n\n const ref = useComposedRefs(presence.ref, getElementRef(child));\n const forceMount = typeof children === 'function';\n return forceMount || presence.isPresent ? React.cloneElement(child, { ref }) : null;\n};\n\nPresence.displayName = 'Presence';\n\n/* -------------------------------------------------------------------------------------------------\n * usePresence\n * -----------------------------------------------------------------------------------------------*/\n\nfunction usePresence(present: boolean) {\n const [node, setNode] = React.useState<HTMLElement>();\n const stylesRef = React.useRef<CSSStyleDeclaration>({} as any);\n const prevPresentRef = React.useRef(present);\n const prevAnimationNameRef = React.useRef<string>('none');\n const initialState = present ? 'mounted' : 'unmounted';\n const [state, send] = useStateMachine(initialState, {\n mounted: {\n UNMOUNT: 'unmounted',\n ANIMATION_OUT: 'unmountSuspended',\n },\n unmountSuspended: {\n MOUNT: 'mounted',\n ANIMATION_END: 'unmounted',\n },\n unmounted: {\n MOUNT: 'mounted',\n },\n });\n\n React.useEffect(() => {\n const currentAnimationName = getAnimationName(stylesRef.current);\n prevAnimationNameRef.current = state === 'mounted' ? currentAnimationName : 'none';\n }, [state]);\n\n useLayoutEffect(() => {\n const styles = stylesRef.current;\n const wasPresent = prevPresentRef.current;\n const hasPresentChanged = wasPresent !== present;\n\n if (hasPresentChanged) {\n const prevAnimationName = prevAnimationNameRef.current;\n const currentAnimationName = getAnimationName(styles);\n\n if (present) {\n send('MOUNT');\n } else if (currentAnimationName === 'none' || styles?.display === 'none') {\n // If there is no exit animation or the element is hidden, animations won't run\n // so we unmount instantly\n send('UNMOUNT');\n } else {\n /**\n * When `present` changes to `false`, we check changes to animation-name to\n * determine whether an animation has started. We chose this approach (reading\n * computed styles) because there is no `animationrun` event and `animationstart`\n * fires after `animation-delay` has expired which would be too late.\n */\n const isAnimating = prevAnimationName !== currentAnimationName;\n\n if (wasPresent && isAnimating) {\n send('ANIMATION_OUT');\n } else {\n send('UNMOUNT');\n }\n }\n\n prevPresentRef.current = present;\n }\n }, [present, send]);\n\n useLayoutEffect(() => {\n if (node) {\n /**\n * Triggering an ANIMATION_OUT during an ANIMATION_IN will fire an `animationcancel`\n * event for ANIMATION_IN after we have entered `unmountSuspended` state. So, we\n * make sure we only trigger ANIMATION_END for the currently active animation.\n */\n const handleAnimationEnd = (event: AnimationEvent) => {\n const currentAnimationName = getAnimationName(stylesRef.current);\n const isCurrentAnimation = currentAnimationName.includes(event.animationName);\n if (event.target === node && isCurrentAnimation) {\n // With React 18 concurrency this update is applied\n // a frame after the animation ends, creating a flash of visible content.\n // By manually flushing we ensure they sync within a frame, removing the flash.\n ReactDOM.flushSync(() => send('ANIMATION_END'));\n }\n };\n const handleAnimationStart = (event: AnimationEvent) => {\n if (event.target === node) {\n // if animation occurred, store its name as the previous animation.\n prevAnimationNameRef.current = getAnimationName(stylesRef.current);\n }\n };\n node.addEventListener('animationstart', handleAnimationStart);\n node.addEventListener('animationcancel', handleAnimationEnd);\n node.addEventListener('animationend', handleAnimationEnd);\n return () => {\n node.removeEventListener('animationstart', handleAnimationStart);\n node.removeEventListener('animationcancel', handleAnimationEnd);\n node.removeEventListener('animationend', handleAnimationEnd);\n };\n } else {\n // Transition to the unmounted state if the node is removed prematurely.\n // We avoid doing so during cleanup as the node may change but still exist.\n send('ANIMATION_END');\n }\n }, [node, send]);\n\n return {\n isPresent: ['mounted', 'unmountSuspended'].includes(state),\n ref: React.useCallback((node: HTMLElement) => {\n if (node) stylesRef.current = getComputedStyle(node);\n setNode(node);\n }, []),\n };\n}\n\n/* -----------------------------------------------------------------------------------------------*/\n\nfunction getAnimationName(styles?: CSSStyleDeclaration) {\n return styles?.animationName || 'none';\n}\n\n// Before React 19 accessing `element.props.ref` will throw a warning and suggest using `element.ref`\n// After React 19 accessing `element.ref` does the opposite.\n// https://github.com/facebook/react/pull/28348\n//\n// Access the ref using the method that doesn't yield a warning.\nfunction getElementRef(element: React.ReactElement) {\n // React <=18 in DEV\n let getter = Object.getOwnPropertyDescriptor(element.props, 'ref')?.get;\n let mayWarn = getter && 'isReactWarning' in getter && getter.isReactWarning;\n if (mayWarn) {\n return (element as any).ref;\n }\n\n // React 19 in DEV\n getter = Object.getOwnPropertyDescriptor(element, 'ref')?.get;\n mayWarn = getter && 'isReactWarning' in getter && getter.isReactWarning;\n if (mayWarn) {\n return element.props.ref;\n }\n\n // Not DEV\n return element.props.ref || (element as any).ref;\n}\n\nexport { Presence };\nexport type { PresenceProps };\n", "import * as React from 'react';\n\ntype Machine<S> = { [k: string]: { [k: string]: S } };\ntype MachineState<T> = keyof T;\ntype MachineEvent<T> = keyof UnionToIntersection<T[keyof T]>;\n\n// \uD83E\uDD2F https://fettblog.eu/typescript-union-to-intersection/\ntype UnionToIntersection<T> = (T extends any ? (x: T) => any : never) extends (x: infer R) => any\n ? R\n : never;\n\nexport function useStateMachine<M>(\n initialState: MachineState<M>,\n machine: M & Machine<MachineState<M>>\n) {\n return React.useReducer((state: MachineState<M>, event: MachineEvent<M>): MachineState<M> => {\n const nextState = (machine[state] as any)[event];\n return nextState ?? state;\n }, initialState);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["React", "node"]
|
|
7
|
+
}
|