import React from 'react'; import { SelectableValue, deprecationWarning } from '@grafana/data'; // @ts-ignore import { default as ReactSelect } from '@torkelo/react-select'; // @ts-ignore import Creatable from '@torkelo/react-select/creatable'; // @ts-ignore import { default as ReactAsyncSelect } from '@torkelo/react-select/async'; // @ts-ignore import { default as AsyncCreatable } from '@torkelo/react-select/async-creatable'; import { Icon } from '../../Icon/Icon'; import { css, cx } from 'emotion'; import { inputSizes } from '../commonStyles'; import { FormInputSize } from '../types'; import resetSelectStyles from './resetSelectStyles'; import { SelectMenu, SelectMenuOptions } from './SelectMenu'; import { IndicatorsContainer } from './IndicatorsContainer'; import { ValueContainer } from './ValueContainer'; import { InputControl } from './InputControl'; import { DropdownIndicator } from './DropdownIndicator'; import { SelectOptionGroup } from './SelectOptionGroup'; import { SingleValue } from './SingleValue'; import { MultiValueContainer, MultiValueRemove } from './MultiValue'; import { useTheme } from '../../../themes'; import { getSelectStyles } from './getSelectStyles'; type SelectValue = T | SelectableValue | T[] | Array>; export interface SelectCommonProps { className?: string; options?: Array>; defaultValue?: any; inputValue?: string; value?: SelectValue; getOptionLabel?: (item: SelectableValue) => string; getOptionValue?: (item: SelectableValue) => string; onCreateOption?: (value: string) => void; onChange: (value: SelectableValue) => {} | void; onInputChange?: (label: string) => void; onKeyDown?: (event: React.KeyboardEvent) => void; placeholder?: string; disabled?: boolean; isSearchable?: boolean; isClearable?: boolean; autoFocus?: boolean; openMenuOnFocus?: boolean; onBlur?: () => void; maxMenuHeight?: number; isLoading?: boolean; noOptionsMessage?: string; isMulti?: boolean; backspaceRemovesValue?: boolean; isOpen?: boolean; components?: any; onOpenMenu?: () => void; onCloseMenu?: () => void; tabSelectsValue?: boolean; formatCreateLabel?: (input: string) => string; allowCustomValue?: boolean; width?: number; size?: FormInputSize; /** item to be rendered in front of the input */ prefix?: JSX.Element | string | null; /** Use a custom element to control Select. A proper ref to the renderControl is needed if 'portal' isn't set to null*/ renderControl?: ControlComponent; /** An element where the dropdown menu should be rendered. In all Select implementations it defaults to document.body .*/ portal?: HTMLElement | null; } export interface SelectAsyncProps { /** When specified as boolean the loadOptions will execute when component is mounted */ defaultOptions?: boolean | Array>; /** Asynchroniously load select options */ loadOptions?: (query: string) => Promise>>; /** Message to display when options are loading */ loadingMessage?: string; } export interface MultiSelectCommonProps extends Omit, 'onChange' | 'isMulti' | 'value'> { value?: Array> | T[]; onChange: (item: Array>) => {} | void; } export interface SelectBaseProps extends SelectCommonProps, SelectAsyncProps { invalid?: boolean; portal: HTMLElement | null; } export interface CustomControlProps { ref: React.Ref; isOpen: boolean; /** Currently selected value */ value?: SelectableValue; /** onClick will be automatically passed to custom control allowing menu toggle */ onClick: () => void; /** onBlur will be automatically passed to custom control closing the menu on element blur */ onBlur: () => void; disabled: boolean; invalid: boolean; } export type ControlComponent = React.ComponentType>; const CustomControl = (props: any) => { const { children, innerProps, selectProps: { menuIsOpen, onMenuClose, onMenuOpen }, isFocused, isMulti, getValue, innerRef, } = props; const selectProps = props.selectProps as SelectBaseProps; if (selectProps.renderControl) { return React.createElement(selectProps.renderControl, { isOpen: menuIsOpen, value: isMulti ? getValue() : getValue()[0], ref: innerRef, onClick: menuIsOpen ? onMenuClose : onMenuOpen, onBlur: onMenuClose, disabled: !!selectProps.disabled, invalid: !!selectProps.invalid, }); } return ( {children} ); }; export function SelectBase({ value, defaultValue, inputValue, onInputChange, onCreateOption, options = [], onChange, onBlur, onKeyDown, onCloseMenu, onOpenMenu, placeholder = 'Choose', getOptionValue, getOptionLabel, isSearchable = true, disabled = false, isClearable = false, isMulti = false, isLoading = false, isOpen, autoFocus = false, openMenuOnFocus = false, maxMenuHeight = 300, noOptionsMessage = 'No options found', tabSelectsValue = true, backspaceRemovesValue = true, allowCustomValue = false, size = 'auto', prefix, formatCreateLabel, loadOptions, loadingMessage = 'Loading options...', defaultOptions, renderControl, width, invalid, portal, components, }: SelectBaseProps) { const theme = useTheme(); const styles = getSelectStyles(theme); let ReactSelectComponent: ReactSelect | Creatable = ReactSelect; const creatableProps: any = {}; let asyncSelectProps: any = {}; let selectedValue = []; if (isMulti && loadOptions) { selectedValue = value as any; } else { // If option is passed as a plain value (value property from SelectableValue property) // we are selecting the corresponding value from the options if (isMulti && value && Array.isArray(value) && !loadOptions) { // @ts-ignore selectedValue = value.map(v => { return options.filter(o => { return v === o.value || o.value === v.value; })[0]; }); } else { selectedValue = options.filter(o => o.value === value || o === value); } } const commonSelectProps = { autoFocus, placeholder, isSearchable, // Passing isDisabled as react-select accepts this prop isDisabled: disabled, // Also passing disabled, as this is the new Select API, and I want to use this prop instead of react-select's one disabled, invalid, prefix, isClearable, isLoading, menuIsOpen: isOpen, defaultValue, inputValue, onInputChange, value: isMulti ? selectedValue : selectedValue[0], getOptionLabel, getOptionValue, openMenuOnFocus, maxMenuHeight, isMulti, backspaceRemovesValue, onMenuOpen: onOpenMenu, onMenuClose: onCloseMenu, tabSelectsValue, options, onChange, onBlur, onKeyDown, menuShouldScrollIntoView: false, renderControl, captureMenuScroll: false, blurInputOnSelect: true, menuPortalTarget: portal, menuPlacement: 'auto', }; // width property is deprecated in favor of size or className let widthClass = ''; if (width) { deprecationWarning('Select', 'width property', 'size or className'); widthClass = 'width-' + width; } if (allowCustomValue) { ReactSelectComponent = Creatable; creatableProps.formatCreateLabel = formatCreateLabel ?? ((input: string) => `Create: ${input}`); creatableProps.onCreateOption = onCreateOption; } // Instead of having AsyncSelect, as a separate component we render ReactAsyncSelect if (loadOptions) { ReactSelectComponent = allowCustomValue ? AsyncCreatable : ReactAsyncSelect; asyncSelectProps = { loadOptions, defaultOptions, }; } return ( (
{props.children}
), SelectContainer: (props: any) => (
{props.children}
), IndicatorsContainer: IndicatorsContainer, IndicatorSeparator: () => <>, Control: CustomControl, Option: SelectMenuOptions, ClearIndicator: (props: any) => { const { clearValue } = props; return ( { e.preventDefault(); e.stopPropagation(); clearValue(); }} /> ); }, LoadingIndicator: (props: any) => { return ; }, LoadingMessage: (props: any) => { return
{loadingMessage}
; }, NoOptionsMessage: (props: any) => { return (
{noOptionsMessage}
); }, DropdownIndicator: (props: any) => , SingleValue: SingleValue, MultiValueContainer: MultiValueContainer, MultiValueRemove: MultiValueRemove, ...components, }} styles={{ ...resetSelectStyles(), //These are required for the menu positioning to function menu: ({ top, bottom, width, position }: any) => ({ top, bottom, width, position, marginBottom: !!bottom ? '10px' : '0', }), }} className={widthClass} {...commonSelectProps} {...creatableProps} {...asyncSelectProps} /> ); }