diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index c26f52cbc0c..201fb79c566 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -108,13 +108,24 @@ import { basenameOfPath } from "../../pierre-icons"; import { cn, randomUUID } from "~/lib/utils"; import { Separator } from "../ui/separator"; +type ComposerCommandMenuPosition = { + bottom: number; + left: number; + maxHeight: number; + width: number; +}; + +function composerCommandMenuPositionsEqual( + a: ComposerCommandMenuPosition, + b: ComposerCommandMenuPosition, +): boolean { + return ( + a.bottom === b.bottom && a.left === b.left && a.maxHeight === b.maxHeight && a.width === b.width + ); +} + function ComposerCommandMenuLayer(props: { anchor: HTMLElement | null; children: ReactNode }) { - const [position, setPosition] = useState<{ - bottom: number; - left: number; - maxHeight: number; - width: number; - } | null>(null); + const [position, setPosition] = useState(null); useLayoutEffect(() => { const anchor = props.anchor; @@ -125,12 +136,15 @@ function ComposerCommandMenuLayer(props: { anchor: HTMLElement | null; children: const updatePosition = () => { const rect = anchor.getBoundingClientRect(); - setPosition({ + const next = { bottom: window.innerHeight - rect.top + 8, left: rect.left, maxHeight: Math.max(96, rect.top - 24), width: rect.width, - }); + }; + setPosition((current) => + current && composerCommandMenuPositionsEqual(current, next) ? current : next, + ); }; updatePosition(); @@ -139,7 +153,16 @@ function ComposerCommandMenuLayer(props: { anchor: HTMLElement | null; children: const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(updatePosition); - observer?.observe(anchor); + if (observer) { + // The composer is centered and capped at a max width, so opening a side + // panel slides it sideways without ever resizing it. Watching the anchor + // alone would leave the menu behind; the ancestors are what shrink, and + // they resize on every frame of the panel animation. + observer.observe(anchor); + for (let element = anchor.parentElement; element; element = element.parentElement) { + observer.observe(element); + } + } return () => { observer?.disconnect();