Skip to content

Commit b962bcc

Browse files
authored
fix(explorer): Hover state fixes (#102372)
Prevents conflicting focus between input area and hover focus on blocks. If you type, the hovering won't steal focus. If you hover, the input won't appear focused. (address Josh's feedback)
1 parent 4c5d854 commit b962bcc

File tree

3 files changed

+60
-27
lines changed

3 files changed

+60
-27
lines changed

static/app/views/seerExplorer/explorerPanel.tsx

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ function ExplorerPanel({isVisible = false}: ExplorerPanelProps) {
1818

1919
const [inputValue, setInputValue] = useState('');
2020
const [focusedBlockIndex, setFocusedBlockIndex] = useState(-1); // -1 means input is focused
21-
const [showSlashCommands, setShowSlashCommands] = useState(false);
21+
const [isSlashCommandsVisible, setIsSlashCommandsVisible] = useState(false);
2222
const [isMinimized, setIsMinimized] = useState(false); // state for slide-down
2323
const textareaRef = useRef<HTMLTextAreaElement>(null);
2424
const scrollContainerRef = useRef<HTMLDivElement>(null);
@@ -27,6 +27,7 @@ function ExplorerPanel({isVisible = false}: ExplorerPanelProps) {
2727
Map<number, (key: 'Enter' | 'ArrowUp' | 'ArrowDown') => boolean>
2828
>(new Map());
2929
const panelRef = useRef<HTMLDivElement>(null);
30+
const hoveredBlockIndex = useRef<number>(-1);
3031

3132
// Custom hooks
3233
const {panelSize, handleMaxSize, handleMedSize} = usePanelSizing();
@@ -112,6 +113,32 @@ function ExplorerPanel({isVisible = false}: ExplorerPanelProps) {
112113
blockRefs.current = blockRefs.current.slice(0, blocks.length);
113114
}, [blocks]);
114115

116+
// Auto-focus input when user starts typing while a block is focused
117+
useEffect(() => {
118+
if (!isVisible) {
119+
return undefined;
120+
}
121+
122+
const handleKeyDown = (e: KeyboardEvent) => {
123+
if (focusedBlockIndex !== -1) {
124+
const isPrintableChar =
125+
e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey;
126+
127+
if (isPrintableChar) {
128+
e.preventDefault();
129+
setFocusedBlockIndex(-1);
130+
textareaRef.current?.focus();
131+
setInputValue(prev => prev + e.key);
132+
}
133+
}
134+
};
135+
136+
document.addEventListener('keydown', handleKeyDown);
137+
return () => {
138+
document.removeEventListener('keydown', handleKeyDown);
139+
};
140+
}, [isVisible, focusedBlockIndex]);
141+
115142
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
116143
if (e.key === 'Escape' && isPolling && !interruptRequested) {
117144
e.preventDefault();
@@ -139,10 +166,6 @@ function ExplorerPanel({isVisible = false}: ExplorerPanelProps) {
139166
textareaRef.current?.focus();
140167
}
141168

142-
// Check if we should show slash commands
143-
const shouldShow = value.startsWith('/') && !value.includes(' ') && value.length > 1;
144-
setShowSlashCommands(shouldShow);
145-
146169
// Auto-resize textarea
147170
const textarea = e.target;
148171
textarea.style.height = 'auto';
@@ -155,6 +178,7 @@ function ExplorerPanel({isVisible = false}: ExplorerPanelProps) {
155178
};
156179

157180
const handleInputClick = () => {
181+
hoveredBlockIndex.current = -1;
158182
setFocusedBlockIndex(-1);
159183
textareaRef.current?.focus();
160184
setIsMinimized(false);
@@ -168,20 +192,15 @@ function ExplorerPanel({isVisible = false}: ExplorerPanelProps) {
168192
// Execute the command
169193
command.handler();
170194

171-
// Clear input and hide slash commands
195+
// Clear input
172196
setInputValue('');
173-
setShowSlashCommands(false);
174197

175198
// Reset textarea height
176199
if (textareaRef.current) {
177200
textareaRef.current.style.height = 'auto';
178201
}
179202
};
180203

181-
const handleSlashCommandsClose = () => {
182-
setShowSlashCommands(false);
183-
};
184-
185204
const panelContent = (
186205
<PanelContainers
187206
ref={panelRef}
@@ -206,7 +225,23 @@ function ExplorerPanel({isVisible = false}: ExplorerPanelProps) {
206225
isFocused={focusedBlockIndex === index}
207226
isPolling={isPolling}
208227
onClick={() => handleBlockClick(index)}
209-
onMouseEnter={() => setFocusedBlockIndex(index)}
228+
onMouseEnter={() => {
229+
// Don't change focus while slash commands menu is open or if already on this block
230+
if (isSlashCommandsVisible || hoveredBlockIndex.current === index) {
231+
return;
232+
}
233+
234+
hoveredBlockIndex.current = index;
235+
setFocusedBlockIndex(index);
236+
if (document.activeElement === textareaRef.current) {
237+
textareaRef.current?.blur();
238+
}
239+
}}
240+
onMouseLeave={() => {
241+
if (hoveredBlockIndex.current === index) {
242+
hoveredBlockIndex.current = -1;
243+
}
244+
}}
210245
onDelete={() => deleteFromIndex(index)}
211246
onNavigate={() => setIsMinimized(true)}
212247
onRegisterEnterHandler={handler => {
@@ -220,14 +255,13 @@ function ExplorerPanel({isVisible = false}: ExplorerPanelProps) {
220255
ref={textareaRef}
221256
inputValue={inputValue}
222257
focusedBlockIndex={focusedBlockIndex}
223-
showSlashCommands={showSlashCommands}
224258
isPolling={isPolling}
225259
interruptRequested={interruptRequested}
226260
onInputChange={handleInputChange}
227261
onKeyDown={handleKeyDown}
228262
onInputClick={handleInputClick}
229263
onCommandSelect={handleCommandSelect}
230-
onSlashCommandsClose={handleSlashCommandsClose}
264+
onSlashCommandsVisibilityChange={setIsSlashCommandsVisible}
231265
onMaxSize={handleMaxSize}
232266
onMedSize={handleMedSize}
233267
onClear={startNewSession}

static/app/views/seerExplorer/inputSection.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ interface InputSectionProps {
1717
onKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
1818
onMaxSize: () => void;
1919
onMedSize: () => void;
20-
onSlashCommandsClose: () => void;
21-
showSlashCommands: boolean;
20+
onSlashCommandsVisibilityChange: (isVisible: boolean) => void;
2221
ref?: React.RefObject<HTMLTextAreaElement | null>;
2322
}
2423

@@ -32,7 +31,7 @@ function InputSection({
3231
onKeyDown,
3332
onInputClick,
3433
onCommandSelect,
35-
onSlashCommandsClose,
34+
onSlashCommandsVisibilityChange,
3635
onMaxSize,
3736
onMedSize,
3837
ref,
@@ -56,7 +55,7 @@ function InputSection({
5655
<SlashCommands
5756
inputValue={inputValue}
5857
onCommandSelect={onCommandSelect}
59-
onClose={onSlashCommandsClose}
58+
onVisibilityChange={onSlashCommandsVisibilityChange}
6059
onMaxSize={onMaxSize}
6160
onMedSize={onMedSize}
6261
onClear={onClear}

static/app/views/seerExplorer/slashCommands.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,19 @@ export interface SlashCommand {
1313
interface SlashCommandsProps {
1414
inputValue: string;
1515
onClear: () => void;
16-
onClose: () => void;
1716
onCommandSelect: (command: SlashCommand) => void;
1817
onMaxSize: () => void;
1918
onMedSize: () => void;
19+
onVisibilityChange?: (isVisible: boolean) => void;
2020
}
2121

2222
function SlashCommands({
2323
inputValue,
2424
onCommandSelect,
25-
onClose,
2625
onMaxSize,
2726
onMedSize,
2827
onClear,
28+
onVisibilityChange,
2929
}: SlashCommandsProps) {
3030
const [selectedIndex, setSelectedIndex] = useState(0);
3131
const openFeedbackForm = useFeedbackForm();
@@ -86,6 +86,11 @@ function SlashCommands({
8686
setSelectedIndex(0);
8787
}, [filteredCommands]);
8888

89+
// Notify parent when visibility changes
90+
useEffect(() => {
91+
onVisibilityChange?.(showSuggestions);
92+
}, [showSuggestions, onVisibilityChange]);
93+
8994
// Handle keyboard navigation with higher priority
9095
const handleKeyDown = useCallback(
9196
(e: KeyboardEvent) => {
@@ -109,16 +114,11 @@ function SlashCommands({
109114
onCommandSelect(filteredCommands[selectedIndex]);
110115
}
111116
break;
112-
case 'Escape':
113-
e.preventDefault();
114-
e.stopPropagation();
115-
onClose();
116-
break;
117117
default:
118118
break;
119119
}
120120
},
121-
[showSuggestions, selectedIndex, filteredCommands, onCommandSelect, onClose]
121+
[showSuggestions, selectedIndex, filteredCommands, onCommandSelect]
122122
);
123123

124124
useEffect(() => {
@@ -162,7 +162,7 @@ const SuggestionsPanel = styled('div')`
162162
border-bottom: none;
163163
border-radius: ${p => p.theme.borderRadius};
164164
box-shadow: ${p => p.theme.dropShadowHeavy};
165-
max-height: 200px;
165+
max-height: 500px;
166166
overflow-y: auto;
167167
z-index: 10;
168168
`;

0 commit comments

Comments
 (0)