From 264ea02ee115f4633d0f75ee835a723b9ae0fd67 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Sun, 23 Aug 2026 18:27:35 +0300 Subject: [PATCH] fix(ui): constrain draft menus to chat area --- packages/ui/src/components/chat/ModelControls.tsx | 10 ++++++---- .../chat/composer/ui/DraftTargetSelectors.tsx | 4 ++-- packages/ui/src/components/ui/dropdown-menu.tsx | 15 +++++++++++++-- packages/ui/src/components/ui/select.tsx | 15 ++++++++++++--- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/packages/ui/src/components/chat/ModelControls.tsx b/packages/ui/src/components/chat/ModelControls.tsx index 80d547b2..a290fde8 100644 --- a/packages/ui/src/components/chat/ModelControls.tsx +++ b/packages/ui/src/components/chat/ModelControls.tsx @@ -2342,9 +2342,11 @@ export const ModelControls: React.FC = ({
@@ -2401,7 +2403,7 @@ export const ModelControls: React.FC = ({
); }} - maxHeightClassName="max-h-[min(400px,calc(100dvh-var(--oc-header-height,56px)-4rem))] flex-1" + maxHeightClassName="max-h-[min(400px,calc(var(--available-height)-4rem))] flex-1" tooltipsEnabled={agentMenuOpen} onEscape={() => setAgentMenuOpen(false)} /> @@ -2735,7 +2737,7 @@ export const ModelControls: React.FC = ({ - +
@@ -2751,7 +2753,7 @@ export const ModelControls: React.FC = ({ />
- +
{!agentSearchQuery.trim() && defaultAgentName && ( <> diff --git a/packages/ui/src/components/chat/composer/ui/DraftTargetSelectors.tsx b/packages/ui/src/components/chat/composer/ui/DraftTargetSelectors.tsx index 8c44d434..c6fba04c 100644 --- a/packages/ui/src/components/chat/composer/ui/DraftTargetSelectors.tsx +++ b/packages/ui/src/components/chat/composer/ui/DraftTargetSelectors.tsx @@ -122,7 +122,7 @@ export function DraftTargetSelectors(props: DraftTargetProps) { : } - + {projects.map((project) => ( @@ -144,7 +144,7 @@ export function DraftTargetSelectors(props: DraftTargetProps) { {selectedBranchLabel ?? t('chat.chatInput.branch')} - + {projectRootBranchOption ? ( {t('chat.chatInput.projectRoot')} diff --git a/packages/ui/src/components/ui/dropdown-menu.tsx b/packages/ui/src/components/ui/dropdown-menu.tsx index 495ff6bc..e8fd327f 100644 --- a/packages/ui/src/components/ui/dropdown-menu.tsx +++ b/packages/ui/src/components/ui/dropdown-menu.tsx @@ -13,7 +13,9 @@ type AsChildRenderProps = { type DropdownPortalContextValue = { portalContainer: HTMLElement | null; + collisionBoundary: Element | null; setPortalContainer: (container: HTMLElement | null) => void; + setCollisionBoundary: (boundary: Element | null) => void; }; const DropdownPortalContext = React.createContext(null); @@ -36,10 +38,13 @@ function DropdownMenu({ ...props }: React.ComponentProps) { const [portalContainer, setPortalContainer] = React.useState(null); + const [collisionBoundary, setCollisionBoundary] = React.useState(null); const portalContextValue = React.useMemo(() => ({ portalContainer, + collisionBoundary, setPortalContainer, - }), [portalContainer]); + setCollisionBoundary, + }), [collisionBoundary, portalContainer]); return ( @@ -62,6 +67,7 @@ function DropdownMenuTrigger({ } const element = target instanceof HTMLElement ? target : null; portalContext.setPortalContainer(resolveDialogContainer(element)); + portalContext.setCollisionBoundary(element?.closest('main') ?? null); }, [portalContext]); const r = renderFromAsChild(asChild, children); @@ -89,6 +95,8 @@ type ContentProps = { alignOffset?: number; portalToBody?: boolean; positionerClassName?: string; + constrainToMain?: boolean; + collisionAvoidance?: React.ComponentProps["collisionAvoidance"]; style?: React.CSSProperties; className?: string; children?: React.ReactNode; @@ -103,6 +111,8 @@ function DropdownMenuContent({ alignOffset, portalToBody = false, positionerClassName, + constrainToMain = false, + collisionAvoidance, style, children, onCloseAutoFocus, @@ -118,6 +128,8 @@ function DropdownMenuContent({ align={align} side={side} alignOffset={alignOffset} + collisionBoundary={constrainToMain ? portalContext?.collisionBoundary ?? undefined : undefined} + collisionAvoidance={collisionAvoidance} className={cn("app-region-no-drag z-50", positionerClassName)} > void; + setCollisionBoundary: (boundary: Element | null) => void; }; const SelectPortalContext = React.createContext(null); @@ -44,10 +46,13 @@ function Select({ ...props }: SelectRootProps) { const [portalContainer, setPortalContainer] = React.useState(null); + const [collisionBoundary, setCollisionBoundary] = React.useState(null); const portalContextValue = React.useMemo(() => ({ portalContainer, + collisionBoundary, setPortalContainer, - }), [portalContainer]); + setCollisionBoundary, + }), [collisionBoundary, portalContainer]); const handleValueChange = React.useCallback( (value: unknown, eventDetails: SelectRootChangeEventDetails) => { @@ -119,6 +124,7 @@ function SelectTrigger({ } const element = target instanceof HTMLElement ? target : null; portalContext.setPortalContainer(resolveDialogContainer(element)); + portalContext.setCollisionBoundary(element?.closest('main') ?? null); }, [portalContext]); const asChildRender: AsChildRenderProps | null = asChild && React.isValidElement(children) @@ -164,6 +170,7 @@ type SelectContentExtra = { side?: "top" | "right" | "bottom" | "left"; align?: "start" | "center" | "end"; collisionAvoidance?: React.ComponentProps["collisionAvoidance"]; + constrainToMain?: boolean; }; function SelectContent({ @@ -176,6 +183,7 @@ function SelectContent({ side, align, collisionAvoidance, + constrainToMain = false, ...props }: React.ComponentProps & SelectContentExtra) { const portalContext = React.useContext(SelectPortalContext); @@ -190,6 +198,7 @@ function SelectContent({ side={side} align={align} collisionAvoidance={collisionAvoidance} + collisionBoundary={constrainToMain ? portalContext?.collisionBoundary ?? undefined : undefined} className="absolute z-[120] pointer-events-auto" >