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,77 +140,75 @@ 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} <div style={{ width: '100%' }}>
onChange={(isOpen) => { {selectedValue && toolRegistry[selectedValue] && !opened ? (
setOpened(isOpen); // Show selected tool in AutomationEntry style when tool is selected and dropdown closed
// Clear search term when menu closes to show proper display <div onClick={handleSearchFocus} style={{ cursor: 'pointer' }}>
if (!isOpen) { <div style={{
setSearchTerm(''); display: 'flex',
} alignItems: 'center',
}} gap: 'var(--mantine-spacing-sm)',
closeOnClickOutside={true} padding: '0 0.5rem',
closeOnEscape={true} borderRadius: 'var(--mantine-radius-sm)',
position="bottom-start" }}>
offset={4} <div style={{ color: 'var(--mantine-color-text)', fontSize: '1.2rem' }}>
withinPortal={false} {toolRegistry[selectedValue].icon}
trapFocus={false}
shadow="sm"
transitionProps={{ duration: 0 }}
>
<Menu.Target>
<div style={{ width: '100%' }}>
{selectedValue && toolRegistry[selectedValue] && !opened ? (
// Show selected tool in AutomationEntry style when tool is selected and not searching
<div onClick={handleSearchFocus} style={{ cursor: 'pointer' }}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 'var(--mantine-spacing-sm)',
padding: '0 0.5rem',
borderRadius: 'var(--mantine-radius-sm)',
}}>
<div style={{ color: 'var(--mantine-color-text)', fontSize: '1.2rem' }}>
{toolRegistry[selectedValue].icon}
</div>
<Text size="sm" style={{ flex: 1, color: 'var(--mantine-color-text)' }}>
{toolRegistry[selectedValue].name}
</Text>
</div>
</div> </div>
) : ( <Text size="sm" style={{ flex: 1, color: 'var(--mantine-color-text)' }}>
// Show search input when no tool selected or actively searching {toolRegistry[selectedValue].name}
<ToolSearch
value={searchTerm}
onChange={handleSearchChange}
toolRegistry={filteredToolRegistry}
mode="filter"
placeholder={getDisplayValue()}
hideIcon={true}
onFocus={handleSearchFocus}
/>
)}
</div>
</Menu.Target>
<Menu.Dropdown p={0} style={{ minWidth: '16rem' }}>
<ScrollArea h={350}>
<Stack gap="sm" p="sm">
{displayGroups.length === 0 ? (
<Text size="sm" c="dimmed" ta="center" p="md">
{isSearching
? t('tools.noSearchResults', 'No tools found')
: t('tools.noTools', 'No tools available')
}
</Text> </Text>
) : ( </div>
renderedTools </div>
)} ) : (
</Stack> // Show search input when no tool selected OR when dropdown is opened
</ScrollArea> <ToolSearch
</Menu.Dropdown> value={searchTerm}
</Menu> onChange={handleSearchChange}
toolRegistry={filteredToolRegistry}
mode="filter"
placeholder={getDisplayValue()}
hideIcon={true}
onFocus={handleInputFocus}
autoFocus={shouldAutoFocus}
/>
)}
</div>
{/* 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}>
<Stack gap="sm" p="sm">
{displayGroups.length === 0 ? (
<Text size="sm" c="dimmed" ta="center" p="md">
{isSearching
? t('tools.noSearchResults', 'No tools found')
: t('tools.noTools', 'No tools available')
}
</Text>
) : (
renderedTools
)}
</Stack>
</ScrollArea>
</div>
)}
</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>
); );