/* eslint-disable no-nested-ternary */ /* eslint-disable sonarjs/cognitive-complexity */ /* eslint-disable react/jsx-props-no-spreading */ /* eslint-disable react/function-component-definition */ import './styles.scss'; import { CloseOutlined, DownOutlined, LoadingOutlined, ReloadOutlined, } from '@ant-design/icons'; import { Color } from '@signozhq/design-tokens'; import { Select } from 'antd'; import cx from 'classnames'; import TextToolTip from 'components/TextToolTip'; import { SOMETHING_WENT_WRONG } from 'constants/api'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { capitalize, isEmpty } from 'lodash-es'; import { ArrowDown, ArrowUp, Info } from 'lucide-react'; import type { BaseSelectRef } from 'rc-select'; import React, { useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import { popupContainer } from 'utils/selectPopupContainer'; import { CustomSelectProps, OptionData } from './types'; import { filterOptionsBySearch, handleScrollToBottom, prioritizeOrAddOptionForSingleSelect, SPACEKEY, } from './utils'; /** * CustomSelect Component * */ const CustomSelect: React.FC = ({ placeholder = 'Search...', className, loading = false, onSearch, options = [], value, onChange, defaultActiveFirstOption = true, noDataMessage, onClear, getPopupContainer, dropdownRender, highlightSearch = true, placement = 'bottomLeft', popupMatchSelectWidth = true, popupClassName, errorMessage, allowClear = false, onRetry, showIncompleteDataMessage = false, showRetryButton = true, isDynamicVariable = false, ...rest }) => { // ===== State & Refs ===== const [isOpen, setIsOpen] = useState(false); const [searchText, setSearchText] = useState(''); const [activeOptionIndex, setActiveOptionIndex] = useState(-1); const [isScrolledToBottom, setIsScrolledToBottom] = useState(false); const isDarkMode = useIsDarkMode(); // Refs for element access and scroll behavior const selectRef = useRef(null); const dropdownRef = useRef(null); const optionRefs = useRef>({}); // Flag to track if dropdown just opened const justOpenedRef = useRef(false); // Add a scroll handler for the dropdown const handleDropdownScroll = useCallback( (e: React.UIEvent): void => { setIsScrolledToBottom(handleScrollToBottom(e)); }, [], ); // ===== Option Filtering & Processing Utilities ===== /** * Checks if a label exists in the provided options */ const isLabelPresent = useCallback( (options: OptionData[], label: string): boolean => options.some((option) => { const lowerLabel = label.toLowerCase(); // Check in nested options if they exist if ('options' in option && Array.isArray(option.options)) { return option.options.some( (subOption) => subOption.label.toLowerCase() === lowerLabel, ); } // Check top-level option return option.label.toLowerCase() === lowerLabel; }), [], ); /** * Separates section and non-section options */ const splitOptions = useCallback((options: OptionData[]): { sectionOptions: OptionData[]; nonSectionOptions: OptionData[]; } => { const sectionOptions: OptionData[] = []; const nonSectionOptions: OptionData[] = []; options.forEach((option) => { if ('options' in option && Array.isArray(option.options)) { sectionOptions.push(option); } else { nonSectionOptions.push(option); } }); return { sectionOptions, nonSectionOptions }; }, []); /** * Apply search filtering to options */ const filteredOptions = useMemo( (): OptionData[] => filterOptionsBySearch(options, searchText), [options, searchText], ); // ===== UI & Rendering Functions ===== /** * Highlights matched text in search results */ const highlightMatchedText = useCallback( (text: string, searchQuery: string): React.ReactNode => { if (!searchQuery || !highlightSearch) return text; try { const parts = text.split( new RegExp( `(${searchQuery.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')})`, 'gi', ), ); return ( <> {parts.map((part, i) => { // Create a deterministic but unique key const uniqueKey = `${text.substring(0, 3)}-${part.substring(0, 3)}-${i}`; return part.toLowerCase() === searchQuery.toLowerCase() ? ( {part} ) : ( part ); })} ); } catch (error) { console.error('Error in text highlighting:', error); return text; } }, [highlightSearch], ); /** * Renders an individual option with proper keyboard navigation support */ const renderOptionItem = useCallback( ( option: OptionData, isSelected: boolean, index?: number, ): React.ReactElement => { const handleSelection = (): void => { if (onChange) { onChange(option.value, option); setIsOpen(false); } }; const isActive = index === activeOptionIndex; const optionId = `option-${index}`; return (
{ if (index !== undefined) { optionRefs.current[index] = el; } }} className={cx('option-item', { selected: isSelected, active: isActive, })} onClick={(e): void => { e.stopPropagation(); handleSelection(); }} onKeyDown={(e): void => { if (e.key === 'Enter' || e.key === SPACEKEY) { e.preventDefault(); handleSelection(); } }} onMouseEnter={(): void => setActiveOptionIndex(index || -1)} role="option" aria-selected={isSelected} aria-disabled={option.disabled} tabIndex={isActive ? 0 : -1} >
{highlightMatchedText(String(option.label || ''), searchText)}
{option.type === 'custom' && (
{capitalize(option.type)}
)}
); }, [highlightMatchedText, searchText, onChange, activeOptionIndex], ); /** * Helper function to render option with index tracking */ const renderOptionWithIndex = useCallback( (option: OptionData, isSelected: boolean, idx: number) => renderOptionItem(option, isSelected, idx), [renderOptionItem], ); /** * Custom clear button renderer */ const clearIcon = useCallback( () => ( { e.stopPropagation(); if (onChange) onChange(undefined, []); if (onClear) onClear(); }} /> ), [onChange, onClear], ); // ===== Event Handlers ===== /** * Handles search input changes */ const handleSearch = useCallback( (value: string): void => { const trimmedValue = value.trim(); setSearchText(trimmedValue); // Reset active option index when search changes if (isOpen) { setActiveOptionIndex(0); } if (onSearch) onSearch(trimmedValue); }, [onSearch, isOpen], ); /** * Prevents event propagation for dropdown clicks */ const handleDropdownClick = useCallback((e: React.MouseEvent): void => { e.stopPropagation(); }, []); /** * Comprehensive keyboard navigation handler */ const handleKeyDown = useCallback( (e: React.KeyboardEvent): void => { // Handle keyboard navigation when dropdown is open if (isOpen) { // Get flattened list of all selectable options const getFlatOptions = (): OptionData[] => { if (!filteredOptions) return []; const flatList: OptionData[] = []; // Process options let processedOptions = isEmpty(value) ? filteredOptions : prioritizeOrAddOptionForSingleSelect(filteredOptions, value); if (!isEmpty(searchText)) { processedOptions = filterOptionsBySearch(processedOptions, searchText); } const { sectionOptions, nonSectionOptions } = splitOptions( processedOptions, ); // Add custom option if needed if ( !isEmpty(searchText) && !isLabelPresent(processedOptions, searchText) ) { flatList.push({ label: searchText, value: searchText, type: 'custom', }); } // Add all options to flat list flatList.push(...nonSectionOptions); sectionOptions.forEach((section) => { if (section.options) { flatList.push(...section.options); } }); return flatList; }; const options = getFlatOptions(); // If we just opened the dropdown and have options, set first option as active if (justOpenedRef.current && options.length > 0) { setActiveOptionIndex(0); justOpenedRef.current = false; } // If no option is active but we have options, activate the first one if (activeOptionIndex === -1 && options.length > 0) { setActiveOptionIndex(0); } switch (e.key) { case 'ArrowDown': e.preventDefault(); if (options.length > 0) { setActiveOptionIndex((prev) => prev < options.length - 1 ? prev + 1 : 0, ); } break; case 'ArrowUp': e.preventDefault(); if (options.length > 0) { setActiveOptionIndex((prev) => prev > 0 ? prev - 1 : options.length - 1, ); } break; case 'Tab': // Tab navigation with Shift key support if (e.shiftKey) { e.preventDefault(); if (options.length > 0) { setActiveOptionIndex((prev) => prev > 0 ? prev - 1 : options.length - 1, ); } } else { e.preventDefault(); if (options.length > 0) { setActiveOptionIndex((prev) => prev < options.length - 1 ? prev + 1 : 0, ); } } break; case 'Enter': e.preventDefault(); if (activeOptionIndex >= 0 && activeOptionIndex < options.length) { // Select the focused option const selectedOption = options[activeOptionIndex]; if (onChange) { onChange(selectedOption.value, selectedOption); setIsOpen(false); setActiveOptionIndex(-1); setSearchText(''); } } else if (!isEmpty(searchText)) { // Add custom value when no option is focused const customOption = { label: searchText, value: searchText, type: 'custom', }; if (onChange) { onChange(customOption.value, customOption); setIsOpen(false); setActiveOptionIndex(-1); setSearchText(''); } } break; case 'Escape': e.preventDefault(); setIsOpen(false); setActiveOptionIndex(-1); setSearchText(''); break; case ' ': // Space key if (activeOptionIndex >= 0 && activeOptionIndex < options.length) { e.preventDefault(); const selectedOption = options[activeOptionIndex]; if (onChange) { onChange(selectedOption.value, selectedOption); setIsOpen(false); setActiveOptionIndex(-1); setSearchText(''); } } break; default: break; } } else if (e.key === 'ArrowDown' || e.key === 'Tab') { // Open dropdown when Down or Tab is pressed while closed e.preventDefault(); setIsOpen(true); justOpenedRef.current = true; // Set flag to initialize active option on next render } }, [ isOpen, activeOptionIndex, filteredOptions, searchText, onChange, splitOptions, value, isLabelPresent, ], ); // ===== Dropdown Rendering ===== /** * Renders the custom dropdown with sections and keyboard navigation */ const customDropdownRender = useCallback((): React.ReactElement => { // Process options based on current value let processedOptions = isEmpty(value) ? filteredOptions : prioritizeOrAddOptionForSingleSelect(filteredOptions, value); if (!isEmpty(searchText)) { processedOptions = filterOptionsBySearch(processedOptions, searchText); } const { sectionOptions, nonSectionOptions } = splitOptions(processedOptions); // Check if we need to add a custom option based on search text const isSearchTextNotPresent = !isEmpty(searchText) && !isLabelPresent(processedOptions, searchText); let optionIndex = 0; // Add custom option if needed if (isSearchTextNotPresent) { nonSectionOptions.unshift({ label: searchText, value: searchText, type: 'custom', }); } // Helper function to map options with index tracking const mapOptions = (options: OptionData[]): React.ReactNode => options.map((option) => { const result = renderOptionWithIndex( option, option.value === value, optionIndex, ); optionIndex += 1; return result; }); const customMenu = (
= 0 ? `option-${activeOptionIndex}` : undefined } > {/* Non-section options */}
{nonSectionOptions.length > 0 && mapOptions(nonSectionOptions)}
{/* Section options */} {sectionOptions.length > 0 && sectionOptions.map((section) => !isEmpty(section.options) ? (
{section.label} {isDynamicVariable && ( } /> )}
{section.options && mapOptions(section.options)}
) : null, )} {/* Navigation help footer */}
{!loading && !errorMessage && !noDataMessage && !(showIncompleteDataMessage && isScrolledToBottom) && (
to navigate
)} {loading && (
Refreshing values...
)} {errorMessage && !loading && (
{errorMessage || SOMETHING_WENT_WRONG}
{onRetry && showRetryButton && (
{ e.stopPropagation(); onRetry(); }} />
)}
)} {showIncompleteDataMessage && isScrolledToBottom && !loading && !errorMessage && (
Don't see the value? Use search
)} {noDataMessage && !loading && !(showIncompleteDataMessage && isScrolledToBottom) && !errorMessage &&
{noDataMessage}
}
); return dropdownRender ? dropdownRender(customMenu) : customMenu; }, [ value, filteredOptions, searchText, splitOptions, isLabelPresent, handleDropdownClick, handleKeyDown, handleDropdownScroll, activeOptionIndex, loading, errorMessage, noDataMessage, dropdownRender, renderOptionWithIndex, onRetry, showIncompleteDataMessage, isScrolledToBottom, showRetryButton, isDarkMode, isDynamicVariable, ]); // Handle dropdown visibility changes const handleDropdownVisibleChange = useCallback((visible: boolean): void => { setIsOpen(visible); if (visible) { justOpenedRef.current = true; setActiveOptionIndex(0); } else { setSearchText(''); setActiveOptionIndex(-1); } }, []); // ===== Side Effects ===== // Clear search text when dropdown closes useEffect(() => { if (!isOpen) { setSearchText(''); setActiveOptionIndex(-1); } }, [isOpen]); // Auto-scroll to active option for keyboard navigation useEffect(() => { if ( isOpen && activeOptionIndex >= 0 && optionRefs.current[activeOptionIndex] ) { optionRefs.current[activeOptionIndex]?.scrollIntoView({ behavior: 'smooth', block: 'nearest', }); } }, [isOpen, activeOptionIndex]); // ===== Final Processing ===== // Apply highlight to matched text in options const optionsWithHighlight = useMemo( () => options ?.filter((option) => String(option.label || '') .toLowerCase() .includes(searchText.toLowerCase()), ) ?.map((option) => ({ ...option, label: highlightMatchedText(String(option.label || ''), searchText), })), [options, searchText, highlightMatchedText], ); // ===== Component Rendering ===== return (