import { autoUpdate, flip, size, useFloating } from '@floating-ui/react-dom'; import classNames from 'classnames'; import React, { FocusEvent, FormEvent, KeyboardEvent, KeyboardEventHandler, MutableRefObject, ReactNode, Ref, SyntheticEvent, useCallback, useEffect, useRef, } from 'react'; import Autosuggest, { AutosuggestPropsBase, BlurEvent, ChangeEvent, RenderInputComponentProps, RenderSuggestionsContainerParams, } from 'react-autosuggest'; import usePrevious from 'Helpers/Hooks/usePrevious'; import { InputChanged } from 'typings/inputs'; import styles from './AutoSuggestInput.css'; interface AutoSuggestInputProps extends Omit, 'renderInputComponent' | 'inputProps'> { forwardedRef?: MutableRefObject | null>; className?: string; inputContainerClassName?: string; name: string; value?: string; placeholder?: string; suggestions: T[]; hasError?: boolean; hasWarning?: boolean; enforceMaxHeight?: boolean; maxHeight?: number; renderInputComponent?: ( inputProps: RenderInputComponentProps, ref: Ref ) => ReactNode; onInputChange: ( event: FormEvent, params: ChangeEvent ) => unknown; onInputKeyDown?: KeyboardEventHandler; onInputFocus?: (event: SyntheticEvent) => unknown; onInputBlur: ( event: FocusEvent, params?: BlurEvent ) => unknown; onChange?: (change: InputChanged) => unknown; } // eslint-disable-next-line @typescript-eslint/no-explicit-any function AutoSuggestInput(props: AutoSuggestInputProps) { const { // TODO: forwaredRef should be replaces with React.forwardRef forwardedRef, className = styles.input, inputContainerClassName = styles.inputContainer, name, value = '', placeholder, suggestions, enforceMaxHeight = true, hasError, hasWarning, maxHeight = 200, getSuggestionValue, renderSuggestion, renderInputComponent, onInputChange, onInputKeyDown, onInputFocus, onInputBlur, onSuggestionsFetchRequested, onSuggestionsClearRequested, onSuggestionSelected, onChange, ...otherProps } = props; const updater = useRef<(() => void) | null>(null); const previousSuggestions = usePrevious(suggestions); const { refs, floatingStyles } = useFloating({ middleware: [ flip({ crossAxis: false, mainAxis: true, }), size({ apply({ availableHeight, elements, rects }) { Object.assign(elements.floating.style, { minWidth: `${rects.reference.width}px`, maxHeight: `${Math.max(0, availableHeight)}px`, }); }, }), ], placement: 'bottom-start', whileElementsMounted: autoUpdate, }); const createRenderInputComponent = useCallback( (inputProps: RenderInputComponentProps) => { if (renderInputComponent) { return renderInputComponent(inputProps, refs.setReference); } return (
); }, [refs.setReference, renderInputComponent] ); const renderSuggestionsContainer = useCallback( ({ containerProps, children }: RenderSuggestionsContainerParams) => { return (
{children}
); }, [enforceMaxHeight, floatingStyles, maxHeight, refs.setFloating] ); const handleInputKeyDown = useCallback( (event: KeyboardEvent) => { if ( event.key === 'Tab' && suggestions.length && suggestions[0] !== value ) { event.preventDefault(); if (value) { onSuggestionSelected?.(event, { suggestion: suggestions[0], suggestionValue: value, suggestionIndex: 0, sectionIndex: null, method: 'enter', }); } } }, [value, suggestions, onSuggestionSelected] ); const inputProps = { className: classNames( className, hasError && styles.hasError, hasWarning && styles.hasWarning ), name, value, placeholder, autoComplete: 'off', spellCheck: false, onChange: onInputChange, onKeyDown: onInputKeyDown || handleInputKeyDown, onFocus: onInputFocus, onBlur: onInputBlur, }; const theme = { container: inputContainerClassName, containerOpen: styles.suggestionsContainerOpen, suggestionsContainer: styles.suggestionsContainer, suggestionsList: styles.suggestionsList, suggestion: styles.suggestion, suggestionHighlighted: styles.suggestionHighlighted, }; useEffect(() => { if (updater.current && suggestions !== previousSuggestions) { updater.current(); } }, [suggestions, previousSuggestions]); return ( ); } export default AutoSuggestInput;