Files
grafana/packages/grafana-ui/src/components/Forms/Select/SelectBase.tsx

360 lines
11 KiB
TypeScript
Raw Normal View History

2020-02-12 09:07:07 +01:00
import React from 'react';
2020-01-07 09:20:06 +01:00
import { SelectableValue, deprecationWarning } from '@grafana/data';
// @ts-ignore
import { default as ReactSelect } from '@torkelo/react-select';
2020-01-07 09:20:06 +01:00
// @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';
2020-01-07 09:20:06 +01:00
import { Icon } from '../../Icon/Icon';
import { css, cx } from 'emotion';
2020-01-07 09:20:06 +01:00
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> = T | SelectableValue<T> | T[] | Array<SelectableValue<T>>;
export interface SelectCommonProps<T> {
className?: string;
options?: Array<SelectableValue<T>>;
defaultValue?: any;
inputValue?: string;
2020-01-07 09:20:06 +01:00
value?: SelectValue<T>;
getOptionLabel?: (item: SelectableValue<T>) => string;
getOptionValue?: (item: SelectableValue<T>) => string;
onCreateOption?: (value: string) => void;
2020-01-07 09:20:06 +01:00
onChange: (value: SelectableValue<T>) => {} | void;
onInputChange?: (label: string) => void;
onKeyDown?: (event: React.KeyboardEvent) => void;
2020-01-07 09:20:06 +01:00
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*/
2020-01-07 09:20:06 +01:00
renderControl?: ControlComponent<T>;
}
export interface SelectAsyncProps<T> {
/** When specified as boolean the loadOptions will execute when component is mounted */
defaultOptions?: boolean | Array<SelectableValue<T>>;
/** Asynchroniously load select options */
loadOptions?: (query: string) => Promise<Array<SelectableValue<T>>>;
/** Message to display when options are loading */
loadingMessage?: string;
}
export interface MultiSelectCommonProps<T> extends Omit<SelectCommonProps<T>, 'onChange' | 'isMulti' | 'value'> {
value?: Array<SelectableValue<T>> | T[];
onChange: (item: Array<SelectableValue<T>>) => {} | void;
}
export interface SelectBaseProps<T> extends SelectCommonProps<T>, SelectAsyncProps<T> {
invalid?: boolean;
}
export interface CustomControlProps<T> {
ref: React.Ref<any>;
isOpen: boolean;
/** Currently selected value */
value?: SelectableValue<T>;
/** 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<T> = React.ComponentType<CustomControlProps<T>>;
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>({
value,
defaultValue,
inputValue,
onInputChange,
onCreateOption,
2020-01-07 09:20:06 +01:00
options = [],
onChange,
onBlur,
onKeyDown,
2020-01-07 09:20:06 +01:00
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,
components,
}: SelectBaseProps<T>) {
const theme = useTheme();
const styles = getSelectStyles(theme);
let ReactSelectComponent: ReactSelect | Creatable = ReactSelect;
2020-01-07 09:20:06 +01:00
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 if (loadOptions) {
const hasValue = defaultValue || value;
selectedValue = hasValue ? [hasValue] : [];
2020-01-07 09:20:06 +01:00
} 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,
2020-01-07 09:20:06 +01:00
value: isMulti ? selectedValue : selectedValue[0],
getOptionLabel,
getOptionValue,
openMenuOnFocus,
maxMenuHeight,
isMulti,
backspaceRemovesValue,
onMenuOpen: onOpenMenu,
onMenuClose: onCloseMenu,
tabSelectsValue,
options,
onChange,
onBlur,
onKeyDown,
2020-01-07 09:20:06 +01:00
menuShouldScrollIntoView: false,
renderControl,
captureMenuScroll: false,
blurInputOnSelect: true,
menuPlacement: 'auto',
2020-01-07 09:20:06 +01:00
};
// 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;
2020-01-07 09:20:06 +01:00
creatableProps.formatCreateLabel = formatCreateLabel ?? ((input: string) => `Create: ${input}`);
creatableProps.onCreateOption = onCreateOption;
2020-01-07 09:20:06 +01:00
}
// Instead of having AsyncSelect, as a separate component we render ReactAsyncSelect
if (loadOptions) {
ReactSelectComponent = allowCustomValue ? AsyncCreatable : ReactAsyncSelect;
2020-01-07 09:20:06 +01:00
asyncSelectProps = {
loadOptions,
defaultOptions,
};
}
return (
2020-02-09 13:37:00 +01:00
<>
<ReactSelectComponent
components={{
MenuList: SelectMenu,
Group: SelectOptionGroup,
ValueContainer: ValueContainer,
Placeholder: (props: any) => (
<div
{...props.innerProps}
className={cx(
css(props.getStyles('placeholder', props)),
css`
display: inline-block;
color: hsl(0, 0%, 50%);
position: absolute;
top: 50%;
transform: translateY(-50%);
box-sizing: border-box;
line-height: 1;
`
)}
>
{props.children}
2020-01-07 09:20:06 +01:00
</div>
2020-02-09 13:37:00 +01:00
),
SelectContainer: (props: any) => (
<div
{...props.innerProps}
className={cx(
css(props.getStyles('container', props)),
css`
position: relative;
`,
inputSizes()[size]
)}
>
{props.children}
</div>
),
IndicatorsContainer: IndicatorsContainer,
IndicatorSeparator: () => <></>,
Control: CustomControl,
Option: SelectMenuOptions,
ClearIndicator: (props: any) => {
const { clearValue } = props;
return (
<Icon
name="times"
onMouseDown={e => {
e.preventDefault();
e.stopPropagation();
clearValue();
}}
/>
);
},
LoadingIndicator: (props: any) => {
return <Icon name="spinner" className="fa fa-spin" />;
},
LoadingMessage: (props: any) => {
return <div className={styles.loadingMessage}>{loadingMessage}</div>;
},
NoOptionsMessage: (props: any) => {
return (
<div className={styles.loadingMessage} aria-label="No options provided">
{noOptionsMessage}
</div>
);
},
DropdownIndicator: (props: any) => <DropdownIndicator isOpen={props.selectProps.menuIsOpen} />,
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',
2020-02-12 09:07:07 +01:00
zIndex: 9999,
2020-02-09 13:37:00 +01:00
}),
}}
className={widthClass}
{...commonSelectProps}
{...creatableProps}
{...asyncSelectProps}
/>
</>
2020-01-07 09:20:06 +01:00
);
}