54 lines
1.3 KiB
TypeScript
54 lines
1.3 KiB
TypeScript
|
|
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||
|
|
|
||
|
|
interface AnimatedPanelProps {
|
||
|
|
open: boolean;
|
||
|
|
children: ReactNode;
|
||
|
|
className?: string;
|
||
|
|
/** Duration in ms for the exit animation before unmounting. */
|
||
|
|
exitDuration?: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function AnimatedPanel({ open, children, className, exitDuration = 140 }: AnimatedPanelProps) {
|
||
|
|
const [mounted, setMounted] = useState(open);
|
||
|
|
const [visible, setVisible] = useState(open);
|
||
|
|
const timerRef = useRef<number | null>(null);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (open) {
|
||
|
|
if (timerRef.current) {
|
||
|
|
window.clearTimeout(timerRef.current);
|
||
|
|
timerRef.current = null;
|
||
|
|
}
|
||
|
|
setMounted(true);
|
||
|
|
requestAnimationFrame(() => {
|
||
|
|
requestAnimationFrame(() => {
|
||
|
|
setVisible(true);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
setVisible(false);
|
||
|
|
timerRef.current = window.setTimeout(() => {
|
||
|
|
setMounted(false);
|
||
|
|
timerRef.current = null;
|
||
|
|
}, exitDuration);
|
||
|
|
}
|
||
|
|
}, [open, exitDuration]);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
return () => {
|
||
|
|
if (timerRef.current) {
|
||
|
|
window.clearTimeout(timerRef.current);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
if (!mounted) return null;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
className={`${className ?? ""} animated-panel${visible ? " is-visible" : ""}`}
|
||
|
|
>
|
||
|
|
{children}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|