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

355 lines
10 KiB
TypeScript
Raw Normal View History

import React, { useCallback } from 'react';
2020-01-07 09:20:06 +01:00
// @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 { Spinner } from '../Spinner/Spinner';
import { css, cx } from 'emotion';
2020-01-07 09:20:06 +01:00
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';
2020-01-07 09:20:06 +01:00
import { getSelectStyles } from './getSelectStyles';
import { cleanValue } from './utils';
import { SelectBaseProps, SelectValue } from './types';
2020-01-07 09:20:06 +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>({
allowCustomValue = false,
autoFocus = false,
backspaceRemovesValue = true,
cacheOptions,
className,
closeMenuOnSelect = true,
components,
defaultOptions,
2020-01-07 09:20:06 +01:00
defaultValue,
disabled = false,
filterOption,
formatCreateLabel,
getOptionLabel,
getOptionValue,
inputValue,
invalid,
2020-01-07 09:20:06 +01:00
isClearable = false,
isLoading = false,
isMulti = false,
2020-01-07 09:20:06 +01:00
isOpen,
isOptionDisabled,
isSearchable = true,
loadOptions,
loadingMessage = 'Loading options...',
2020-01-07 09:20:06 +01:00
maxMenuHeight = 300,
FieldColor: Adds new standard color option for color (#28039) * FieldColor: Added field color option * Progress * FieldColor: Added custom schemes * move to fieldColor * move to fieldColor * add back the standard color picker * FieldColor: Added registry for field color modes * wip refactor * Seperate scale from color mode * Progress * schemes working * Stuff is working * Added fallback * Updated threshold tests * Added unit tests * added more tests * Made it work with new graph panel * Use scale calculator from graph panel * Updates * Updated test * Renaming things * Update packages/grafana-data/src/field/displayProcessor.ts Co-authored-by: Ryan McKinley <ryantxu@gmail.com> * updated according to feedback, added docs * Updated docs * Updated * Update docs/sources/panels/field-options/standard-field-options.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/panels/field-options/standard-field-options.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Updated docs according to feedback * fixed test * Updated * Updated wording * Change to fieldState.seriesIndex * Updated tests * Updates * New names * More work needed to support bar gauge and showing the color modes in the picker * Now correct gradients work in bar gauge * before rename * Unifying the concept * Updates * review feedback * Updated * Skip minification * Updated * UI improvements Co-authored-by: Ryan McKinley <ryantxu@gmail.com> Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>
2020-10-09 09:34:57 +02:00
minMenuHeight,
maxVisibleValues,
menuPlacement = 'auto',
menuPosition,
2020-01-07 09:20:06 +01:00
noOptionsMessage = 'No options found',
onBlur,
onChange,
onCloseMenu,
onCreateOption,
onInputChange,
onKeyDown,
onOpenMenu,
openMenuOnFocus = false,
options = [],
placeholder = 'Choose',
2020-01-07 09:20:06 +01:00
prefix,
renderControl,
showAllSelectedWhenOpen = true,
tabSelectsValue = true,
value,
2020-01-07 09:20:06 +01:00
width,
}: SelectBaseProps<T>) {
const theme = useTheme();
const styles = getSelectStyles(theme);
const onChangeWithEmpty = useCallback(
(value: SelectValue<T>) => {
if (isMulti && (value === undefined || value === null)) {
return onChange([]);
}
onChange(value);
},
[isMulti, onChange]
);
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 = cleanValue(value, options);
2020-01-07 09:20:06 +01:00
}
}
const commonSelectProps = {
autoFocus,
backspaceRemovesValue,
captureMenuScroll: false,
closeMenuOnSelect,
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,
filterOption,
getOptionLabel,
getOptionValue,
inputValue,
2020-01-07 09:20:06 +01:00
invalid,
isClearable,
// Passing isDisabled as react-select accepts this prop
isDisabled: disabled,
2020-01-07 09:20:06 +01:00
isLoading,
isMulti,
isOptionDisabled,
isSearchable,
maxMenuHeight,
FieldColor: Adds new standard color option for color (#28039) * FieldColor: Added field color option * Progress * FieldColor: Added custom schemes * move to fieldColor * move to fieldColor * add back the standard color picker * FieldColor: Added registry for field color modes * wip refactor * Seperate scale from color mode * Progress * schemes working * Stuff is working * Added fallback * Updated threshold tests * Added unit tests * added more tests * Made it work with new graph panel * Use scale calculator from graph panel * Updates * Updated test * Renaming things * Update packages/grafana-data/src/field/displayProcessor.ts Co-authored-by: Ryan McKinley <ryantxu@gmail.com> * updated according to feedback, added docs * Updated docs * Updated * Update docs/sources/panels/field-options/standard-field-options.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/panels/field-options/standard-field-options.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Updated docs according to feedback * fixed test * Updated * Updated wording * Change to fieldState.seriesIndex * Updated tests * Updates * New names * More work needed to support bar gauge and showing the color modes in the picker * Now correct gradients work in bar gauge * before rename * Unifying the concept * Updates * review feedback * Updated * Skip minification * Updated * UI improvements Co-authored-by: Ryan McKinley <ryantxu@gmail.com> Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>
2020-10-09 09:34:57 +02:00
minMenuHeight,
maxVisibleValues,
2020-01-07 09:20:06 +01:00
menuIsOpen: isOpen,
menuPlacement,
menuPosition,
menuShouldScrollIntoView: false,
onBlur,
onChange: onChangeWithEmpty,
onInputChange,
onKeyDown,
2020-01-07 09:20:06 +01:00
onMenuClose: onCloseMenu,
onMenuOpen: onOpenMenu,
openMenuOnFocus,
2020-01-07 09:20:06 +01:00
options,
placeholder,
prefix,
2020-01-07 09:20:06 +01:00
renderControl,
showAllSelectedWhenOpen,
tabSelectsValue,
value: isMulti ? selectedValue : selectedValue[0],
2020-01-07 09:20:06 +01:00
};
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,
cacheOptions,
2020-01-07 09:20:06 +01:00
defaultOptions,
};
}
2020-01-07 09:20:06 +01:00
return (
2020-02-09 13:37:00 +01:00
<>
<ReactSelectComponent
2020-02-09 13:37:00 +01:00
components={{
MenuList: SelectMenu,
Group: SelectOptionGroup,
ValueContainer,
Placeholder(props: any) {
return (
<div
{...props.innerProps}
className={cx(
css(props.getStyles('placeholder', props)),
css`
display: inline-block;
color: ${theme.colors.formInputPlaceholderText};
position: absolute;
top: 50%;
transform: translateY(-50%);
box-sizing: border-box;
line-height: 1;
`
)}
>
{props.children}
</div>
);
},
IndicatorsContainer(props: any) {
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,
})
);
return <IndicatorsContainer {...props}>{indicatorChildren}</IndicatorsContainer>;
}
return <IndicatorsContainer {...props} />;
},
IndicatorSeparator() {
return <></>;
},
2020-02-09 13:37:00 +01:00
Control: CustomControl,
Option: SelectMenuOptions,
ClearIndicator(props: any) {
2020-02-09 13:37:00 +01:00
const { clearValue } = props;
return (
<Icon
name="times"
onMouseDown={e => {
e.preventDefault();
e.stopPropagation();
clearValue();
}}
/>
);
},
LoadingIndicator(props: any) {
return <Spinner inline={true} />;
2020-02-09 13:37:00 +01:00
},
LoadingMessage(props: any) {
2020-02-09 13:37:00 +01:00
return <div className={styles.loadingMessage}>{loadingMessage}</div>;
},
NoOptionsMessage(props: any) {
2020-02-09 13:37:00 +01:00
return (
<div className={styles.loadingMessage} aria-label="No options provided">
{noOptionsMessage}
</div>
);
},
DropdownIndicator(props: any) {
return <DropdownIndicator isOpen={props.selectProps.menuIsOpen} />;
},
2020-02-09 13:37:00 +01:00
SingleValue: SingleValue,
MultiValueContainer: MultiValueContainer,
MultiValueRemove: MultiValueRemove,
...components,
}}
styles={{
...resetSelectStyles(),
menuPortal: ({ position, width }: any) => ({
position,
width,
2020-04-22 16:18:22 +02:00
zIndex: theme.zIndex.dropdown,
}),
2020-02-09 13:37:00 +01:00
//These are required for the menu positioning to function
menu: ({ top, bottom, position }: any) => ({
2020-02-09 13:37:00 +01:00
top,
bottom,
position,
marginBottom: !!bottom ? '10px' : '0',
minWidth: '100%',
2020-04-22 16:18:22 +02:00
zIndex: theme.zIndex.dropdown,
2020-02-09 13:37:00 +01:00
}),
container: () => ({
position: 'relative',
width: width ? `${8 * width}px` : '100%',
}),
option: (provided: any, state: any) => ({
...provided,
opacity: state.isDisabled ? 0.5 : 1,
}),
2020-02-09 13:37:00 +01:00
}}
className={className}
2020-02-09 13:37:00 +01:00
{...commonSelectProps}
{...creatableProps}
{...asyncSelectProps}
/>
</>
2020-01-07 09:20:06 +01:00
);
}