diff --git a/frontend/components/animate-ui/effects/motion-highlight.tsx b/frontend/components/animate-ui/effects/motion-highlight.tsx index 0c8d719f..6cd68ee3 100644 --- a/frontend/components/animate-ui/effects/motion-highlight.tsx +++ b/frontend/components/animate-ui/effects/motion-highlight.tsx @@ -188,6 +188,10 @@ function MotionHighlight({ else if (defaultValue !== undefined) setActiveValue(defaultValue); }, [value, defaultValue]); + React.useEffect(() => { + if (mode === 'parent' && !activeValue) clearBounds(); + }, [mode, activeValue, clearBounds]); + const id = React.useId(); React.useEffect(() => { @@ -368,7 +372,6 @@ function MotionHighlightItem({ setActiveValue, mode, setBounds, - clearBounds, hover, enabled, className: contextClassName, @@ -392,7 +395,8 @@ function MotionHighlightItem({ React.useEffect(() => { if (mode !== 'parent') return; - let rafId: number; + if (!isActive) return; + let rafId: number | null = null; let previousBounds: Bounds | null = null; const shouldUpdateBounds = forceUpdateBounds === true || @@ -421,18 +425,18 @@ function MotionHighlightItem({ setBounds(bounds); }; - if (isActive) { - updateBounds(); - setActiveClassName(activeClassName ?? ''); - } else if (!activeValue) clearBounds(); + updateBounds(); + setActiveClassName(activeClassName ?? ''); - if (shouldUpdateBounds) return () => cancelAnimationFrame(rafId); + if (shouldUpdateBounds) { + return () => { + if (rafId !== null) cancelAnimationFrame(rafId); + }; + } }, [ mode, isActive, - activeValue, setBounds, - clearBounds, activeClassName, setActiveClassName, forceUpdateBounds, diff --git a/frontend/components/animate-ui/radix/dialog.tsx b/frontend/components/animate-ui/radix/dialog.tsx index 16aaea7a..8c14b7e3 100644 --- a/frontend/components/animate-ui/radix/dialog.tsx +++ b/frontend/components/animate-ui/radix/dialog.tsx @@ -31,28 +31,30 @@ const useDialog = (): DialogContextType => { type DialogProps = React.ComponentProps; -function Dialog({children, ...props}: DialogProps) { - const [isOpen, setIsOpen] = React.useState( - props?.open ?? props?.defaultOpen ?? false, +function Dialog({children, open, defaultOpen, onOpenChange, ...props}: DialogProps) { + const [uncontrolledOpen, setUncontrolledOpen] = React.useState( + defaultOpen ?? false, ); - - React.useEffect(() => { - if (props?.open !== undefined) setIsOpen(props.open); - }, [props?.open]); + const isControlled = open !== undefined; + const isOpen = open ?? uncontrolledOpen; const handleOpenChange = React.useCallback( - (open: boolean) => { - setIsOpen(open); - props.onOpenChange?.(open); + (nextOpen: boolean) => { + if (!isControlled) setUncontrolledOpen(nextOpen); + onOpenChange?.(nextOpen); }, - [props], + [isControlled, onOpenChange], ); + const contextValue = React.useMemo(() => ({isOpen}), [isOpen]); + return ( - + {children} @@ -86,7 +88,7 @@ function DialogOverlay({className, ...props}: DialogOverlayProps) { { - getActiveValue(); + const container = localRef.current; + if (!container) return; + const frameId = requestAnimationFrame(getActiveValue); const observer = new MutationObserver(getActiveValue); - if (localRef.current) { - observer.observe(localRef.current, { - attributes: true, - childList: true, - subtree: true, - }); - } + observer.observe(container, { + attributes: true, + attributeFilter: ['data-state'], + childList: true, + subtree: true, + }); return () => { + cancelAnimationFrame(frameId); observer.disconnect(); }; }, [getActiveValue]); @@ -193,10 +195,13 @@ function TabsContents({ React.useEffect(() => { if (!containerRef.current) return; + let frameId: number | null = null; const resizeObserver = new ResizeObserver((entries) => { const newHeight = entries?.[0]?.contentRect.height; if (!newHeight) return; - requestAnimationFrame(() => { + if (frameId !== null) cancelAnimationFrame(frameId); + frameId = requestAnimationFrame(() => { + frameId = null; setHeight(newHeight); }); }); @@ -205,6 +210,7 @@ function TabsContents({ return () => { resizeObserver.disconnect(); + if (frameId !== null) cancelAnimationFrame(frameId); }; }, [children]); diff --git a/frontend/components/common/markdown/Editor.tsx b/frontend/components/common/markdown/Editor.tsx index 5daf90c5..b837499e 100644 --- a/frontend/components/common/markdown/Editor.tsx +++ b/frontend/components/common/markdown/Editor.tsx @@ -1,6 +1,6 @@ 'use client'; -import {useState, useRef, useCallback, useEffect} from 'react'; +import {useState, useRef, useCallback, useEffect, useMemo} from 'react'; import {Button} from '@/components/ui/button'; import {Textarea} from '@/components/ui/textarea'; import {Tabs, TabsList, TabsTrigger} from '@/components/ui/tabs'; @@ -37,7 +37,7 @@ export function MarkdownEditor({ const [displayLineCount, setDisplayLineCount] = useState(1); const [lineMapping, setLineMapping] = useState>([]); - const lines = value.split('\n'); + const lines = useMemo(() => value.split('\n'), [value]); const calculateLineMapping = useCallback(() => { if (!textareaRef.current || lines.length === 0) { return [{logicalLine: 1, isFirstLineOfLogicalLine: true}]; @@ -109,32 +109,42 @@ export function MarkdownEditor({ const handleInputChange = useCallback((e: React.ChangeEvent) => { onChange(e.target.value); - - setTimeout(() => { - const newLineMapping = calculateLineMapping(); - setLineMapping(newLineMapping); - setDisplayLineCount(newLineMapping.length); - }, 0); - }, [onChange, calculateLineMapping]); + }, [onChange]); useEffect(() => { + let frameId: number | null = null; const updateDisplayLines = () => { const newLineMapping = calculateLineMapping(); - setLineMapping(newLineMapping); - setDisplayLineCount(newLineMapping.length); + setLineMapping((current) => { + const unchanged = current.length === newLineMapping.length && + current.every((line, index) => + line.logicalLine === newLineMapping[index]?.logicalLine && + line.isFirstLineOfLogicalLine === newLineMapping[index]?.isFirstLineOfLogicalLine, + ); + return unchanged ? current : newLineMapping; + }); + setDisplayLineCount((current) => + current === newLineMapping.length ? current : newLineMapping.length, + ); + }; + const scheduleUpdate = () => { + if (frameId !== null) cancelAnimationFrame(frameId); + frameId = requestAnimationFrame(() => { + frameId = null; + updateDisplayLines(); + }); }; if (textareaRef.current) { - setTimeout(updateDisplayLines, 0); + scheduleUpdate(); - const resizeObserver = new ResizeObserver(() => { - setTimeout(updateDisplayLines, 0); - }); + const resizeObserver = new ResizeObserver(scheduleUpdate); resizeObserver.observe(textareaRef.current); return () => { resizeObserver.disconnect(); + if (frameId !== null) cancelAnimationFrame(frameId); }; } }, [calculateLineMapping]); @@ -150,7 +160,8 @@ export function MarkdownEditor({ } }; - setTimeout(syncStyles, 0); + const frameId = requestAnimationFrame(syncStyles); + return () => cancelAnimationFrame(frameId); } }, [value, displayLineCount]);