import * as React from "react"; import { VisualState } from "./types"; import { useKBar } from "./useKBar"; import { useOuterClick } from "./utils"; interface KBarAnimatorProps { style?: React.CSSProperties; className?: string; } const appearanceAnimationKeyframes = [ { opacity: 0, transform: "scale(.99)", }, { opacity: 1, transform: "scale(1.01)" }, { opacity: 1, transform: "scale(1)" }, ]; const bumpAnimationKeyframes = [ { transform: "scale(1)", }, { transform: "scale(.98)", }, { transform: "scale(1)", }, ]; export const KBarAnimator: React.FC< React.PropsWithChildren > = ({ children, style, className }) => { const { visualState, currentRootActionId, query, options } = useKBar( (state) => ({ visualState: state.visualState, currentRootActionId: state.currentRootActionId, }) ); const outerRef = React.useRef(null); const innerRef = React.useRef(null); const enterMs = options?.animations?.enterMs || 0; const exitMs = options?.animations?.exitMs || 0; // Show/hide animation React.useEffect(() => { if (visualState === VisualState.showing) { return; } const duration = visualState === VisualState.animatingIn ? enterMs : exitMs; const element = outerRef.current; element?.animate(appearanceAnimationKeyframes, { duration, easing: // TODO: expose easing in options visualState === VisualState.animatingOut ? "ease-in" : "ease-out", direction: visualState === VisualState.animatingOut ? "reverse" : "normal", fill: "forwards", }); }, [options, visualState, enterMs, exitMs]); // Height animation const previousHeight = React.useRef(); React.useEffect(() => { // Only animate if we're actually showing if (visualState === VisualState.showing) { const outer = outerRef.current; const inner = innerRef.current; if (!outer || !inner) { return; } const ro = new ResizeObserver((entries) => { for (let entry of entries) { const cr = entry.contentRect; if (!previousHeight.current) { previousHeight.current = cr.height; } outer.animate( [ { height: `${previousHeight.current}px`, }, { height: `${cr.height}px`, }, ], { duration: enterMs / 2, // TODO: expose configs here easing: "ease-out", fill: "forwards", } ); previousHeight.current = cr.height; } }); ro.observe(inner); return () => { ro.unobserve(inner); }; } }, [visualState, options, enterMs, exitMs]); // Bump animation between nested actions const firstRender = React.useRef(true); React.useEffect(() => { if (firstRender.current) { firstRender.current = false; return; } const element = outerRef.current; if (element) { element.animate(bumpAnimationKeyframes, { duration: enterMs, easing: "ease-out", }); } }, [currentRootActionId, enterMs]); useOuterClick(outerRef, () => { query.setVisualState(VisualState.animatingOut); options.callbacks?.onClose?.(); }); return (
{children}
); };