diff --git a/src/components/Projects/SelectLanguages.tsx b/src/components/Projects/SelectLanguages.tsx index bde1db0f2..92d7ace64 100644 --- a/src/components/Projects/SelectLanguages.tsx +++ b/src/components/Projects/SelectLanguages.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'; import { useOutsideClick } from '../../hooks/use-outside-click'; import { httpGet } from '../../lib/http'; import { useToast } from '../../hooks/use-toast'; -import { ChevronDown, X } from 'lucide-react'; +import { ChevronDown, Search, X } from 'lucide-react'; type SelectLanguagesProps = { projectId: string; @@ -14,10 +14,44 @@ export function SelectLanguages(props: SelectLanguagesProps) { const { projectId, onSelectLanguage, selectedLanguage } = props; const dropdownRef = useRef(null); + const searchInputRef = useRef(null); + const optionsRef = useRef(null); const toast = useToast(); const [distinctLanguages, setDistinctLanguages] = useState([]); const [isOpen, setIsOpen] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + const [highlightedIndex, setHighlightedIndex] = useState(0); + + const filteredLanguages = distinctLanguages.filter((language) => + language.toLowerCase().includes(searchQuery.toLowerCase()), + ); + + // Handle scrolling of highlighted option into view + useEffect(() => { + if (!isOpen || !optionsRef.current) { + return; + } + + const options = optionsRef.current.getElementsByTagName('button'); + const highlightedOption = options[highlightedIndex]; + if (!highlightedOption) { + return; + } + + const containerRect = optionsRef.current.getBoundingClientRect(); + const optionRect = highlightedOption.getBoundingClientRect(); + + const isAbove = optionRect.top < containerRect.top; + const isBelow = optionRect.bottom > containerRect.bottom; + + if (isAbove || isBelow) { + highlightedOption.scrollIntoView({ + block: 'nearest', + behavior: 'instant', + }); + } + }, [highlightedIndex, isOpen]); const loadDistinctLanguages = async () => { const { response, error } = await httpGet( @@ -34,53 +68,124 @@ export function SelectLanguages(props: SelectLanguagesProps) { useOutsideClick(dropdownRef, () => { setIsOpen(false); + setSearchQuery(''); + setHighlightedIndex(0); }); useEffect(() => { loadDistinctLanguages().finally(() => {}); }, []); + useEffect(() => { + if (isOpen && searchInputRef.current) { + searchInputRef.current.focus(); + } + }, [isOpen]); + + const handleKeyDown = (e: React.KeyboardEvent) => { + switch (e.key) { + case 'ArrowDown': + e.preventDefault(); + setHighlightedIndex((prev) => + prev >= filteredLanguages.length - 1 ? 0 : prev + 1, + ); + break; + case 'ArrowUp': + e.preventDefault(); + setHighlightedIndex((prev) => + prev <= 0 ? filteredLanguages.length - 1 : prev - 1, + ); + break; + case 'Enter': + e.preventDefault(); + if (filteredLanguages[highlightedIndex]) { + onSelectLanguage(filteredLanguages[highlightedIndex]); + setIsOpen(false); + setSearchQuery(''); + setHighlightedIndex(0); + } + break; + case 'Escape': + setIsOpen(false); + setSearchQuery(''); + setHighlightedIndex(0); + break; + } + }; + return (
- - {selectedLanguage && ( +
- )} + {selectedLanguage && ( + + )} +
{isOpen && (
- {distinctLanguages.map((language) => { - const isSelected = selectedLanguage === language; +
+ + { + setSearchQuery(e.target.value); + setHighlightedIndex(0); + }} + /> +
+
+ {filteredLanguages.map((language, index) => { + const isSelected = selectedLanguage === language; + const isHighlighted = index === highlightedIndex; - return ( - - ); - })} + return ( + + ); + })} + {filteredLanguages.length === 0 && ( +
+ No languages found +
+ )} +
)}