Upgrade React 18.3.1 → 19.2.4 with full type compatibility. Changes: - react, react-dom → ^19.0.0 - @types/react, @types/react-dom → ^19.0.0 - lucide-react → ^0.577.0 (React 19 type fixes) - @tiptap/* → ^3.20.4 (React 19 support) - useRef calls now require explicit initial value (undefined) - RefObject types updated for React 19 (includes null) - MutableRefObject → RefObject (deprecated in 19) - act() import moved from react-dom/test-utils to react - Scoped JSX namespace imports added where needed
117 lines
3.5 KiB
TypeScript
117 lines
3.5 KiB
TypeScript
import { type JSX, useCallback, useEffect, useRef, useState } from 'react';
|
|
|
|
import type { CSSProperties, MutableRefObject, PropsWithChildren, Ref } from 'react';
|
|
|
|
export const ENTRY_REVEAL_ANIMATION_MS = 700;
|
|
export const ENTRY_REVEAL_EASING = 'cubic-bezier(0.22, 1, 0.36, 1)';
|
|
|
|
interface AnimatedHeightRevealProps extends PropsWithChildren {
|
|
animate?: boolean;
|
|
className?: string;
|
|
style?: CSSProperties;
|
|
containerRef?: Ref<HTMLDivElement>;
|
|
}
|
|
|
|
function assignRef<T>(ref: Ref<T> | undefined, value: T | null): void {
|
|
if (!ref) return;
|
|
if (typeof ref === 'function') {
|
|
ref(value);
|
|
return;
|
|
}
|
|
const mutableRef = ref as MutableRefObject<T | null>;
|
|
mutableRef.current = value;
|
|
}
|
|
|
|
export const AnimatedHeightReveal = ({
|
|
animate,
|
|
className,
|
|
style,
|
|
containerRef,
|
|
children,
|
|
}: AnimatedHeightRevealProps): JSX.Element => {
|
|
const [shouldAnimateOnMount] = useState(() => Boolean(animate));
|
|
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
|
const animationFrameRef = useRef<number | null>(null);
|
|
const [prefersReducedMotion] = useState(
|
|
() => window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
);
|
|
const [isExpanded, setIsExpanded] = useState(
|
|
() => !animate || window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
);
|
|
// Overflow must stay hidden during the height transition so the grid clip
|
|
// actually works. Switch to visible only after the animation completes.
|
|
const [overflowVisible, setOverflowVisible] = useState(
|
|
() => !animate || window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
);
|
|
|
|
const setWrapperRef = useCallback(
|
|
(node: HTMLDivElement | null) => {
|
|
wrapperRef.current = node;
|
|
assignRef(containerRef, node);
|
|
},
|
|
[containerRef]
|
|
);
|
|
|
|
const clearPendingAnimation = useCallback(() => {
|
|
if (animationFrameRef.current !== null) {
|
|
cancelAnimationFrame(animationFrameRef.current);
|
|
animationFrameRef.current = null;
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!shouldAnimateOnMount || prefersReducedMotion) {
|
|
return;
|
|
}
|
|
|
|
animationFrameRef.current = requestAnimationFrame(() => {
|
|
animationFrameRef.current = requestAnimationFrame(() => {
|
|
setIsExpanded(true);
|
|
animationFrameRef.current = null;
|
|
});
|
|
});
|
|
|
|
// Switch overflow to visible after the height transition finishes
|
|
// so popovers/tooltips inside can render outside bounds.
|
|
const overflowTimer = setTimeout(() => {
|
|
setOverflowVisible(true);
|
|
}, ENTRY_REVEAL_ANIMATION_MS + 50);
|
|
|
|
return () => {
|
|
clearPendingAnimation();
|
|
clearTimeout(overflowTimer);
|
|
};
|
|
}, [clearPendingAnimation, shouldAnimateOnMount, prefersReducedMotion]);
|
|
|
|
useEffect(
|
|
() => () => {
|
|
clearPendingAnimation();
|
|
},
|
|
[clearPendingAnimation]
|
|
);
|
|
|
|
const shouldTransition = shouldAnimateOnMount && !prefersReducedMotion && isExpanded;
|
|
|
|
return (
|
|
<div
|
|
ref={setWrapperRef}
|
|
className={className}
|
|
style={{
|
|
display: 'grid',
|
|
gridTemplateRows: isExpanded ? '1fr' : '0fr',
|
|
opacity: isExpanded ? 1 : 0,
|
|
transition: shouldTransition
|
|
? [
|
|
`grid-template-rows ${ENTRY_REVEAL_ANIMATION_MS}ms ${ENTRY_REVEAL_EASING}`,
|
|
`opacity ${ENTRY_REVEAL_ANIMATION_MS}ms ease`,
|
|
].join(', ')
|
|
: undefined,
|
|
...style,
|
|
}}
|
|
>
|
|
<div style={{ minHeight: 0, minWidth: 0, overflow: overflowVisible ? 'visible' : 'hidden' }}>
|
|
{children}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|