import React from 'react'; import {CircularProgress, Grid, TextField, Typography} from '@material-ui/core'; import {makeStyles} from '@material-ui/core/styles'; import {Autocomplete} from '@material-ui/lab'; import {VariableSizeList} from 'react-window'; import {getIn, useField, useFormikContext} from 'formik'; import {usePrevious} from '@selectquotelabs/sqhooks'; import {useForm} from './useForm'; import { ListboxVirtualizedComponentProps, OuterElementContextInterface, OuterElementTypeProps, SQFormAutocompleteProps, } from './SQFormAutocomplete'; import type {ListChildComponentProps} from 'react-window'; import type {AutocompleteProps} from '@material-ui/lab'; import type {SQFormOption, SQFormOptionValue} from '../../types'; export interface SQFormAsyncAutocompleteProps extends SQFormAutocompleteProps { /** updates consumer's local state value for the input, which is passed to a useQuery hook */ handleAsyncInputChange: (value: SQFormOptionValue) => void; /** Whether the component is loading */ loading?: boolean; /** Whether the popup is open */ open?: boolean; /** Callback for when the popup requests to be opened */ onOpen?: AutocompleteProps<SQFormOptionValue, false, false, false>['onOpen']; /** Callback for when the popup requests to be closed */ onClose?: AutocompleteProps< SQFormOptionValue, false, false, false >['onClose']; } // MUI uses px, a numeric value is needed for calculations const LISTBOX_PADDING = 8; // px const useStyles = makeStyles({ listbox: { '& ul': { padding: 0, margin: 0, }, }, }); const OuterElementContext = React.createContext<OuterElementContextInterface | null>({}); const OuterElementType = React.forwardRef<HTMLDivElement>( (props: OuterElementTypeProps, ref) => { const outerProps = React.useContext(OuterElementContext); return <div ref={ref} {...props} {...outerProps} />; } ); function renderRow({data, index, style}: ListChildComponentProps) { return React.cloneElement(data[index], { style: { ...style, top: (style.top as number) + LISTBOX_PADDING, }, }); } // Adapter for react-window const ListboxVirtualizedComponent = React.forwardRef<HTMLDivElement>( function ListboxVirtualizedComponent( { basewidth, left, lockWidthToField, ...restProps }: ListboxVirtualizedComponentProps, ref ): React.ReactElement { const {children, ...listboxProps} = restProps; const LIST_MAX_VIEWABLE_ITEMS = 8; const LIST_OVERSCAN_COUNT = 5; const items = React.Children.toArray(children); const ITEM_COUNT = items.length; const ITEM_SIZE = 36; const height = React.useMemo(() => { if (ITEM_COUNT > LIST_MAX_VIEWABLE_ITEMS) { return LIST_MAX_VIEWABLE_ITEMS * ITEM_SIZE; } return items.length * ITEM_SIZE + 2 * LISTBOX_PADDING; }, [ITEM_COUNT, items]); const getItemSize = React.useCallback(() => ITEM_SIZE, []); return ( <div ref={ref}> <OuterElementContext.Provider value={listboxProps}> <VariableSizeList itemData={items} height={height} width="100%" key={ITEM_COUNT} outerElementType={OuterElementType} innerElementType="ul" itemSize={getItemSize} overscanCount={LIST_OVERSCAN_COUNT} itemCount={ITEM_COUNT} > {renderRow} </VariableSizeList> </OuterElementContext.Provider> </div> ); } as React.ForwardRefRenderFunction<HTMLDivElement> ); function SQFormAsyncAutocomplete({ children, isDisabled = false, label, name, onBlur, onChange, onInputChange, handleAsyncInputChange, loading = false, open, onOpen, onClose, size = 'auto', }: SQFormAsyncAutocompleteProps): React.ReactElement { const classes = useStyles(); const {setFieldValue, setTouched, values} = useFormikContext(); const [{value}] = useField(name); const { fieldState: {isFieldError, isFieldRequired}, fieldHelpers: {HelperTextComponent}, } = useForm({name}); const initialValue = React.useMemo(() => { const optionInitialValue = children.find((option) => { if (option.value === value) { return option; } return null; }); return optionInitialValue; }, [children, value]); const [inputValue, setInputValue] = React.useState(''); const prevValue = usePrevious(value); React.useEffect(() => { setInputValue(initialValue?.label || ''); }, [initialValue]); React.useEffect(() => { // Form Reset if (prevValue && inputValue && !value) { setInputValue(''); } }, [value, inputValue, name, prevValue, values]); const handleAutocompleteBlur = React.useCallback( (event) => { setTouched({[name]: true}); onBlur && onBlur(event); }, [name, onBlur, setTouched] ); const handleAutocompleteChange = React.useCallback( (event, value, reason) => { const selectedValue = getIn(value, 'value'); onChange && onChange(event, selectedValue, reason); if (reason === 'clear') { return setFieldValue(name, ''); } return setFieldValue(name, selectedValue); }, [name, onChange, setFieldValue] ); const handleInputChange = React.useCallback( (event, value) => { setInputValue(value); handleAsyncInputChange(value); onInputChange && onInputChange(event, value); }, [handleAsyncInputChange, onInputChange] ); return ( <Grid item sm={size}> <Autocomplete id={name} style={{width: '100%'}} disableListWrap classes={classes} ListboxComponent={ ListboxVirtualizedComponent as React.ComponentType< React.HTMLAttributes<HTMLElement> > } options={children} onBlur={handleAutocompleteBlur} onChange={handleAutocompleteChange} onInputChange={handleInputChange} open={open} onOpen={onOpen} onClose={onClose} inputValue={inputValue} disabled={isDisabled} getOptionLabel={(option) => option.label} getOptionDisabled={(option: SQFormOption) => option.isDisabled || false} renderInput={(params) => { return ( <TextField {...params} color="primary" disabled={isDisabled} error={isFieldError} fullWidth={true} InputLabelProps={{ ...params.InputLabelProps, shrink: true, }} inputProps={{ ...params.inputProps, disabled: isDisabled, }} InputProps={{ ...params.InputProps, endAdornment: ( <> {loading ? ( <CircularProgress color="inherit" size={20} /> ) : null} {params.InputProps.endAdornment} </> ), }} FormHelperTextProps={{error: isFieldError}} name={name} label={label} helperText={!isDisabled && HelperTextComponent} required={isFieldRequired} /> ); }} renderOption={(option) => ( <Typography variant="body2" noWrap> {option.label} </Typography> )} /> </Grid> ); } export default SQFormAsyncAutocomplete;