Skip to content

Commit 9a67ee4

Browse files
committed
feat: add usePresence hook
1 parent 0e0f070 commit 9a67ee4

3 files changed

Lines changed: 116 additions & 0 deletions

File tree

packages/hooks/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export * from './use-key-filter';
66
export * from './use-mask';
77
export * from './use-match-media';
88
export * from './use-mount-effect';
9+
export * from './use-presence';
910
export * from './use-previous';
1011
export * from './use-props';
1112
export * from './use-scrolltop';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
test.skip('empty placeholder', () => {});
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import * as React from 'react';
2+
3+
/**
4+
* usePresence hook is used to manage the presence of a component.
5+
*
6+
* @param open - The open state.
7+
* @returns An object containing the present, exiting, mounted, and ref states.
8+
*
9+
* @example
10+
* ```tsx
11+
* const { present, exiting, mounted, ref } = usePresence(true);
12+
*
13+
* return present && (
14+
* <div className="card flex justify-center"></div>
15+
* );
16+
*/
17+
18+
export function usePresence(open: boolean, fallbackMs: number = 500) {
19+
const [present, setPresent] = React.useState(open);
20+
const [exiting, setExiting] = React.useState(false);
21+
const [mounted, setMounted] = React.useState(false);
22+
const ref = React.useRef<HTMLElement>(null);
23+
const cleanupRef = React.useRef<(() => void) | null>(null);
24+
const rafCleanupRef = React.useRef<(() => void) | null>(null);
25+
26+
React.useEffect(() => {
27+
if (cleanupRef.current) {
28+
cleanupRef.current();
29+
cleanupRef.current = null;
30+
}
31+
32+
if (rafCleanupRef.current) {
33+
rafCleanupRef.current();
34+
rafCleanupRef.current = null;
35+
}
36+
37+
if (open) {
38+
setPresent(true);
39+
setExiting(false);
40+
41+
const rafs: number[] = [];
42+
43+
rafs.push(
44+
requestAnimationFrame(() => {
45+
rafs.push(
46+
requestAnimationFrame(() => {
47+
rafs.push(requestAnimationFrame(() => setMounted(true)));
48+
})
49+
);
50+
})
51+
);
52+
53+
rafCleanupRef.current = () => {
54+
rafs.forEach((raf) => cancelAnimationFrame(raf));
55+
rafs.length = 0;
56+
};
57+
} else if (ref.current) {
58+
setExiting(true);
59+
setMounted(false);
60+
const node = ref.current;
61+
let isHandled = false;
62+
63+
const handleEnd = () => {
64+
if (isHandled) return;
65+
66+
isHandled = true;
67+
68+
setPresent(false);
69+
setExiting(false);
70+
71+
node.removeEventListener('transitionend', handleEnd);
72+
node.removeEventListener('animationend', handleEnd);
73+
74+
cleanupRef.current = null;
75+
};
76+
77+
node.addEventListener('transitionend', handleEnd, { passive: true });
78+
node.addEventListener('animationend', handleEnd, { passive: true });
79+
80+
// const fallbackTimeout = setTimeout(() => {
81+
// if (!isHandled) {
82+
// handleEnd();
83+
// }
84+
// }, fallbackMs);
85+
86+
cleanupRef.current = () => {
87+
// clearTimeout(fallbackTimeout);
88+
89+
if (!isHandled) {
90+
node.removeEventListener('transitionend', handleEnd);
91+
node.removeEventListener('animationend', handleEnd);
92+
}
93+
};
94+
} else {
95+
setMounted(false);
96+
setPresent(false);
97+
setExiting(false);
98+
}
99+
}, [open, fallbackMs]);
100+
101+
React.useEffect(() => {
102+
return () => {
103+
if (cleanupRef.current) {
104+
cleanupRef.current();
105+
}
106+
107+
if (rafCleanupRef.current) {
108+
rafCleanupRef.current();
109+
}
110+
};
111+
}, []);
112+
113+
return { present, exiting, mounted, ref };
114+
}

0 commit comments

Comments
 (0)