import React, { useState, ChangeEvent, useRef, useEffect, useCallback, } from 'react'; import Icon from '../Icon'; import Label, { LabelProps } from '../Label/Label'; import classNames from 'classnames'; import './Autocomplete.scss'; import { createPortal } from 'react-dom'; type SelectedOptionType = string | null; export interface Option { id: number; label: string; } interface FilteredOption { original: Option; index: number; } interface AutocompleteProps { /** * Label props for Autocomplete */ labelProps?: LabelProps; /** * Set true or false to display the Label */ showLabel?: boolean; /** * set boolean value to ensure if error */ error?: boolean; /** * set boolean value to display/disabled */ disabled?: boolean; /** * set boolean value to display the border */ showBorder?: boolean; /** * Customize the border based on boolean */ showBorderOnHover?: boolean; /** * for customize the Autocomplete use className props(if required) */ className?: string; /** * for error message passed argument as string */ errorMsg?: string; /** * Required options for autocomplete in the appropriate manner */ options: Option[]; /** * placeholder for the Autocomplete */ placeholder: string; /** * for controlled component pass the onChange props */ onChange?: (option: Option) => void; /** * for clear the selected option from the input onClear is must */ onClear?: () => void; /** * By Default select one option selectedData is must */ selectedData?: Option; } interface DropdownListProps { positionX: number; positionY: number; dropdownWidth: number; filteredOptions: FilteredOption[]; focusedOptionIndex: number; selectedOptionIndex: number; updateOption: (option: Option, index: number) => void; setSearchTerm: React.Dispatch>; setShowDropdown: React.Dispatch>; showDropdown: boolean; onBlurHandler: () => void; dropdownDirection: 'down' | 'up'; } const DropdownList: React.FC = ({ positionX, positionY, dropdownWidth, filteredOptions, focusedOptionIndex, selectedOptionIndex, updateOption, setSearchTerm, setShowDropdown, showDropdown, onBlurHandler, dropdownDirection, }) => { const handleClearSearch = () => { setSearchTerm(''); setShowDropdown(false); }; return ( <> {showDropdown && (
)}
{filteredOptions.length > 0 ? ( ) : (
    no options
)}
); }; const Autocomplete: React.FC = ({ options = [], placeholder = '', labelProps, disabled, className, errorMsg, error, onChange, onClear, showLabel = true, showBorder = true, showBorderOnHover = true, selectedData, ...props }: AutocompleteProps) => { const [showDropdown, setShowDropdown] = useState(false); const [selectedOption, setSelectedOption] = useState(null); const [selectedOptionIndex, setSelectedOptionIndex] = useState(-1); const [searchTerm, setSearchTerm] = useState(''); const [focusedOptionIndex, setFocusedOptionIndex] = useState(-1); const [dropdownDirection, setDropdownDirection] = useState<'down' | 'up'>( 'down' ); const inputRef = useRef(null); const parentCheckingRef = useRef(null); const [dropdownPosition, setDropdownPosition] = useState({ posX: 0, posY: 0, width: 0, }); const OFFSET = 45; const getDropdownPosition = () => { if (parentCheckingRef?.current) { const rect = parentCheckingRef.current.getBoundingClientRect(); const viewportHeight = window.innerHeight; const spaceBelow = viewportHeight - rect.bottom; const minimumSpaceRequired = 200; const newDropdownDirection = spaceBelow >= minimumSpaceRequired ? 'down' : 'up'; setDropdownDirection(newDropdownDirection); setDropdownPosition({ posX: rect.left + window.scrollX, posY: newDropdownDirection === 'down' ? rect.bottom + window.scrollY : rect.top + window.scrollY - OFFSET, width: parentCheckingRef.current.offsetWidth, }); } }; const handleInput = () => { setShowDropdown(true); getDropdownPosition(); if (selectedOptionIndex !== -1) { setFocusedOptionIndex(selectedOptionIndex); } }; const updateOption = (option: Option, index: number) => { setSelectedOption(option.label); setSelectedOptionIndex(index); setSearchTerm(''); setShowDropdown(false); setFocusedOptionIndex(index); if (inputRef.current) { inputRef.current.focus(); inputRef.current.setSelectionRange( option.label.length, option.label.length ); } if (onChange) { onChange(option); } }; const handleInputChange = (event: ChangeEvent) => { const value = event.target.value; setSearchTerm(value); setShowDropdown(true); setFocusedOptionIndex(-1); if (value === '') { clearSelection(); } }; const handleKeyPress = (event: React.KeyboardEvent) => { if (event.key === 'Backspace' && searchTerm === '' && selectedOption) { clearSelection(); } }; const onBlurHandler = useCallback(() => { setDropdownPosition({ posX: 0, posY: 0, width: 0, }); setShowDropdown(false); }, []); const filteredOptions: FilteredOption[] = options .map((option, index) => ({ original: option, index, })) .filter((option) => option.original.label.toLowerCase().includes(searchTerm.toLowerCase()) ); useEffect(() => { filteredOptions.forEach((value) => { if (value?.original?.label === selectedData?.label) { updateOption(value?.original, value?.index); } }); }, [selectedData?.label]); const handleArrowKey = (event: KeyboardEvent) => { if (!showDropdown || disabled) return; if (event.key === 'ArrowDown') { setFocusedOptionIndex((prevIndex) => { const newIndex = Math.min(prevIndex + 1, filteredOptions.length - 1); scrollOptionIntoView(newIndex); return newIndex; }); } else if (event.key === 'ArrowUp') { setFocusedOptionIndex((prevIndex) => { const newIndex = Math.max(prevIndex - 1, 0); scrollOptionIntoView(newIndex); return newIndex; }); } else if (event.key === 'Enter' && focusedOptionIndex >= 0) { updateOption( filteredOptions[focusedOptionIndex].original, filteredOptions[focusedOptionIndex].index ); } }; const scrollOptionIntoView = (index: number) => { const optionElement = document.querySelector(`#option-${index}`); if (optionElement) { optionElement.scrollIntoView({ block: 'nearest' }); } }; const clearSelection = () => { setSelectedOption(null); setSearchTerm(''); setFocusedOptionIndex(-1); setSelectedOptionIndex(-1); setShowDropdown(false); if (onClear) { onClear(); } }; useEffect(() => { document.addEventListener('keydown', handleArrowKey); return () => { document.removeEventListener('keydown', handleArrowKey); }; }, [showDropdown, filteredOptions, focusedOptionIndex]); useEffect(() => { const updateElementPosition = () => { if (parentCheckingRef.current) { const rect = parentCheckingRef.current.getBoundingClientRect(); setDropdownPosition({ posX: rect.left + window.scrollX, posY: dropdownDirection === 'down' ? rect.bottom + window.scrollY : rect.top + window.scrollY - OFFSET, width: parentCheckingRef.current.offsetWidth, }); } }; const disableScroll = () => { const bodyScrollWidth = window.innerWidth - document.body.clientWidth; if (document.body.scrollHeight > window.innerHeight) { document.body.style.paddingRight = `${bodyScrollWidth}px`; } document.body.style.overflow = 'hidden'; }; const enableScroll = () => { document.body.style.paddingRight = ''; document.body.style.overflow = ''; }; if (showDropdown) { disableScroll(); updateElementPosition(); } else { enableScroll(); } const handleResize = () => { updateElementPosition(); }; window.addEventListener('resize', handleResize); window.addEventListener('scroll', handleResize, true); return () => { enableScroll(); window.removeEventListener('resize', handleResize); window.removeEventListener('scroll', handleResize); }; }, [showDropdown, dropdownDirection]); const toggleDropdown = () => { setShowDropdown((prevState) => !prevState); }; return ( <>
{showLabel && (
); }; export default Autocomplete;