2021-12-13 14:59:51 +01:00
|
|
|
import React, { ComponentProps, useCallback, useEffect, useRef, useState } from 'react';
|
2021-04-02 11:11:46 +02:00
|
|
|
import { default as ReactSelect } from 'react-select';
|
|
|
|
|
import Creatable from 'react-select/creatable';
|
|
|
|
|
import { default as ReactAsyncSelect } from 'react-select/async';
|
|
|
|
|
import { default as AsyncCreatable } from 'react-select/async-creatable';
|
2020-01-07 09:20:06 +01:00
|
|
|
|
2020-04-02 10:57:35 +02:00
|
|
|
import { Icon } from '../Icon/Icon';
|
2020-11-04 13:34:40 +01:00
|
|
|
import { Spinner } from '../Spinner/Spinner';
|
2021-12-15 13:33:35 +01:00
|
|
|
import { useCustomSelectStyles } from './resetSelectStyles';
|
2020-01-07 09:20:06 +01:00
|
|
|
import { SelectMenu, SelectMenuOptions } from './SelectMenu';
|
|
|
|
|
import { IndicatorsContainer } from './IndicatorsContainer';
|
|
|
|
|
import { ValueContainer } from './ValueContainer';
|
|
|
|
|
import { InputControl } from './InputControl';
|
2021-10-19 12:29:33 +01:00
|
|
|
import { SelectContainer } from './SelectContainer';
|
2020-01-07 09:20:06 +01:00
|
|
|
import { DropdownIndicator } from './DropdownIndicator';
|
|
|
|
|
import { SelectOptionGroup } from './SelectOptionGroup';
|
|
|
|
|
import { SingleValue } from './SingleValue';
|
|
|
|
|
import { MultiValueContainer, MultiValueRemove } from './MultiValue';
|
2021-04-21 14:25:43 +02:00
|
|
|
import { useTheme2 } from '../../themes';
|
2020-01-07 09:20:06 +01:00
|
|
|
import { getSelectStyles } from './getSelectStyles';
|
2020-12-14 15:11:25 +02:00
|
|
|
import { cleanValue, findSelectedValue } from './utils';
|
2021-11-09 18:20:36 +01:00
|
|
|
import { ActionMeta, SelectBaseProps, SelectValue } from './types';
|
2020-01-07 09:20:06 +01:00
|
|
|
|
2020-04-21 16:06:34 +01:00
|
|
|
interface ExtraValuesIndicatorProps {
|
|
|
|
|
maxVisibleValues?: number | undefined;
|
|
|
|
|
selectedValuesCount: number;
|
|
|
|
|
menuIsOpen: boolean;
|
|
|
|
|
showAllSelectedWhenOpen: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const renderExtraValuesIndicator = (props: ExtraValuesIndicatorProps) => {
|
|
|
|
|
const { maxVisibleValues, selectedValuesCount, menuIsOpen, showAllSelectedWhenOpen } = props;
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
maxVisibleValues !== undefined &&
|
|
|
|
|
selectedValuesCount > maxVisibleValues &&
|
|
|
|
|
!(showAllSelectedWhenOpen && menuIsOpen)
|
|
|
|
|
) {
|
|
|
|
|
return (
|
|
|
|
|
<span key="excess-values" id="excess-values">
|
|
|
|
|
(+{selectedValuesCount - maxVisibleValues})
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
};
|
|
|
|
|
|
2020-01-07 09:20:06 +01:00
|
|
|
const CustomControl = (props: any) => {
|
|
|
|
|
const {
|
|
|
|
|
children,
|
|
|
|
|
innerProps,
|
|
|
|
|
selectProps: { menuIsOpen, onMenuClose, onMenuOpen },
|
|
|
|
|
isFocused,
|
|
|
|
|
isMulti,
|
|
|
|
|
getValue,
|
|
|
|
|
innerRef,
|
|
|
|
|
} = props;
|
|
|
|
|
const selectProps = props.selectProps as SelectBaseProps<any>;
|
|
|
|
|
|
|
|
|
|
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 (
|
|
|
|
|
<InputControl
|
|
|
|
|
ref={innerRef}
|
|
|
|
|
innerProps={innerProps}
|
|
|
|
|
prefix={selectProps.prefix}
|
|
|
|
|
focused={isFocused}
|
|
|
|
|
invalid={!!selectProps.invalid}
|
|
|
|
|
disabled={!!selectProps.disabled}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</InputControl>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function SelectBase<T>({
|
2020-03-10 08:41:38 +02:00
|
|
|
allowCustomValue = false,
|
2021-10-21 14:55:02 +01:00
|
|
|
allowCreateWhileLoading = false,
|
2021-02-16 16:19:55 +00:00
|
|
|
'aria-label': ariaLabel,
|
2020-03-10 08:41:38 +02:00
|
|
|
autoFocus = false,
|
|
|
|
|
backspaceRemovesValue = true,
|
2020-04-25 21:48:20 +01:00
|
|
|
cacheOptions,
|
2020-04-23 08:18:53 +03:00
|
|
|
className,
|
2020-04-21 16:06:34 +01:00
|
|
|
closeMenuOnSelect = true,
|
2020-03-10 08:41:38 +02:00
|
|
|
components,
|
|
|
|
|
defaultOptions,
|
2020-01-07 09:20:06 +01:00
|
|
|
defaultValue,
|
|
|
|
|
disabled = false,
|
2020-05-04 12:43:09 +03:00
|
|
|
filterOption,
|
2020-03-10 08:41:38 +02:00
|
|
|
formatCreateLabel,
|
|
|
|
|
getOptionLabel,
|
|
|
|
|
getOptionValue,
|
|
|
|
|
inputValue,
|
|
|
|
|
invalid,
|
2020-01-07 09:20:06 +01:00
|
|
|
isClearable = false,
|
2021-02-16 16:19:55 +00:00
|
|
|
id,
|
2020-01-07 09:20:06 +01:00
|
|
|
isLoading = false,
|
2020-03-10 08:41:38 +02:00
|
|
|
isMulti = false,
|
2021-02-16 16:19:55 +00:00
|
|
|
inputId,
|
2020-01-07 09:20:06 +01:00
|
|
|
isOpen,
|
2020-04-25 21:48:20 +01:00
|
|
|
isOptionDisabled,
|
2020-03-10 08:41:38 +02:00
|
|
|
isSearchable = true,
|
|
|
|
|
loadOptions,
|
|
|
|
|
loadingMessage = 'Loading options...',
|
2020-01-07 09:20:06 +01:00
|
|
|
maxMenuHeight = 300,
|
2020-10-09 09:34:57 +02:00
|
|
|
minMenuHeight,
|
2020-04-21 16:06:34 +01:00
|
|
|
maxVisibleValues,
|
2021-07-14 14:04:23 +01:00
|
|
|
menuPlacement = 'auto',
|
2020-04-23 08:18:53 +03:00
|
|
|
menuPosition,
|
2022-02-10 11:46:35 +00:00
|
|
|
// TODO change this to default to true for Grafana 9
|
2021-08-04 15:47:53 +01:00
|
|
|
menuShouldPortal = false,
|
2020-01-07 09:20:06 +01:00
|
|
|
noOptionsMessage = 'No options found',
|
2020-03-10 08:41:38 +02:00
|
|
|
onBlur,
|
|
|
|
|
onChange,
|
|
|
|
|
onCloseMenu,
|
|
|
|
|
onCreateOption,
|
|
|
|
|
onInputChange,
|
|
|
|
|
onKeyDown,
|
|
|
|
|
onOpenMenu,
|
|
|
|
|
openMenuOnFocus = false,
|
|
|
|
|
options = [],
|
|
|
|
|
placeholder = 'Choose',
|
2020-01-07 09:20:06 +01:00
|
|
|
prefix,
|
|
|
|
|
renderControl,
|
2020-04-21 16:06:34 +01:00
|
|
|
showAllSelectedWhenOpen = true,
|
2020-03-10 08:41:38 +02:00
|
|
|
tabSelectsValue = true,
|
|
|
|
|
value,
|
2020-01-07 09:20:06 +01:00
|
|
|
width,
|
2021-06-25 11:05:36 +01:00
|
|
|
isValidNewOption,
|
2022-02-07 15:18:17 +01:00
|
|
|
formatOptionLabel,
|
2020-01-07 09:20:06 +01:00
|
|
|
}: SelectBaseProps<T>) {
|
2021-04-21 14:25:43 +02:00
|
|
|
const theme = useTheme2();
|
2020-01-07 09:20:06 +01:00
|
|
|
const styles = getSelectStyles(theme);
|
2021-12-13 14:59:51 +01:00
|
|
|
|
|
|
|
|
const reactSelectRef = useRef<{ controlRef: HTMLElement }>(null);
|
|
|
|
|
const [closeToBottom, setCloseToBottom] = useState<boolean>(false);
|
2021-12-15 13:33:35 +01:00
|
|
|
const selectStyles = useCustomSelectStyles(theme, width);
|
2021-12-13 14:59:51 +01:00
|
|
|
|
|
|
|
|
// Infer the menu position for asynchronously loaded options. menuPlacement="auto" doesn't work when the menu is
|
|
|
|
|
// automatically opened when the component is created (it happens in SegmentSelect by setting menuIsOpen={true}).
|
|
|
|
|
// We can remove this workaround when the bug in react-select is fixed: https://github.com/JedWatson/react-select/issues/4936
|
|
|
|
|
// Note: we use useEffect instead of hooking into onMenuOpen due to another bug: https://github.com/JedWatson/react-select/issues/3375
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (
|
|
|
|
|
loadOptions &&
|
|
|
|
|
isOpen &&
|
|
|
|
|
reactSelectRef.current &&
|
|
|
|
|
reactSelectRef.current.controlRef &&
|
|
|
|
|
menuPlacement === 'auto'
|
|
|
|
|
) {
|
|
|
|
|
const distance = window.innerHeight - reactSelectRef.current.controlRef.getBoundingClientRect().bottom;
|
|
|
|
|
setCloseToBottom(distance < maxMenuHeight);
|
|
|
|
|
}
|
|
|
|
|
}, [maxMenuHeight, menuPlacement, loadOptions, isOpen]);
|
|
|
|
|
|
2020-02-25 15:07:13 +01:00
|
|
|
const onChangeWithEmpty = useCallback(
|
2021-11-09 18:20:36 +01:00
|
|
|
(value: SelectValue<T>, action: ActionMeta) => {
|
2020-02-25 15:07:13 +01:00
|
|
|
if (isMulti && (value === undefined || value === null)) {
|
2021-11-09 18:20:36 +01:00
|
|
|
return onChange([], action);
|
2020-02-25 15:07:13 +01:00
|
|
|
}
|
2021-11-09 18:20:36 +01:00
|
|
|
onChange(value, action);
|
2020-02-25 15:07:13 +01:00
|
|
|
},
|
2020-10-21 09:06:41 +02:00
|
|
|
[isMulti, onChange]
|
2020-02-25 15:07:13 +01:00
|
|
|
);
|
2021-04-02 11:11:46 +02:00
|
|
|
|
|
|
|
|
let ReactSelectComponent = ReactSelect;
|
|
|
|
|
|
2021-06-25 11:05:36 +01:00
|
|
|
const creatableProps: ComponentProps<typeof Creatable> = {};
|
2020-01-07 09:20:06 +01:00
|
|
|
let asyncSelectProps: any = {};
|
2021-01-27 19:10:18 +02:00
|
|
|
let selectedValue;
|
2020-01-07 09:20:06 +01:00
|
|
|
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
|
2021-01-20 07:59:48 +01:00
|
|
|
selectedValue = value.map((v) => findSelectedValue(v.value ?? v, options));
|
2020-02-13 10:13:03 +00:00
|
|
|
} else if (loadOptions) {
|
|
|
|
|
const hasValue = defaultValue || value;
|
|
|
|
|
selectedValue = hasValue ? [hasValue] : [];
|
2020-01-07 09:20:06 +01:00
|
|
|
} else {
|
2020-03-03 16:09:52 +02:00
|
|
|
selectedValue = cleanValue(value, options);
|
2020-01-07 09:20:06 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const commonSelectProps = {
|
2021-02-16 16:19:55 +00:00
|
|
|
'aria-label': ariaLabel,
|
2020-01-07 09:20:06 +01:00
|
|
|
autoFocus,
|
2020-03-10 08:41:38 +02:00
|
|
|
backspaceRemovesValue,
|
|
|
|
|
captureMenuScroll: false,
|
2020-04-21 16:06:34 +01:00
|
|
|
closeMenuOnSelect,
|
2021-07-14 14:04:23 +01:00
|
|
|
// We don't want to close if we're actually scrolling the menu
|
|
|
|
|
// So only close if none of the parents are the select menu itself
|
2020-03-10 08:41:38 +02:00
|
|
|
defaultValue,
|
2020-01-07 09:20:06 +01:00
|
|
|
// Also passing disabled, as this is the new Select API, and I want to use this prop instead of react-select's one
|
|
|
|
|
disabled,
|
2020-05-04 12:43:09 +03:00
|
|
|
filterOption,
|
2020-03-10 08:41:38 +02:00
|
|
|
getOptionLabel,
|
|
|
|
|
getOptionValue,
|
|
|
|
|
inputValue,
|
2020-01-07 09:20:06 +01:00
|
|
|
invalid,
|
|
|
|
|
isClearable,
|
2021-02-16 16:19:55 +00:00
|
|
|
id,
|
2020-03-10 08:41:38 +02:00
|
|
|
// Passing isDisabled as react-select accepts this prop
|
|
|
|
|
isDisabled: disabled,
|
2020-01-07 09:20:06 +01:00
|
|
|
isLoading,
|
2020-03-10 08:41:38 +02:00
|
|
|
isMulti,
|
2021-02-16 16:19:55 +00:00
|
|
|
inputId,
|
2020-04-25 21:48:20 +01:00
|
|
|
isOptionDisabled,
|
2020-03-10 08:41:38 +02:00
|
|
|
isSearchable,
|
|
|
|
|
maxMenuHeight,
|
2020-10-09 09:34:57 +02:00
|
|
|
minMenuHeight,
|
2020-04-21 16:06:34 +01:00
|
|
|
maxVisibleValues,
|
2020-01-07 09:20:06 +01:00
|
|
|
menuIsOpen: isOpen,
|
2021-12-13 14:59:51 +01:00
|
|
|
menuPlacement: menuPlacement === 'auto' && closeToBottom ? 'top' : menuPlacement,
|
2020-03-10 08:41:38 +02:00
|
|
|
menuPosition,
|
2021-07-15 10:54:38 +01:00
|
|
|
menuShouldBlockScroll: true,
|
2021-08-04 15:47:53 +01:00
|
|
|
menuPortalTarget: menuShouldPortal ? document.body : undefined,
|
2020-03-10 08:41:38 +02:00
|
|
|
menuShouldScrollIntoView: false,
|
|
|
|
|
onBlur,
|
|
|
|
|
onChange: onChangeWithEmpty,
|
2020-01-17 11:30:33 +01:00
|
|
|
onInputChange,
|
2020-03-10 08:41:38 +02:00
|
|
|
onKeyDown,
|
2020-01-07 09:20:06 +01:00
|
|
|
onMenuClose: onCloseMenu,
|
2020-03-10 08:41:38 +02:00
|
|
|
onMenuOpen: onOpenMenu,
|
2022-02-07 15:18:17 +01:00
|
|
|
formatOptionLabel,
|
2020-03-10 08:41:38 +02:00
|
|
|
openMenuOnFocus,
|
2020-01-07 09:20:06 +01:00
|
|
|
options,
|
2020-03-10 08:41:38 +02:00
|
|
|
placeholder,
|
|
|
|
|
prefix,
|
2020-01-07 09:20:06 +01:00
|
|
|
renderControl,
|
2020-04-21 16:06:34 +01:00
|
|
|
showAllSelectedWhenOpen,
|
2020-03-10 08:41:38 +02:00
|
|
|
tabSelectsValue,
|
2021-01-27 19:10:18 +02:00
|
|
|
value: isMulti ? selectedValue : selectedValue?.[0],
|
2020-01-07 09:20:06 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (allowCustomValue) {
|
2021-04-02 11:11:46 +02:00
|
|
|
ReactSelectComponent = Creatable as any;
|
2021-10-21 14:55:02 +01:00
|
|
|
creatableProps.allowCreateWhileLoading = allowCreateWhileLoading;
|
2020-01-07 09:20:06 +01:00
|
|
|
creatableProps.formatCreateLabel = formatCreateLabel ?? ((input: string) => `Create: ${input}`);
|
2020-01-31 13:47:34 +01:00
|
|
|
creatableProps.onCreateOption = onCreateOption;
|
2021-06-25 11:05:36 +01:00
|
|
|
creatableProps.isValidNewOption = isValidNewOption;
|
2020-01-07 09:20:06 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Instead of having AsyncSelect, as a separate component we render ReactAsyncSelect
|
|
|
|
|
if (loadOptions) {
|
2021-04-02 11:11:46 +02:00
|
|
|
ReactSelectComponent = (allowCustomValue ? AsyncCreatable : ReactAsyncSelect) as any;
|
2020-01-07 09:20:06 +01:00
|
|
|
asyncSelectProps = {
|
|
|
|
|
loadOptions,
|
2020-04-25 21:48:20 +01:00
|
|
|
cacheOptions,
|
2020-01-07 09:20:06 +01:00
|
|
|
defaultOptions,
|
|
|
|
|
};
|
|
|
|
|
}
|
2020-03-03 11:51:17 +01:00
|
|
|
|
2020-01-07 09:20:06 +01:00
|
|
|
return (
|
2020-02-09 13:37:00 +01:00
|
|
|
<>
|
2020-03-03 11:51:17 +01:00
|
|
|
<ReactSelectComponent
|
2021-12-13 14:59:51 +01:00
|
|
|
ref={reactSelectRef}
|
2020-02-09 13:37:00 +01:00
|
|
|
components={{
|
|
|
|
|
MenuList: SelectMenu,
|
|
|
|
|
Group: SelectOptionGroup,
|
2020-04-29 15:02:09 +02:00
|
|
|
ValueContainer,
|
2020-12-01 23:19:52 +08:00
|
|
|
IndicatorsContainer(props: any) {
|
2020-04-21 16:06:34 +01:00
|
|
|
const { selectProps } = props;
|
|
|
|
|
const { value, showAllSelectedWhenOpen, maxVisibleValues, menuIsOpen } = selectProps;
|
|
|
|
|
|
|
|
|
|
if (maxVisibleValues !== undefined) {
|
|
|
|
|
const selectedValuesCount = value.length;
|
|
|
|
|
const indicatorChildren = [...props.children];
|
|
|
|
|
indicatorChildren.splice(
|
|
|
|
|
-1,
|
|
|
|
|
0,
|
|
|
|
|
renderExtraValuesIndicator({
|
|
|
|
|
maxVisibleValues,
|
|
|
|
|
selectedValuesCount,
|
|
|
|
|
showAllSelectedWhenOpen,
|
|
|
|
|
menuIsOpen,
|
|
|
|
|
})
|
|
|
|
|
);
|
2020-11-18 15:36:35 +01:00
|
|
|
return <IndicatorsContainer {...props}>{indicatorChildren}</IndicatorsContainer>;
|
2020-04-21 16:06:34 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return <IndicatorsContainer {...props} />;
|
|
|
|
|
},
|
2020-12-01 23:19:52 +08:00
|
|
|
IndicatorSeparator() {
|
|
|
|
|
return <></>;
|
|
|
|
|
},
|
2020-02-09 13:37:00 +01:00
|
|
|
Control: CustomControl,
|
|
|
|
|
Option: SelectMenuOptions,
|
2020-12-01 23:19:52 +08:00
|
|
|
ClearIndicator(props: any) {
|
2020-02-09 13:37:00 +01:00
|
|
|
const { clearValue } = props;
|
|
|
|
|
return (
|
|
|
|
|
<Icon
|
|
|
|
|
name="times"
|
2021-07-11 17:18:37 +02:00
|
|
|
role="button"
|
|
|
|
|
aria-label="select-clear-value"
|
|
|
|
|
className={styles.singleValueRemove}
|
2021-01-20 07:59:48 +01:00
|
|
|
onMouseDown={(e) => {
|
2020-02-09 13:37:00 +01:00
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
clearValue();
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
},
|
2020-12-01 23:19:52 +08:00
|
|
|
LoadingIndicator(props: any) {
|
2020-11-04 13:34:40 +01:00
|
|
|
return <Spinner inline={true} />;
|
2020-02-09 13:37:00 +01:00
|
|
|
},
|
2020-12-01 23:19:52 +08:00
|
|
|
LoadingMessage(props: any) {
|
2020-02-09 13:37:00 +01:00
|
|
|
return <div className={styles.loadingMessage}>{loadingMessage}</div>;
|
|
|
|
|
},
|
2020-12-01 23:19:52 +08:00
|
|
|
NoOptionsMessage(props: any) {
|
2020-02-09 13:37:00 +01:00
|
|
|
return (
|
|
|
|
|
<div className={styles.loadingMessage} aria-label="No options provided">
|
|
|
|
|
{noOptionsMessage}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
},
|
2020-12-01 23:19:52 +08:00
|
|
|
DropdownIndicator(props: any) {
|
|
|
|
|
return <DropdownIndicator isOpen={props.selectProps.menuIsOpen} />;
|
|
|
|
|
},
|
2021-05-15 11:06:42 +03:00
|
|
|
SingleValue(props: any) {
|
|
|
|
|
return <SingleValue {...props} disabled={disabled} />;
|
|
|
|
|
},
|
2020-02-09 13:37:00 +01:00
|
|
|
MultiValueContainer: MultiValueContainer,
|
|
|
|
|
MultiValueRemove: MultiValueRemove,
|
2021-10-19 12:29:33 +01:00
|
|
|
SelectContainer,
|
2020-02-09 13:37:00 +01:00
|
|
|
...components,
|
|
|
|
|
}}
|
2021-12-15 13:33:35 +01:00
|
|
|
styles={selectStyles}
|
2020-04-22 13:38:50 +02:00
|
|
|
className={className}
|
2020-02-09 13:37:00 +01:00
|
|
|
{...commonSelectProps}
|
|
|
|
|
{...creatableProps}
|
|
|
|
|
{...asyncSelectProps}
|
|
|
|
|
/>
|
|
|
|
|
</>
|
2020-01-07 09:20:06 +01:00
|
|
|
);
|
|
|
|
|
}
|