forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathOpenSurface.tsx
More file actions
111 lines (100 loc) · 2.75 KB
/
Copy pathOpenSurface.tsx
File metadata and controls
111 lines (100 loc) · 2.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
'use client';
import {
useEffect,
useLayoutEffect,
useRef,
useState,
type HTMLAttributes,
type ReactNode,
} from 'react';
import { cn } from '@/lib/utils';
import { prefersReducedMotion, readDurationToken } from '@/lib/transitionsDev';
function closeMs(className?: string): number {
if (typeof className === 'string' && className.includes('t-modal')) {
return readDurationToken('--modal-close-dur', 150);
}
return readDurationToken('--dropdown-close-dur', 150);
}
export function OpenSurface({
open = true,
onExited,
className,
children,
...props
}: HTMLAttributes<HTMLDivElement> & {
children: ReactNode;
open?: boolean;
onExited?: () => void;
}) {
const ref = useRef<HTMLDivElement>(null);
const onExitedRef = useRef(onExited);
onExitedRef.current = onExited;
const [mounted, setMounted] = useState(open);
const [phase, setPhase] = useState<'idle' | 'open' | 'closing'>('idle');
useLayoutEffect(() => {
if (open) setMounted(true);
}, [open]);
useLayoutEffect(() => {
if (!mounted) return;
const el = ref.current;
const parent = el?.parentElement;
const host = parent?.hasAttribute('data-state') ? parent : null;
if (host) {
const sync = () => {
setPhase(host.getAttribute('data-state') === 'closed' ? 'closing' : 'open');
};
if (host.getAttribute('data-state') === 'closed') {
setPhase('closing');
}
const observer = new MutationObserver(sync);
observer.observe(host, { attributes: true, attributeFilter: ['data-state'] });
return () => observer.disconnect();
}
setPhase(open ? 'idle' : 'closing');
}, [mounted, open]);
useEffect(() => {
if (!mounted || !open) return;
setPhase((current) => (current === 'closing' ? current : 'open'));
}, [mounted, open]);
useEffect(() => {
if (open || !mounted) return;
const finish = () => {
setMounted(false);
onExitedRef.current?.();
};
if (prefersReducedMotion()) {
finish();
return;
}
const el = ref.current;
let finished = false;
const once = () => {
if (finished) return;
finished = true;
finish();
};
const onEnd = (event: TransitionEvent) => {
if (event.target === el) once();
};
el?.addEventListener('transitionend', onEnd);
const timer = window.setTimeout(once, closeMs(className) + 50);
return () => {
el?.removeEventListener('transitionend', onEnd);
window.clearTimeout(timer);
};
}, [open, mounted, className]);
if (!mounted) return null;
return (
<div
ref={ref}
className={cn(
className,
phase === 'open' && 'is-open',
phase === 'closing' && 'is-closing',
)}
{...props}
>
{children}
</div>
);
}