diff --git a/packages/grafana-ui/src/components/Select/Select.story.tsx b/packages/grafana-ui/src/components/Select/Select.story.tsx index 4578e210f6b..cafb86f2227 100644 --- a/packages/grafana-ui/src/components/Select/Select.story.tsx +++ b/packages/grafana-ui/src/components/Select/Select.story.tsx @@ -24,6 +24,8 @@ export default { }, }; +const BEHAVIOUR_GROUP = 'Behaviour props'; + const loadAsyncOptions = () => { return new Promise>>(resolve => { setTimeout(() => { @@ -33,7 +35,6 @@ const loadAsyncOptions = () => { }; const getKnobs = () => { - const BEHAVIOUR_GROUP = 'Behaviour props'; const disabled = boolean('Disabled', false, BEHAVIOUR_GROUP); const invalid = boolean('Invalid', false, BEHAVIOUR_GROUP); const loading = boolean('Loading', false, BEHAVIOUR_GROUP); @@ -66,6 +67,18 @@ const getKnobs = () => { }; }; +const getMultiSelectKnobs = () => { + const isClearable = boolean('Clearable', false, BEHAVIOUR_GROUP); + const closeMenuOnSelect = boolean('Close on Select', false, BEHAVIOUR_GROUP); + const maxVisibleValues = number('Max. visible values', 5, undefined, BEHAVIOUR_GROUP); + + return { + isClearable, + closeMenuOnSelect, + maxVisibleValues, + }; +}; + const getDynamicProps = () => { const knobs = getKnobs(); return { @@ -177,6 +190,7 @@ export const multiSelect = () => { setValue(v); }} {...getDynamicProps()} + {...getMultiSelectKnobs()} /> ); diff --git a/packages/grafana-ui/src/components/Select/SelectBase.test.tsx b/packages/grafana-ui/src/components/Select/SelectBase.test.tsx index 99a8c550950..8c1c5cde710 100644 --- a/packages/grafana-ui/src/components/Select/SelectBase.test.tsx +++ b/packages/grafana-ui/src/components/Select/SelectBase.test.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { mount, ReactWrapper } from 'enzyme'; import { SelectBase } from './SelectBase'; import { SelectableValue } from '@grafana/data'; +import { MultiValueContainer } from './MultiValue'; const onChangeHandler = () => jest.fn(); const findMenuElement = (container: ReactWrapper) => container.find({ 'aria-label': 'Select options menu' }); @@ -54,6 +55,107 @@ describe('SelectBase', () => { }); }); + describe('when maxVisibleValues prop', () => { + let excessiveOptions: Array> = []; + beforeAll(() => { + excessiveOptions = [ + { + label: 'Option 1', + value: 1, + }, + { + label: 'Option 2', + value: 2, + }, + { + label: 'Option 3', + value: 3, + }, + { + label: 'Option 4', + value: 4, + }, + { + label: 'Option 5', + value: 5, + }, + ]; + }); + + describe('is provided', () => { + it('should only display maxVisibleValues options, and additional number of values should be displayed as indicator', () => { + const container = mount( + + ); + + expect(container.find(MultiValueContainer)).toHaveLength(3); + expect(container.find('#excess-values').text()).toBe('(+2)'); + }); + + describe('and showAllSelectedWhenOpen prop is true', () => { + it('should show all selected options when menu is open', () => { + const container = mount( + + ); + + expect(container.find(MultiValueContainer)).toHaveLength(5); + expect(container.find('#excess-values')).toHaveLength(0); + }); + }); + + describe('and showAllSelectedWhenOpen prop is false', () => { + it('should not show all selected options when menu is open', () => { + const container = mount( + + ); + + expect(container.find('#excess-values').text()).toBe('(+2)'); + expect(container.find(MultiValueContainer)).toHaveLength(3); + }); + }); + }); + + describe('is not provided', () => { + it('should always show all selected options', () => { + const container = mount( + + ); + + expect(container.find(MultiValueContainer)).toHaveLength(5); + expect(container.find('#excess-values')).toHaveLength(0); + }); + }); + }); + describe('options', () => { it('renders menu with provided options', () => { const container = mount(); diff --git a/packages/grafana-ui/src/components/Select/SelectBase.tsx b/packages/grafana-ui/src/components/Select/SelectBase.tsx index 82f9e219a9d..8cd1fdcfc83 100644 --- a/packages/grafana-ui/src/components/Select/SelectBase.tsx +++ b/packages/grafana-ui/src/components/Select/SelectBase.tsx @@ -24,6 +24,31 @@ import { getSelectStyles } from './getSelectStyles'; import { cleanValue } from './utils'; import { SelectBaseProps, SelectValue } from './types'; +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 ( + + (+{selectedValuesCount - maxVisibleValues}) + + ); + } + + return null; +}; + const CustomControl = (props: any) => { const { children, @@ -66,6 +91,7 @@ export function SelectBase({ allowCustomValue = false, autoFocus = false, backspaceRemovesValue = true, + closeMenuOnSelect = true, components, defaultOptions, defaultValue, @@ -83,6 +109,7 @@ export function SelectBase({ loadOptions, loadingMessage = 'Loading options...', maxMenuHeight = 300, + maxVisibleValues, menuPosition, menuPlacement = 'auto', noOptionsMessage = 'No options found', @@ -98,6 +125,7 @@ export function SelectBase({ placeholder = 'Choose', prefix, renderControl, + showAllSelectedWhenOpen = true, tabSelectsValue = true, className, value, @@ -142,6 +170,7 @@ export function SelectBase({ autoFocus, backspaceRemovesValue, captureMenuScroll: false, + closeMenuOnSelect, 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, @@ -156,6 +185,7 @@ export function SelectBase({ isMulti, isSearchable, maxMenuHeight, + maxVisibleValues, menuIsOpen: isOpen, menuPlacement, menuPosition, @@ -171,6 +201,7 @@ export function SelectBase({ placeholder, prefix, renderControl, + showAllSelectedWhenOpen, tabSelectsValue, value: isMulti ? selectedValue : selectedValue[0], }; @@ -196,7 +227,22 @@ export function SelectBase({ components={{ MenuList: SelectMenu, Group: SelectOptionGroup, - ValueContainer: ValueContainer, + ValueContainer: (props: any) => { + const { menuIsOpen } = props.selectProps; + if ( + Array.isArray(props.children) && + Array.isArray(props.children[0]) && + maxVisibleValues !== undefined && + !(showAllSelectedWhenOpen && menuIsOpen) + ) { + const [valueChildren, ...otherChildren] = props.children; + const truncatedValues = valueChildren.slice(0, maxVisibleValues); + + return ; + } + + return ; + }, Placeholder: (props: any) => (
({ {props.children}
), - IndicatorsContainer: IndicatorsContainer, + 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 ; + } + + return ; + }, IndicatorSeparator: () => <>, Control: CustomControl, Option: SelectMenuOptions, diff --git a/packages/grafana-ui/src/components/Select/types.ts b/packages/grafana-ui/src/components/Select/types.ts index cfe8fc07a17..038a2a169ba 100644 --- a/packages/grafana-ui/src/components/Select/types.ts +++ b/packages/grafana-ui/src/components/Select/types.ts @@ -9,6 +9,7 @@ export interface SelectCommonProps { autoFocus?: boolean; backspaceRemovesValue?: boolean; className?: string; + closeMenuOnSelect?: boolean; /** Used for custom components. For more information, see `react-select` */ components?: any; defaultValue?: any; @@ -24,7 +25,9 @@ export interface SelectCommonProps { isOpen?: boolean; /** Disables the possibility to type into the input*/ isSearchable?: boolean; + showAllSelectedWhenOpen?: boolean; maxMenuHeight?: number; + maxVisibleValues?: number; menuPlacement?: 'auto' | 'bottom' | 'top'; menuPosition?: 'fixed' | 'absolute'; /** The message to display when no options could be found */