authorgravatar for 71498452+StiensWout@users.noreply.github.comWout Stiens <71498452+StiensWout@users.noreply.github.com> 2026-08-05 11:19:58+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-05 11:19:58+02:00
log9235c83eb7f16c321ad02aa4cc502fbe0be252c1
tree96662d092b5d6eca3b1704eff23a31bde3655fe8
parente0c85a20ef0ec9b49ade5989a13850493036a49b
signature Signed by PGP key B5690EEEBB952194

fix(web): keep the composer command menu anchored to the composer (#5336)


1 files changed, 32 insertions(+), 9 deletions(-)

apps/web/src/components/chat/ChatComposer.tsx+32-9
......@@ -108,13 +108,24 @@ import { basenameOfPath } from "../../pierre-icons";
108108import { cn, randomUUID } from "~/lib/utils";
109109import { Separator } from "../ui/separator";
110110
111type ComposerCommandMenuPosition = {
112 bottom: number;
113 left: number;
114 maxHeight: number;
115 width: number;
116};
117
118function composerCommandMenuPositionsEqual(
119 a: ComposerCommandMenuPosition,
120 b: ComposerCommandMenuPosition,
121): boolean {
122 return (
123 a.bottom === b.bottom && a.left === b.left && a.maxHeight === b.maxHeight && a.width === b.width
124 );
125}
126
111127function ComposerCommandMenuLayer(props: { anchor: HTMLElement | null; children: ReactNode }) {
112 const [position, setPosition] = useState<{
113 bottom: number;
114 left: number;
115 maxHeight: number;
116 width: number;
117 } | null>(null);
128 const [position, setPosition] = useState<ComposerCommandMenuPosition | null>(null);
118129
119130 useLayoutEffect(() => {
120131 const anchor = props.anchor;
......@@ -125,12 +136,15 @@ function ComposerCommandMenuLayer(props: { anchor: HTMLElement | null; children:
125136
126137 const updatePosition = () => {
127138 const rect = anchor.getBoundingClientRect();
128 setPosition({
139 const next = {
129140 bottom: window.innerHeight - rect.top + 8,
130141 left: rect.left,
131142 maxHeight: Math.max(96, rect.top - 24),
132143 width: rect.width,
133 });
144 };
145 setPosition((current) =>
146 current && composerCommandMenuPositionsEqual(current, next) ? current : next,
147 );
134148 };
135149
136150 updatePosition();
......@@ -139,7 +153,16 @@ function ComposerCommandMenuLayer(props: { anchor: HTMLElement | null; children:
139153
140154 const observer =
141155 typeof ResizeObserver === "undefined" ? null : new ResizeObserver(updatePosition);
142 observer?.observe(anchor);
156 if (observer) {
157 // The composer is centered and capped at a max width, so opening a side
158 // panel slides it sideways without ever resizing it. Watching the anchor
159 // alone would leave the menu behind; the ancestors are what shrink, and
160 // they resize on every frame of the panel animation.
161 observer.observe(anchor);
162 for (let element = anchor.parentElement; element; element = element.parentElement) {
163 observer.observe(element);
164 }
165 }
143166
144167 return () => {
145168 observer?.disconnect();