Drop downs appear correctly

This commit is contained in:
Connor Yoh 2025-08-25 13:30:08 +01:00
parent 4e3bf1251d
commit f7a4aaa766
2 changed files with 117 additions and 74 deletions

View File

@ -1,6 +1,6 @@
import React, { useState, useMemo, useCallback } from 'react'; import React, { useState, useMemo, useCallback, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Menu, Stack, Text, ScrollArea } from '@mantine/core'; import { Stack, Text, ScrollArea } from '@mantine/core';
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy'; import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
import { useToolSections } from '../../../hooks/useToolSections'; import { useToolSections } from '../../../hooks/useToolSections';
import { renderToolButtons } from '../shared/renderToolButtons'; import { renderToolButtons } from '../shared/renderToolButtons';
@ -24,6 +24,8 @@ export default function ToolSelector({
const { t } = useTranslation(); const { t } = useTranslation();
const [opened, setOpened] = useState(false); const [opened, setOpened] = useState(false);
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [shouldAutoFocus, setShouldAutoFocus] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
// Filter out excluded tools (like 'automate' itself) // Filter out excluded tools (like 'automate' itself)
const baseFilteredTools = useMemo(() => { const baseFilteredTools = useMemo(() => {
@ -66,13 +68,20 @@ export default function ToolSelector({
} }
if (!sections || sections.length === 0) { if (!sections || sections.length === 0) {
// If no sections, create a simple group from filtered tools
if (baseFilteredTools.length > 0) {
return [{
name: 'All Tools',
tools: baseFilteredTools.map(([key, tool]) => ({ key, ...tool }))
}];
}
return []; return [];
} }
// Find the "all" section which contains all tools without duplicates // Find the "all" section which contains all tools without duplicates
const allSection = sections.find(s => (s as any).key === 'all'); const allSection = sections.find(s => (s as any).key === 'all');
return allSection?.subcategories || []; return allSection?.subcategories || [];
}, [isSearching, searchGroups, sections]); }, [isSearching, searchGroups, sections, baseFilteredTools]);
const handleToolSelect = useCallback((toolKey: string) => { const handleToolSelect = useCallback((toolKey: string) => {
onSelect(toolKey); onSelect(toolKey);
@ -88,8 +97,25 @@ export default function ToolSelector({
const handleSearchFocus = () => { const handleSearchFocus = () => {
setOpened(true); setOpened(true);
setShouldAutoFocus(true); // Request auto-focus for the input
}; };
// Handle click outside to close dropdown
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setOpened(false);
setSearchTerm('');
}
};
if (opened) {
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}
}, [opened]);
const handleSearchChange = (value: string) => { const handleSearchChange = (value: string) => {
setSearchTerm(value); setSearchTerm(value);
if (!opened) { if (!opened) {
@ -97,6 +123,14 @@ export default function ToolSelector({
} }
}; };
const handleInputFocus = () => {
if (!opened) {
setOpened(true);
}
// Clear auto-focus flag since input is now focused
setShouldAutoFocus(false);
};
// Get display value for selected tool // Get display value for selected tool
const getDisplayValue = () => { const getDisplayValue = () => {
if (selectedValue && toolRegistry[selectedValue]) { if (selectedValue && toolRegistry[selectedValue]) {
@ -106,29 +140,11 @@ export default function ToolSelector({
}; };
return ( return (
<div style={{ position: 'relative', width: '100%' }}> <div ref={containerRef} style={{ position: 'relative', width: '100%' }}>
<Menu {/* Always show the target - either selected tool or search input */}
opened={opened}
onChange={(isOpen) => {
setOpened(isOpen);
// Clear search term when menu closes to show proper display
if (!isOpen) {
setSearchTerm('');
}
}}
closeOnClickOutside={true}
closeOnEscape={true}
position="bottom-start"
offset={4}
withinPortal={false}
trapFocus={false}
shadow="sm"
transitionProps={{ duration: 0 }}
>
<Menu.Target>
<div style={{ width: '100%' }}> <div style={{ width: '100%' }}>
{selectedValue && toolRegistry[selectedValue] && !opened ? ( {selectedValue && toolRegistry[selectedValue] && !opened ? (
// Show selected tool in AutomationEntry style when tool is selected and not searching // Show selected tool in AutomationEntry style when tool is selected and dropdown closed
<div onClick={handleSearchFocus} style={{ cursor: 'pointer' }}> <div onClick={handleSearchFocus} style={{ cursor: 'pointer' }}>
<div style={{ <div style={{
display: 'flex', display: 'flex',
@ -146,7 +162,7 @@ export default function ToolSelector({
</div> </div>
</div> </div>
) : ( ) : (
// Show search input when no tool selected or actively searching // Show search input when no tool selected OR when dropdown is opened
<ToolSearch <ToolSearch
value={searchTerm} value={searchTerm}
onChange={handleSearchChange} onChange={handleSearchChange}
@ -154,13 +170,29 @@ export default function ToolSelector({
mode="filter" mode="filter"
placeholder={getDisplayValue()} placeholder={getDisplayValue()}
hideIcon={true} hideIcon={true}
onFocus={handleSearchFocus} onFocus={handleInputFocus}
autoFocus={shouldAutoFocus}
/> />
)} )}
</div> </div>
</Menu.Target>
<Menu.Dropdown p={0} style={{ minWidth: '16rem' }}> {/* Custom dropdown */}
{opened && (
<div
style={{
position: 'absolute',
top: '100%',
left: 0,
right: 0,
zIndex: 1000,
backgroundColor: 'var(--mantine-color-body)',
border: '1px solid var(--mantine-color-gray-3)',
borderRadius: 'var(--mantine-radius-sm)',
boxShadow: 'var(--mantine-shadow-sm)',
marginTop: '4px',
minWidth: '16rem'
}}
>
<ScrollArea h={350}> <ScrollArea h={350}>
<Stack gap="sm" p="sm"> <Stack gap="sm" p="sm">
{displayGroups.length === 0 ? ( {displayGroups.length === 0 ? (
@ -175,8 +207,8 @@ export default function ToolSelector({
)} )}
</Stack> </Stack>
</ScrollArea> </ScrollArea>
</Menu.Dropdown> </div>
</Menu> )}
</div> </div>
); );
} }

View File

@ -15,6 +15,7 @@ interface ToolSearchProps {
placeholder?: string; placeholder?: string;
hideIcon?: boolean; hideIcon?: boolean;
onFocus?: () => void; onFocus?: () => void;
autoFocus?: boolean;
} }
const ToolSearch = ({ const ToolSearch = ({
@ -26,7 +27,8 @@ const ToolSearch = ({
selectedToolKey, selectedToolKey,
placeholder, placeholder,
hideIcon = false, hideIcon = false,
onFocus onFocus,
autoFocus = false
}: ToolSearchProps) => { }: ToolSearchProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [dropdownOpen, setDropdownOpen] = useState(false); const [dropdownOpen, setDropdownOpen] = useState(false);
@ -67,6 +69,15 @@ const ToolSearch = ({
return () => document.removeEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside);
}, []); }, []);
// Auto-focus the input when requested
useEffect(() => {
if (autoFocus && searchRef.current) {
setTimeout(() => {
searchRef.current?.focus();
}, 10);
}
}, [autoFocus]);
const searchInput = ( const searchInput = (
<div className="search-input-container"> <div className="search-input-container">
<TextInput <TextInput
@ -76,7 +87,7 @@ const ToolSearch = ({
placeholder={placeholder || t("toolPicker.searchPlaceholder", "Search tools...")} placeholder={placeholder || t("toolPicker.searchPlaceholder", "Search tools...")}
icon={hideIcon ? undefined : <span className="material-symbols-rounded">search</span>} icon={hideIcon ? undefined : <span className="material-symbols-rounded">search</span>}
autoComplete="off" autoComplete="off"
onFocus={onFocus}
/> />
</div> </div>
); );