mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 06:09:23 +00:00
Drop downs appear correctly
This commit is contained in:
parent
4e3bf1251d
commit
f7a4aaa766
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user