@grafana/ui
Version:
Grafana Components Library
389 lines (386 loc) • 13.5 kB
JavaScript
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
import { isArray, negate } from 'lodash';
import * as React from 'react';
import { useRef, useState, useImperativeHandle, useEffect, useCallback } from 'react';
import ReactSelect from 'react-select';
import ReactAsyncSelect from 'react-select/async';
import AsyncCreatable from 'react-select/async-creatable';
import Creatable from 'react-select/creatable';
import { toOption } from '@grafana/data';
import { t, Trans } from '@grafana/i18n';
import { useTheme2 } from '../../themes/ThemeContext.mjs';
import { Icon } from '../Icon/Icon.mjs';
import { CustomInput } from './CustomInput.mjs';
import { DropdownIndicator } from './DropdownIndicator.mjs';
import { IndicatorsContainer } from './IndicatorsContainer.mjs';
import { InputControl } from './InputControl.mjs';
import { MultiValueRemove, MultiValueContainer } from './MultiValue.mjs';
import { SelectContainer } from './SelectContainer.mjs';
import { VirtualizedSelectMenu, SelectMenu, SelectMenuOptions } from './SelectMenu.mjs';
import { SelectOptionGroup } from './SelectOptionGroup.mjs';
import { SelectOptionGroupHeader } from './SelectOptionGroupHeader.mjs';
import { SingleValue } from './SingleValue.mjs';
import { ValueContainer } from './ValueContainer.mjs';
import { getSelectStyles } from './getSelectStyles.mjs';
import { useCustomSelectStyles } from './resetSelectStyles.mjs';
import { ToggleAllState } from './types.mjs';
import { findSelectedValue, cleanValue, omitDescriptions } from './utils.mjs';
"use strict";
const CustomControl = (props) => {
const {
children,
innerProps,
selectProps: { menuIsOpen, onMenuClose, onMenuOpen },
isFocused,
isMulti,
getValue,
innerRef
} = props;
const selectProps = props.selectProps;
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 /* @__PURE__ */ jsx(
InputControl,
{
ref: innerRef,
innerProps,
prefix: selectProps.prefix,
focused: isFocused,
invalid: !!selectProps.invalid,
disabled: !!selectProps.disabled,
children
}
);
};
function determineToggleAllState(selectedValue, options) {
if (options.length === selectedValue.length) {
return ToggleAllState.allSelected;
} else if (selectedValue.length === 0) {
return ToggleAllState.noneSelected;
} else {
return ToggleAllState.indeterminate;
}
}
function SelectBase({
allowCustomValue = false,
allowCreateWhileLoading = false,
"aria-label": ariaLabel,
"data-testid": dataTestid,
autoFocus = false,
backspaceRemovesValue = true,
blurInputOnSelect,
cacheOptions,
className,
closeMenuOnSelect = true,
components,
createOptionPosition = "last",
defaultOptions,
defaultValue,
disabled = false,
filterOption,
formatCreateLabel,
getOptionLabel,
getOptionValue,
inputValue,
invalid,
isClearable = false,
id,
isLoading = false,
isMulti = false,
inputId,
isOpen,
isOptionDisabled,
isSearchable = true,
loadOptions,
loadingMessage = "Loading options...",
maxMenuHeight = 300,
minMenuHeight,
maxVisibleValues,
menuPlacement = "auto",
menuPosition,
menuShouldPortal = true,
noOptionsMessage = t("grafana-ui.select.no-options-label", "No options found"),
onBlur,
onChange,
onCloseMenu,
onCreateOption,
onInputChange,
onKeyDown,
onMenuScrollToBottom,
onMenuScrollToTop,
onOpenMenu,
onFocus,
toggleAllOptions,
openMenuOnFocus = false,
options = [],
placeholder = t("grafana-ui.select.placeholder", "Choose"),
prefix,
renderControl,
showAllSelectedWhenOpen = true,
tabSelectsValue = true,
value,
virtualized = false,
noMultiValueWrap,
width,
isValidNewOption,
formatOptionLabel,
hideSelectedOptions,
selectRef,
...rest
}) {
const theme = useTheme2();
const styles = getSelectStyles(theme);
const reactSelectRef = useRef(null);
const [closeToBottom, setCloseToBottom] = useState(false);
const selectStyles = useCustomSelectStyles(theme, width);
const [hasInputValue, setHasInputValue] = useState(!!inputValue);
useImperativeHandle(selectRef, () => reactSelectRef.current, []);
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]);
const onChangeWithEmpty = useCallback(
(value2, action) => {
if (isMulti && (value2 === void 0 || value2 === null)) {
return onChange([], action);
}
onChange(value2, action);
},
[isMulti, onChange]
);
let ReactSelectComponent = ReactSelect;
const creatableProps = {};
let asyncSelectProps = {};
let selectedValue;
if (isMulti && loadOptions) {
selectedValue = value;
} else {
if (isMulti && value && Array.isArray(value) && !loadOptions) {
selectedValue = value.map((v) => {
var _a;
const selectableValue = findSelectedValue((_a = v.value) != null ? _a : v, options);
if (selectableValue) {
return selectableValue;
}
return typeof v === "string" ? toOption(v) : v;
});
} else if (loadOptions) {
const hasValue = defaultValue || value;
selectedValue = hasValue ? [hasValue] : [];
} else {
selectedValue = cleanValue(value, options);
}
}
const commonSelectProps = {
"aria-label": ariaLabel,
"data-testid": dataTestid,
autoFocus,
backspaceRemovesValue,
blurInputOnSelect,
captureMenuScroll: onMenuScrollToBottom || onMenuScrollToTop,
closeMenuOnSelect,
// 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
defaultValue,
// Also passing disabled, as this is the new Select API, and I want to use this prop instead of react-select's one
disabled,
// react-select always tries to filter the options even at first menu open, which is a problem for performance
// in large lists. So we set it to not try to filter the options if there is no input value.
filterOption: hasInputValue ? filterOption : null,
getOptionLabel,
getOptionValue,
hideSelectedOptions,
inputValue,
invalid,
isClearable,
id,
// Passing isDisabled as react-select accepts this prop
isDisabled: disabled,
isLoading,
isMulti,
inputId,
isOptionDisabled,
isSearchable,
maxMenuHeight,
minMenuHeight,
maxVisibleValues,
menuIsOpen: isOpen,
menuPlacement: menuPlacement === "auto" && closeToBottom ? "top" : menuPlacement,
menuPosition,
menuShouldBlockScroll: true,
menuPortalTarget: menuShouldPortal && typeof document !== "undefined" ? document.body : void 0,
menuShouldScrollIntoView: false,
onBlur,
onChange: onChangeWithEmpty,
onInputChange: (val, actionMeta) => {
var _a;
const newValue = (_a = onInputChange == null ? void 0 : onInputChange(val, actionMeta)) != null ? _a : val;
const newHasValue = !!newValue;
if (newHasValue !== hasInputValue) {
setHasInputValue(newHasValue);
}
return newValue;
},
onKeyDown,
onMenuClose: onCloseMenu,
onMenuOpen: onOpenMenu,
onMenuScrollToBottom,
onMenuScrollToTop,
onFocus,
formatOptionLabel,
openMenuOnFocus,
options: virtualized ? omitDescriptions(options) : options,
placeholder,
prefix,
renderControl,
showAllSelectedWhenOpen,
tabSelectsValue,
value: isMulti ? selectedValue : selectedValue == null ? void 0 : selectedValue[0],
noMultiValueWrap
};
if (allowCustomValue) {
ReactSelectComponent = Creatable;
creatableProps.allowCreateWhileLoading = allowCreateWhileLoading;
creatableProps.formatCreateLabel = formatCreateLabel != null ? formatCreateLabel : defaultFormatCreateLabel;
creatableProps.onCreateOption = onCreateOption;
creatableProps.createOptionPosition = createOptionPosition;
creatableProps.isValidNewOption = isValidNewOption;
}
if (loadOptions) {
ReactSelectComponent = allowCustomValue ? AsyncCreatable : ReactAsyncSelect;
asyncSelectProps = {
loadOptions,
cacheOptions,
defaultOptions
};
}
const SelectMenuComponent = virtualized ? VirtualizedSelectMenu : SelectMenu;
let toggleAllState = ToggleAllState.noneSelected;
if ((toggleAllOptions == null ? void 0 : toggleAllOptions.enabled) && isArray(selectedValue)) {
if (toggleAllOptions == null ? void 0 : toggleAllOptions.determineToggleAllState) {
toggleAllState = toggleAllOptions.determineToggleAllState(selectedValue, options);
} else {
toggleAllState = determineToggleAllState(selectedValue, options);
}
}
const toggleAll = useCallback(() => {
let toSelect = toggleAllState === ToggleAllState.noneSelected ? options : [];
if (toggleAllOptions == null ? void 0 : toggleAllOptions.optionsFilter) {
toSelect = toggleAllState === ToggleAllState.noneSelected ? options.filter(toggleAllOptions.optionsFilter) : options.filter(negate(toggleAllOptions.optionsFilter));
}
onChange(toSelect, {
action: "select-option",
option: {}
});
}, [options, toggleAllOptions, onChange, toggleAllState]);
return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(
ReactSelectComponent,
{
ref: reactSelectRef,
components: {
MenuList: SelectMenuComponent,
Group: SelectOptionGroup,
GroupHeading: SelectOptionGroupHeader,
ValueContainer,
IndicatorsContainer: CustomIndicatorsContainer,
IndicatorSeparator,
Control: CustomControl,
Option: SelectMenuOptions,
ClearIndicator(props) {
const { clearValue } = props;
return /* @__PURE__ */ jsx(
Icon,
{
name: "times",
role: "button",
"aria-label": t("grafana-ui.select.clear-value", "Clear value"),
className: styles.singleValueRemove,
onMouseDown: (e) => {
e.preventDefault();
e.stopPropagation();
clearValue();
}
}
);
},
LoadingIndicator() {
return null;
},
LoadingMessage() {
return /* @__PURE__ */ jsx("div", { className: styles.loadingMessage, children: loadingMessage });
},
NoOptionsMessage() {
return /* @__PURE__ */ jsx(
"div",
{
className: styles.loadingMessage,
"aria-label": t("grafana-ui.select.empty-options", "No options provided"),
children: noOptionsMessage
}
);
},
DropdownIndicator,
SingleValue(props) {
return /* @__PURE__ */ jsx(SingleValue, { ...props, isDisabled: disabled });
},
SelectContainer,
MultiValueContainer,
MultiValueRemove: !disabled ? MultiValueRemove : () => null,
Input: CustomInput,
...components
},
toggleAllOptions: (toggleAllOptions == null ? void 0 : toggleAllOptions.enabled) && {
state: toggleAllState,
selectAllClicked: toggleAll,
selectedCount: isArray(selectedValue) ? selectedValue.length : void 0
},
styles: selectStyles,
className,
autoWidth: width === "auto",
...commonSelectProps,
...creatableProps,
...asyncSelectProps,
...rest
}
) });
}
function defaultFormatCreateLabel(input) {
return /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "8px", alignItems: "center" }, children: [
/* @__PURE__ */ jsx("div", { children: input }),
/* @__PURE__ */ jsx("div", { style: { flexGrow: 1 } }),
/* @__PURE__ */ jsx("div", { className: "muted small", style: { display: "flex", gap: "8px", alignItems: "center" }, children: /* @__PURE__ */ jsx(Trans, { i18nKey: "grafana-ui.select.default-create-label", children: "Hit enter to add" }) })
] });
}
function CustomIndicatorsContainer(props) {
const { showAllSelectedWhenOpen, maxVisibleValues, menuIsOpen } = props.selectProps;
const value = props.getValue();
if (maxVisibleValues !== void 0 && Array.isArray(props.children)) {
const selectedValuesCount = value.length;
if (selectedValuesCount > maxVisibleValues && !(showAllSelectedWhenOpen && menuIsOpen)) {
const indicatorChildren = [...props.children];
indicatorChildren.splice(
-1,
0,
/* @__PURE__ */ jsx("span", { id: "excess-values", children: `(+${selectedValuesCount - maxVisibleValues})` }, "excess-values")
);
return /* @__PURE__ */ jsx(IndicatorsContainer, { ...props, children: indicatorChildren });
}
}
return /* @__PURE__ */ jsx(IndicatorsContainer, { ...props });
}
function IndicatorSeparator() {
return /* @__PURE__ */ jsx(Fragment, {});
}
export { SelectBase };
//# sourceMappingURL=SelectBase.mjs.map