@huin-core/react-presence 1.0.1 → 1.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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 +3 -3
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
|
+
}
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@huin-core/react-presence",
|
3
|
-
"version": "1.0.
|
3
|
+
"version": "1.0.3",
|
4
4
|
"license": "MIT",
|
5
5
|
"exports": {
|
6
6
|
".": {
|
@@ -28,8 +28,8 @@
|
|
28
28
|
"version": "yarn version"
|
29
29
|
},
|
30
30
|
"dependencies": {
|
31
|
-
"@huin-core/react-compose-refs": "
|
32
|
-
"@huin-core/react-use-layout-effect": "
|
31
|
+
"@huin-core/react-compose-refs": "latest",
|
32
|
+
"@huin-core/react-use-layout-effect": "latest"
|
33
33
|
},
|
34
34
|
"peerDependencies": {
|
35
35
|
"@types/react": "*",
|