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 { Menu, Stack, Text, ScrollArea } from '@mantine/core';
import { Stack, Text, ScrollArea } from '@mantine/core';
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
import { useToolSections } from '../../../hooks/useToolSections';
import { renderToolButtons } from '../shared/renderToolButtons';
@ -24,6 +24,8 @@ export default function ToolSelector({
const { t } = useTranslation();
const [opened, setOpened] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [shouldAutoFocus, setShouldAutoFocus] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
// Filter out excluded tools (like 'automate' itself)
const baseFilteredTools = useMemo(() => {
@ -66,13 +68,20 @@ export default function ToolSelector({
}
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 [];
}
// Find the "all" section which contains all tools without duplicates
const allSection = sections.find(s => (s as any).key === 'all');
return allSection?.subcategories || [];
}, [isSearching, searchGroups, sections]);
}, [isSearching, searchGroups, sections, baseFilteredTools]);
const handleToolSelect = useCallback((toolKey: string) => {
onSelect(toolKey);
@ -88,8 +97,25 @@ export default function ToolSelector({
const handleSearchFocus = () => {
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) => {
setSearchTerm(value);
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
const getDisplayValue = () => {
if (selectedValue && toolRegistry[selectedValue]) {
@ -106,77 +140,75 @@ export default function ToolSelector({
};
return (
<div style={{ position: 'relative', width: '100%' }}>
<Menu
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%' }}>
{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 ref={containerRef} style={{ position: 'relative', width: '100%' }}>
{/* Always show the target - either selected tool or search input */}
<div style={{ width: '100%' }}>
{selectedValue && toolRegistry[selectedValue] && !opened ? (
// Show selected tool in AutomationEntry style when tool is selected and dropdown closed
<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>
) : (
// Show search input when no tool selected or actively searching
<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 size="sm" style={{ flex: 1, color: 'var(--mantine-color-text)' }}>
{toolRegistry[selectedValue].name}
</Text>
) : (
renderedTools
)}
</Stack>
</ScrollArea>
</Menu.Dropdown>
</Menu>
</div>
</div>
) : (
// Show search input when no tool selected OR when dropdown is opened
<ToolSearch
value={searchTerm}
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>
);
}

View File

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