Skip to content

Commit 93c016b

Browse files
committed
fix: use portal for command suggestions in creation mode
The CommandSuggestions popup was being clipped by overflow:hidden on parent containers in creation mode. In workspace mode, ChatInput is positioned outside the overflow-hidden scroll container. Solution: Pass anchorRef to CommandSuggestions when in creation mode. When anchorRef is provided, the component uses createPortal to render to document.body with fixed positioning relative to the anchor element. This escapes the overflow:hidden containers and ensures suggestions are visible in both variants.
1 parent fddea6c commit 93c016b

File tree

3 files changed

+67
-17
lines changed

3 files changed

+67
-17
lines changed

src/browser/components/ChatInput/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -781,13 +781,15 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
781781
/>
782782

783783
{/* Command suggestions - available in both variants */}
784+
{/* In creation mode, use portal (anchorRef) to escape overflow:hidden containers */}
784785
<CommandSuggestions
785786
suggestions={commandSuggestions}
786787
onSelectSuggestion={handleCommandSelect}
787788
onDismiss={() => setShowCommandSuggestions(false)}
788789
isVisible={showCommandSuggestions}
789790
ariaLabel="Slash command suggestions"
790791
listId={commandListId}
792+
anchorRef={variant === "creation" ? inputRef : undefined}
791793
/>
792794

793795
<div className="flex items-end" data-component="ChatInputControls">

src/browser/components/CommandSuggestions.tsx

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import React, { useState, useEffect } from "react";
1+
import React, { useState, useEffect, useRef, useLayoutEffect } from "react";
2+
import { createPortal } from "react-dom";
23
import { cn } from "@/common/lib/utils";
34
import type { SlashSuggestion } from "@/browser/utils/slashCommands/types";
45

@@ -13,6 +14,8 @@ interface CommandSuggestionsProps {
1314
isVisible: boolean;
1415
ariaLabel?: string;
1516
listId?: string;
17+
/** Reference to the input element for portal positioning */
18+
anchorRef?: React.RefObject<HTMLElement | null>;
1619
}
1720

1821
// Main component
@@ -23,14 +26,51 @@ export const CommandSuggestions: React.FC<CommandSuggestionsProps> = ({
2326
isVisible,
2427
ariaLabel = "Command suggestions",
2528
listId,
29+
anchorRef,
2630
}) => {
2731
const [selectedIndex, setSelectedIndex] = useState(0);
32+
const [position, setPosition] = useState<{ top: number; left: number; width: number } | null>(
33+
null
34+
);
35+
const menuRef = useRef<HTMLDivElement>(null);
2836

2937
// Reset selection whenever suggestions change
3038
useEffect(() => {
3139
setSelectedIndex(0);
3240
}, [suggestions]);
3341

42+
// Calculate position when using portal mode
43+
useLayoutEffect(() => {
44+
if (!anchorRef?.current || !isVisible) {
45+
setPosition(null);
46+
return;
47+
}
48+
49+
const updatePosition = () => {
50+
const anchor = anchorRef.current;
51+
if (!anchor) return;
52+
53+
const rect = anchor.getBoundingClientRect();
54+
const menuHeight = menuRef.current?.offsetHeight ?? 200;
55+
56+
setPosition({
57+
top: rect.top - menuHeight - 8, // 8px gap above anchor
58+
left: rect.left,
59+
width: rect.width,
60+
});
61+
};
62+
63+
updatePosition();
64+
65+
// Update on resize/scroll
66+
window.addEventListener("resize", updatePosition);
67+
window.addEventListener("scroll", updatePosition, true);
68+
return () => {
69+
window.removeEventListener("resize", updatePosition);
70+
window.removeEventListener("scroll", updatePosition, true);
71+
};
72+
}, [anchorRef, isVisible, suggestions]);
73+
3474
// Handle keyboard navigation
3575
useEffect(() => {
3676
if (!isVisible || suggestions.length === 0) return;
@@ -84,16 +124,31 @@ export const CommandSuggestions: React.FC<CommandSuggestionsProps> = ({
84124
const activeSuggestion = suggestions[selectedIndex] ?? suggestions[0];
85125
const resolvedListId = listId ?? `command-suggestions-list`;
86126

87-
return (
127+
const content = (
88128
<div
129+
ref={menuRef}
89130
id={resolvedListId}
90131
role="listbox"
91132
aria-label={ariaLabel}
92133
aria-activedescendant={
93134
activeSuggestion ? `${resolvedListId}-option-${activeSuggestion.id}` : undefined
94135
}
95136
data-command-suggestions
96-
className="bg-separator border-border-light absolute right-0 bottom-full left-0 z-[100] mb-2 flex max-h-[200px] flex-col overflow-y-auto rounded border shadow-[0_-4px_12px_rgba(0,0,0,0.4)]"
137+
className={cn(
138+
"bg-separator border-border-light z-[100] flex max-h-[200px] flex-col overflow-y-auto rounded border shadow-[0_-4px_12px_rgba(0,0,0,0.4)]",
139+
// Use absolute positioning relative to parent when not in portal mode
140+
!anchorRef && "absolute right-0 bottom-full left-0 mb-2"
141+
)}
142+
style={
143+
anchorRef && position
144+
? {
145+
position: "fixed",
146+
top: position.top,
147+
left: position.left,
148+
width: position.width,
149+
}
150+
: undefined
151+
}
97152
>
98153
{suggestions.map((suggestion, index) => (
99154
<div
@@ -119,4 +174,11 @@ export const CommandSuggestions: React.FC<CommandSuggestionsProps> = ({
119174
</div>
120175
</div>
121176
);
177+
178+
// Use portal when anchorRef is provided (to escape overflow:hidden containers)
179+
if (anchorRef) {
180+
return createPortal(content, document.body);
181+
}
182+
183+
return content;
122184
};

tests/manual_parse_check.ts

Lines changed: 0 additions & 14 deletions
This file was deleted.

0 commit comments

Comments
 (0)