UNPKG

@grafana/ui

Version:
389 lines (386 loc) • 13.5 kB
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