Commit 3d23ab54 by kay delaney Committed by GitHub

UI: Adds option to limit number of visible selected options for Select component (#23722)

* UI: Adds option to limit number of visible selected options to Select component
parent 871ad734
...@@ -24,6 +24,8 @@ export default { ...@@ -24,6 +24,8 @@ export default {
}, },
}; };
const BEHAVIOUR_GROUP = 'Behaviour props';
const loadAsyncOptions = () => { const loadAsyncOptions = () => {
return new Promise<Array<SelectableValue<string>>>(resolve => { return new Promise<Array<SelectableValue<string>>>(resolve => {
setTimeout(() => { setTimeout(() => {
...@@ -33,7 +35,6 @@ const loadAsyncOptions = () => { ...@@ -33,7 +35,6 @@ const loadAsyncOptions = () => {
}; };
const getKnobs = () => { const getKnobs = () => {
const BEHAVIOUR_GROUP = 'Behaviour props';
const disabled = boolean('Disabled', false, BEHAVIOUR_GROUP); const disabled = boolean('Disabled', false, BEHAVIOUR_GROUP);
const invalid = boolean('Invalid', false, BEHAVIOUR_GROUP); const invalid = boolean('Invalid', false, BEHAVIOUR_GROUP);
const loading = boolean('Loading', false, BEHAVIOUR_GROUP); const loading = boolean('Loading', false, BEHAVIOUR_GROUP);
...@@ -66,6 +67,18 @@ const getKnobs = () => { ...@@ -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 getDynamicProps = () => {
const knobs = getKnobs(); const knobs = getKnobs();
return { return {
...@@ -177,6 +190,7 @@ export const multiSelect = () => { ...@@ -177,6 +190,7 @@ export const multiSelect = () => {
setValue(v); setValue(v);
}} }}
{...getDynamicProps()} {...getDynamicProps()}
{...getMultiSelectKnobs()}
/> />
</> </>
); );
......
...@@ -2,6 +2,7 @@ import React from 'react'; ...@@ -2,6 +2,7 @@ import React from 'react';
import { mount, ReactWrapper } from 'enzyme'; import { mount, ReactWrapper } from 'enzyme';
import { SelectBase } from './SelectBase'; import { SelectBase } from './SelectBase';
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { MultiValueContainer } from './MultiValue';
const onChangeHandler = () => jest.fn(); const onChangeHandler = () => jest.fn();
const findMenuElement = (container: ReactWrapper) => container.find({ 'aria-label': 'Select options menu' }); const findMenuElement = (container: ReactWrapper) => container.find({ 'aria-label': 'Select options menu' });
...@@ -54,6 +55,107 @@ describe('SelectBase', () => { ...@@ -54,6 +55,107 @@ describe('SelectBase', () => {
}); });
}); });
describe('when maxVisibleValues prop', () => {
let excessiveOptions: Array<SelectableValue<number>> = [];
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(
<SelectBase
onChange={onChangeHandler}
isMulti={true}
maxVisibleValues={3}
options={excessiveOptions}
value={excessiveOptions}
isOpen={false}
/>
);
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(
<SelectBase
onChange={onChangeHandler}
isMulti={true}
maxVisibleValues={3}
options={excessiveOptions}
value={excessiveOptions}
showAllSelectedWhenOpen={true}
isOpen={true}
/>
);
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(
<SelectBase
onChange={onChangeHandler}
isMulti={true}
maxVisibleValues={3}
value={excessiveOptions}
options={excessiveOptions}
showAllSelectedWhenOpen={false}
isOpen={true}
/>
);
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(
<SelectBase
onChange={onChangeHandler}
isMulti={true}
options={excessiveOptions}
value={excessiveOptions}
isOpen={false}
/>
);
expect(container.find(MultiValueContainer)).toHaveLength(5);
expect(container.find('#excess-values')).toHaveLength(0);
});
});
});
describe('options', () => { describe('options', () => {
it('renders menu with provided options', () => { it('renders menu with provided options', () => {
const container = mount(<SelectBase options={options} onChange={onChangeHandler} isOpen />); const container = mount(<SelectBase options={options} onChange={onChangeHandler} isOpen />);
......
...@@ -24,6 +24,31 @@ import { getSelectStyles } from './getSelectStyles'; ...@@ -24,6 +24,31 @@ import { getSelectStyles } from './getSelectStyles';
import { cleanValue } from './utils'; import { cleanValue } from './utils';
import { SelectBaseProps, SelectValue } from './types'; 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 (
<span key="excess-values" id="excess-values">
(+{selectedValuesCount - maxVisibleValues})
</span>
);
}
return null;
};
const CustomControl = (props: any) => { const CustomControl = (props: any) => {
const { const {
children, children,
...@@ -66,6 +91,7 @@ export function SelectBase<T>({ ...@@ -66,6 +91,7 @@ export function SelectBase<T>({
allowCustomValue = false, allowCustomValue = false,
autoFocus = false, autoFocus = false,
backspaceRemovesValue = true, backspaceRemovesValue = true,
closeMenuOnSelect = true,
components, components,
defaultOptions, defaultOptions,
defaultValue, defaultValue,
...@@ -83,6 +109,7 @@ export function SelectBase<T>({ ...@@ -83,6 +109,7 @@ export function SelectBase<T>({
loadOptions, loadOptions,
loadingMessage = 'Loading options...', loadingMessage = 'Loading options...',
maxMenuHeight = 300, maxMenuHeight = 300,
maxVisibleValues,
menuPosition, menuPosition,
menuPlacement = 'auto', menuPlacement = 'auto',
noOptionsMessage = 'No options found', noOptionsMessage = 'No options found',
...@@ -98,6 +125,7 @@ export function SelectBase<T>({ ...@@ -98,6 +125,7 @@ export function SelectBase<T>({
placeholder = 'Choose', placeholder = 'Choose',
prefix, prefix,
renderControl, renderControl,
showAllSelectedWhenOpen = true,
tabSelectsValue = true, tabSelectsValue = true,
className, className,
value, value,
...@@ -142,6 +170,7 @@ export function SelectBase<T>({ ...@@ -142,6 +170,7 @@ export function SelectBase<T>({
autoFocus, autoFocus,
backspaceRemovesValue, backspaceRemovesValue,
captureMenuScroll: false, captureMenuScroll: false,
closeMenuOnSelect,
defaultValue, defaultValue,
// Also passing disabled, as this is the new Select API, and I want to use this prop instead of react-select's one // Also passing disabled, as this is the new Select API, and I want to use this prop instead of react-select's one
disabled, disabled,
...@@ -156,6 +185,7 @@ export function SelectBase<T>({ ...@@ -156,6 +185,7 @@ export function SelectBase<T>({
isMulti, isMulti,
isSearchable, isSearchable,
maxMenuHeight, maxMenuHeight,
maxVisibleValues,
menuIsOpen: isOpen, menuIsOpen: isOpen,
menuPlacement, menuPlacement,
menuPosition, menuPosition,
...@@ -171,6 +201,7 @@ export function SelectBase<T>({ ...@@ -171,6 +201,7 @@ export function SelectBase<T>({
placeholder, placeholder,
prefix, prefix,
renderControl, renderControl,
showAllSelectedWhenOpen,
tabSelectsValue, tabSelectsValue,
value: isMulti ? selectedValue : selectedValue[0], value: isMulti ? selectedValue : selectedValue[0],
}; };
...@@ -196,7 +227,22 @@ export function SelectBase<T>({ ...@@ -196,7 +227,22 @@ export function SelectBase<T>({
components={{ components={{
MenuList: SelectMenu, MenuList: SelectMenu,
Group: SelectOptionGroup, 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 <ValueContainer {...props} children={[truncatedValues, ...otherChildren]} />;
}
return <ValueContainer {...props} />;
},
Placeholder: (props: any) => ( Placeholder: (props: any) => (
<div <div
{...props.innerProps} {...props.innerProps}
...@@ -216,7 +262,28 @@ export function SelectBase<T>({ ...@@ -216,7 +262,28 @@ export function SelectBase<T>({
{props.children} {props.children}
</div> </div>
), ),
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 <IndicatorsContainer {...props} children={indicatorChildren} />;
}
return <IndicatorsContainer {...props} />;
},
IndicatorSeparator: () => <></>, IndicatorSeparator: () => <></>,
Control: CustomControl, Control: CustomControl,
Option: SelectMenuOptions, Option: SelectMenuOptions,
......
...@@ -9,6 +9,7 @@ export interface SelectCommonProps<T> { ...@@ -9,6 +9,7 @@ export interface SelectCommonProps<T> {
autoFocus?: boolean; autoFocus?: boolean;
backspaceRemovesValue?: boolean; backspaceRemovesValue?: boolean;
className?: string; className?: string;
closeMenuOnSelect?: boolean;
/** Used for custom components. For more information, see `react-select` */ /** Used for custom components. For more information, see `react-select` */
components?: any; components?: any;
defaultValue?: any; defaultValue?: any;
...@@ -24,7 +25,9 @@ export interface SelectCommonProps<T> { ...@@ -24,7 +25,9 @@ export interface SelectCommonProps<T> {
isOpen?: boolean; isOpen?: boolean;
/** Disables the possibility to type into the input*/ /** Disables the possibility to type into the input*/
isSearchable?: boolean; isSearchable?: boolean;
showAllSelectedWhenOpen?: boolean;
maxMenuHeight?: number; maxMenuHeight?: number;
maxVisibleValues?: number;
menuPlacement?: 'auto' | 'bottom' | 'top'; menuPlacement?: 'auto' | 'bottom' | 'top';
menuPosition?: 'fixed' | 'absolute'; menuPosition?: 'fixed' | 'absolute';
/** The message to display when no options could be found */ /** The message to display when no options could be found */
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment