fix(ui): constrain draft menus to chat area
This commit is contained in:
@@ -2342,9 +2342,11 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
side="top"
|
side="top"
|
||||||
className="w-[min(380px,calc(100vw-2rem))] max-h-[calc(100dvh-var(--oc-header-height,56px)-0.5rem)] p-0 flex flex-col"
|
className="w-[min(380px,calc(100vw-2rem))] p-0 flex flex-col overflow-hidden"
|
||||||
align="end"
|
align="end"
|
||||||
alignOffset={-40}
|
alignOffset={-40}
|
||||||
|
constrainToMain
|
||||||
|
collisionAvoidance={{ side: 'none', align: 'shift' }}
|
||||||
onKeyDownCapture={handleModelShortcutKeyDownCapture}
|
onKeyDownCapture={handleModelShortcutKeyDownCapture}
|
||||||
>
|
>
|
||||||
<div className="p-1 border-b border-border/40">
|
<div className="p-1 border-b border-border/40">
|
||||||
@@ -2401,7 +2403,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
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}
|
tooltipsEnabled={agentMenuOpen}
|
||||||
onEscape={() => setAgentMenuOpen(false)}
|
onEscape={() => setAgentMenuOpen(false)}
|
||||||
/>
|
/>
|
||||||
@@ -2735,7 +2737,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<DropdownMenuContent side="top" align="end" alignOffset={-40} className="w-[min(280px,calc(100vw-2rem))] max-h-[calc(100dvh-var(--oc-header-height,56px)-0.5rem)] p-0 flex flex-col">
|
<DropdownMenuContent side="top" align="end" alignOffset={-40} constrainToMain collisionAvoidance={{ side: 'none', align: 'shift' }} className="w-[min(280px,calc(100vw-2rem))] p-0 flex flex-col overflow-hidden">
|
||||||
<div className="p-2 border-b border-border/40">
|
<div className="p-2 border-b border-border/40">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Icon name="search" className="absolute left-2.5 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground" />
|
<Icon name="search" className="absolute left-2.5 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground" />
|
||||||
@@ -2751,7 +2753,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ScrollableOverlay outerClassName="max-h-[min(400px,calc(100dvh-var(--oc-header-height,56px)-4rem))] flex-1">
|
<ScrollableOverlay outerClassName="max-h-[min(400px,calc(var(--available-height)-4rem))] flex-1">
|
||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
{!agentSearchQuery.trim() && defaultAgentName && (
|
{!agentSearchQuery.trim() && defaultAgentName && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export function DraftTargetSelectors(props: DraftTargetProps) {
|
|||||||
: <ProjectLabel project={selectedProject} theme={theme} />}
|
: <ProjectLabel project={selectedProject} theme={theme} />}
|
||||||
</SelectValue>
|
</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent side="top" collisionAvoidance={{ side: 'none' }} fitContent>
|
<SelectContent side="top" collisionAvoidance={{ side: 'none' }} constrainToMain fitContent>
|
||||||
{projects.map((project) => (
|
{projects.map((project) => (
|
||||||
<SelectItem key={project.id} value={project.id} className="max-w-[24rem] truncate">
|
<SelectItem key={project.id} value={project.id} className="max-w-[24rem] truncate">
|
||||||
<ProjectLabel project={project} theme={theme} />
|
<ProjectLabel project={project} theme={theme} />
|
||||||
@@ -144,7 +144,7 @@ export function DraftTargetSelectors(props: DraftTargetProps) {
|
|||||||
{selectedBranchLabel ?? t('chat.chatInput.branch')}
|
{selectedBranchLabel ?? t('chat.chatInput.branch')}
|
||||||
</SelectValue>
|
</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent side="top" collisionAvoidance={{ side: 'none' }} className="w-max min-w-48">
|
<SelectContent side="top" collisionAvoidance={{ side: 'none' }} constrainToMain className="w-max min-w-48">
|
||||||
{projectRootBranchOption ? (
|
{projectRootBranchOption ? (
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
<SelectLabel>{t('chat.chatInput.projectRoot')}</SelectLabel>
|
<SelectLabel>{t('chat.chatInput.projectRoot')}</SelectLabel>
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ type AsChildRenderProps = {
|
|||||||
|
|
||||||
type DropdownPortalContextValue = {
|
type DropdownPortalContextValue = {
|
||||||
portalContainer: HTMLElement | null;
|
portalContainer: HTMLElement | null;
|
||||||
|
collisionBoundary: Element | null;
|
||||||
setPortalContainer: (container: HTMLElement | null) => void;
|
setPortalContainer: (container: HTMLElement | null) => void;
|
||||||
|
setCollisionBoundary: (boundary: Element | null) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DropdownPortalContext = React.createContext<DropdownPortalContextValue | null>(null);
|
const DropdownPortalContext = React.createContext<DropdownPortalContextValue | null>(null);
|
||||||
@@ -36,10 +38,13 @@ function DropdownMenu({
|
|||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof BaseMenu.Root>) {
|
}: React.ComponentProps<typeof BaseMenu.Root>) {
|
||||||
const [portalContainer, setPortalContainer] = React.useState<HTMLElement | null>(null);
|
const [portalContainer, setPortalContainer] = React.useState<HTMLElement | null>(null);
|
||||||
|
const [collisionBoundary, setCollisionBoundary] = React.useState<Element | null>(null);
|
||||||
const portalContextValue = React.useMemo<DropdownPortalContextValue>(() => ({
|
const portalContextValue = React.useMemo<DropdownPortalContextValue>(() => ({
|
||||||
portalContainer,
|
portalContainer,
|
||||||
|
collisionBoundary,
|
||||||
setPortalContainer,
|
setPortalContainer,
|
||||||
}), [portalContainer]);
|
setCollisionBoundary,
|
||||||
|
}), [collisionBoundary, portalContainer]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownPortalContext.Provider value={portalContextValue}>
|
<DropdownPortalContext.Provider value={portalContextValue}>
|
||||||
@@ -62,6 +67,7 @@ function DropdownMenuTrigger({
|
|||||||
}
|
}
|
||||||
const element = target instanceof HTMLElement ? target : null;
|
const element = target instanceof HTMLElement ? target : null;
|
||||||
portalContext.setPortalContainer(resolveDialogContainer(element));
|
portalContext.setPortalContainer(resolveDialogContainer(element));
|
||||||
|
portalContext.setCollisionBoundary(element?.closest('main') ?? null);
|
||||||
}, [portalContext]);
|
}, [portalContext]);
|
||||||
|
|
||||||
const r = renderFromAsChild(asChild, children);
|
const r = renderFromAsChild(asChild, children);
|
||||||
@@ -89,6 +95,8 @@ type ContentProps = {
|
|||||||
alignOffset?: number;
|
alignOffset?: number;
|
||||||
portalToBody?: boolean;
|
portalToBody?: boolean;
|
||||||
positionerClassName?: string;
|
positionerClassName?: string;
|
||||||
|
constrainToMain?: boolean;
|
||||||
|
collisionAvoidance?: React.ComponentProps<typeof BaseMenu.Positioner>["collisionAvoidance"];
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
className?: string;
|
className?: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
@@ -103,6 +111,8 @@ function DropdownMenuContent({
|
|||||||
alignOffset,
|
alignOffset,
|
||||||
portalToBody = false,
|
portalToBody = false,
|
||||||
positionerClassName,
|
positionerClassName,
|
||||||
|
constrainToMain = false,
|
||||||
|
collisionAvoidance,
|
||||||
style,
|
style,
|
||||||
children,
|
children,
|
||||||
onCloseAutoFocus,
|
onCloseAutoFocus,
|
||||||
@@ -118,6 +128,8 @@ function DropdownMenuContent({
|
|||||||
align={align}
|
align={align}
|
||||||
side={side}
|
side={side}
|
||||||
alignOffset={alignOffset}
|
alignOffset={alignOffset}
|
||||||
|
collisionBoundary={constrainToMain ? portalContext?.collisionBoundary ?? undefined : undefined}
|
||||||
|
collisionAvoidance={collisionAvoidance}
|
||||||
className={cn("app-region-no-drag z-50", positionerClassName)}
|
className={cn("app-region-no-drag z-50", positionerClassName)}
|
||||||
>
|
>
|
||||||
<BaseMenu.Popup
|
<BaseMenu.Popup
|
||||||
@@ -128,7 +140,6 @@ function DropdownMenuContent({
|
|||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
dropdownMenuPopupClass,
|
dropdownMenuPopupClass,
|
||||||
"max-h-[calc(100dvh-var(--oc-header-height,56px)-0.5rem)] overflow-y-auto",
|
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ type AsChildRenderProps = {
|
|||||||
|
|
||||||
type SelectPortalContextValue = {
|
type SelectPortalContextValue = {
|
||||||
portalContainer: HTMLElement | null;
|
portalContainer: HTMLElement | null;
|
||||||
|
collisionBoundary: Element | null;
|
||||||
setPortalContainer: (container: HTMLElement | null) => void;
|
setPortalContainer: (container: HTMLElement | null) => void;
|
||||||
|
setCollisionBoundary: (boundary: Element | null) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SelectPortalContext = React.createContext<SelectPortalContextValue | null>(null);
|
const SelectPortalContext = React.createContext<SelectPortalContextValue | null>(null);
|
||||||
@@ -44,10 +46,13 @@ function Select<Value extends string = string>({
|
|||||||
...props
|
...props
|
||||||
}: SelectRootProps<Value>) {
|
}: SelectRootProps<Value>) {
|
||||||
const [portalContainer, setPortalContainer] = React.useState<HTMLElement | null>(null);
|
const [portalContainer, setPortalContainer] = React.useState<HTMLElement | null>(null);
|
||||||
|
const [collisionBoundary, setCollisionBoundary] = React.useState<Element | null>(null);
|
||||||
const portalContextValue = React.useMemo<SelectPortalContextValue>(() => ({
|
const portalContextValue = React.useMemo<SelectPortalContextValue>(() => ({
|
||||||
portalContainer,
|
portalContainer,
|
||||||
|
collisionBoundary,
|
||||||
setPortalContainer,
|
setPortalContainer,
|
||||||
}), [portalContainer]);
|
setCollisionBoundary,
|
||||||
|
}), [collisionBoundary, portalContainer]);
|
||||||
|
|
||||||
const handleValueChange = React.useCallback(
|
const handleValueChange = React.useCallback(
|
||||||
(value: unknown, eventDetails: SelectRootChangeEventDetails) => {
|
(value: unknown, eventDetails: SelectRootChangeEventDetails) => {
|
||||||
@@ -119,6 +124,7 @@ function SelectTrigger({
|
|||||||
}
|
}
|
||||||
const element = target instanceof HTMLElement ? target : null;
|
const element = target instanceof HTMLElement ? target : null;
|
||||||
portalContext.setPortalContainer(resolveDialogContainer(element));
|
portalContext.setPortalContainer(resolveDialogContainer(element));
|
||||||
|
portalContext.setCollisionBoundary(element?.closest('main') ?? null);
|
||||||
}, [portalContext]);
|
}, [portalContext]);
|
||||||
|
|
||||||
const asChildRender: AsChildRenderProps | null = asChild && React.isValidElement(children)
|
const asChildRender: AsChildRenderProps | null = asChild && React.isValidElement(children)
|
||||||
@@ -164,6 +170,7 @@ type SelectContentExtra = {
|
|||||||
side?: "top" | "right" | "bottom" | "left";
|
side?: "top" | "right" | "bottom" | "left";
|
||||||
align?: "start" | "center" | "end";
|
align?: "start" | "center" | "end";
|
||||||
collisionAvoidance?: React.ComponentProps<typeof BaseSelect.Positioner>["collisionAvoidance"];
|
collisionAvoidance?: React.ComponentProps<typeof BaseSelect.Positioner>["collisionAvoidance"];
|
||||||
|
constrainToMain?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function SelectContent({
|
function SelectContent({
|
||||||
@@ -176,6 +183,7 @@ function SelectContent({
|
|||||||
side,
|
side,
|
||||||
align,
|
align,
|
||||||
collisionAvoidance,
|
collisionAvoidance,
|
||||||
|
constrainToMain = false,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof BaseSelect.Popup> & SelectContentExtra) {
|
}: React.ComponentProps<typeof BaseSelect.Popup> & SelectContentExtra) {
|
||||||
const portalContext = React.useContext(SelectPortalContext);
|
const portalContext = React.useContext(SelectPortalContext);
|
||||||
@@ -190,6 +198,7 @@ function SelectContent({
|
|||||||
side={side}
|
side={side}
|
||||||
align={align}
|
align={align}
|
||||||
collisionAvoidance={collisionAvoidance}
|
collisionAvoidance={collisionAvoidance}
|
||||||
|
collisionBoundary={constrainToMain ? portalContext?.collisionBoundary ?? undefined : undefined}
|
||||||
className="absolute z-[120] pointer-events-auto"
|
className="absolute z-[120] pointer-events-auto"
|
||||||
>
|
>
|
||||||
<BaseSelect.Popup
|
<BaseSelect.Popup
|
||||||
@@ -198,7 +207,7 @@ function SelectContent({
|
|||||||
color: 'var(--surface-elevated-foreground)',
|
color: 'var(--surface-elevated-foreground)',
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"oc-glass-popover oc-glass-floating pointer-events-auto transition-all duration-150 ease-out data-[starting-style]:opacity-0 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[ending-style]:scale-95 relative z-[120] max-h-[min(var(--available-height),calc(100dvh-var(--oc-header-height,56px)-0.5rem))] min-w-[8rem] origin-[var(--transform-origin)] overflow-x-hidden rounded-xl",
|
"oc-glass-popover oc-glass-floating pointer-events-auto transition-all duration-150 ease-out data-[starting-style]:opacity-0 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[ending-style]:scale-95 relative z-[120] max-h-[var(--available-height)] min-w-[8rem] origin-[var(--transform-origin)] overflow-x-hidden rounded-xl",
|
||||||
!alignItemWithTrigger &&
|
!alignItemWithTrigger &&
|
||||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
fitContent && "w-max min-w-0",
|
fitContent && "w-max min-w-0",
|
||||||
@@ -208,7 +217,7 @@ function SelectContent({
|
|||||||
>
|
>
|
||||||
<ScrollableOverlay
|
<ScrollableOverlay
|
||||||
outerClassName={cn(
|
outerClassName={cn(
|
||||||
"max-h-[min(var(--available-height),calc(100dvh-var(--oc-header-height,56px)-0.5rem))]",
|
"max-h-[var(--available-height)]",
|
||||||
fitContent ? "w-max" : "w-full"
|
fitContent ? "w-max" : "w-full"
|
||||||
)}
|
)}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
Reference in New Issue
Block a user