import { isNil } from 'lodash'; import React from 'react'; import type { Option, OptionValues, ReactSelectProps } from 'react-select'; import Select, { Creatable } from 'react-select'; import VirtualizedSelect from 'react-virtualized-select'; import { StringsAsOptions } from './StringsAsOptions'; import { TetheredSelect } from '../../TetheredSelect'; import type { IFormInputProps, IFormInputValidation, OmitControlledInputPropsFrom } from './interface'; import { noop } from '../../../utils'; import { createFakeReactSyntheticEvent, isStringArray, orEmptyString } from './utils'; import { useValidationData } from '../validation'; export interface IReactSelectInputProps extends IFormInputProps, OmitControlledInputPropsFrom> { stringOptions?: string[]; mode?: 'TETHERED' | 'VIRTUALIZED' | 'PLAIN' | 'CREATABLE'; } // TODO: use standard css classes (from style guide?) // Currently the form-control class is needed for ng-invalid, but messes up the rendering of react-select export const reactSelectValidationErrorStyle = { borderColor: 'var(--color-danger)', WebkitBoxShadow: 'inset 0 1px 1px rgba(0, 0, 0, 0.075)', boxShadow: 'inset 0 1px 1px rgba(0, 0, 0, 0.075)', }; /** * Given a IControlledInputProps "field" (i.e., from Formik), returns an onChange handler * somewhat compatible with the controlled input pattern */ export const reactSelectOnChangeAdapter = (name: string, onChange: IReactSelectInputProps['onChange']) => { return (selection: Option | Option[]) => { const value = !selection ? null : Array.isArray(selection) ? selection.map((x) => x.value) : selection.value; const target = { name, value }; const event = createFakeReactSyntheticEvent(target); return (onChange || noop)(event); }; }; /** * Given a IControlledInputProps "field" (i.e., from Formik), returns an onBlur handler * somewhat compatible with the controlled input pattern */ export const reactSelectOnBlurAdapter = (name: string, value: any, onBlur: IReactSelectInputProps['onBlur']) => { return () => { const target = { name, value }; const event = createFakeReactSyntheticEvent(target); return (onBlur || noop)(event); }; }; /** * A react-select Input * * This input supports error validation state rendering. It adapts the onChange event to a controlled input event. * * This component does not attempt to support async loading */ export function ReactSelectInput(props: IReactSelectInputProps) { const { name, onChange, onBlur, value, validation = {} as IFormInputValidation, stringOptions, options: optionOptions, ignoreAccents: accents, inputClassName, ...otherProps } = props; // Default to false because this feature is SLOW const ignoreAccents = isNil(accents) ? false : accents; const mode = props.mode || 'TETHERED'; const className = orEmptyString(inputClassName); const { category } = useValidationData(validation.messageNode, validation.touched); const style = category === 'error' ? reactSelectValidationErrorStyle : {}; const fieldValue = props.multi ? (isNil(value) ? [] : value) : orEmptyString(value); const fieldProps = { name, value: fieldValue, onBlur: reactSelectOnBlurAdapter(name, value, onBlur), onChange: reactSelectOnChangeAdapter(name, onChange), }; const commonProps = { className, style, ignoreAccents, ...fieldProps, ...otherProps } as any; const SelectElement = ({ options }: { options: any[] }) => mode === 'TETHERED' ? ( ) : mode === 'VIRTUALIZED' ? ( ) : mode === 'CREATABLE' ? ( ) : (