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 {
},
};
const BEHAVIOUR_GROUP = 'Behaviour props';
const loadAsyncOptions = () => {
return new Promise<Array<SelectableValue<string>>>(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()}
/>
</>
);
......
......@@ -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<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', () => {
it('renders menu with provided options', () => {
const container = mount(<SelectBase options={options} onChange={onChangeHandler} isOpen />);
......
......@@ -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 (
<span key="excess-values" id="excess-values">
(+{selectedValuesCount - maxVisibleValues})
</span>
);
}
return null;
};
const CustomControl = (props: any) => {
const {
children,
......@@ -66,6 +91,7 @@ export function SelectBase<T>({
allowCustomValue = false,
autoFocus = false,
backspaceRemovesValue = true,
closeMenuOnSelect = true,
components,
defaultOptions,
defaultValue,
......@@ -83,6 +109,7 @@ export function SelectBase<T>({
loadOptions,
loadingMessage = 'Loading options...',
maxMenuHeight = 300,
maxVisibleValues,
menuPosition,
menuPlacement = 'auto',
noOptionsMessage = 'No options found',
......@@ -98,6 +125,7 @@ export function SelectBase<T>({
placeholder = 'Choose',
prefix,
renderControl,
showAllSelectedWhenOpen = true,
tabSelectsValue = true,
className,
value,
......@@ -142,6 +170,7 @@ export function SelectBase<T>({
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<T>({
isMulti,
isSearchable,
maxMenuHeight,
maxVisibleValues,
menuIsOpen: isOpen,
menuPlacement,
menuPosition,
......@@ -171,6 +201,7 @@ export function SelectBase<T>({
placeholder,
prefix,
renderControl,
showAllSelectedWhenOpen,
tabSelectsValue,
value: isMulti ? selectedValue : selectedValue[0],
};
......@@ -196,7 +227,22 @@ export function SelectBase<T>({
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 <ValueContainer {...props} children={[truncatedValues, ...otherChildren]} />;
}
return <ValueContainer {...props} />;
},
Placeholder: (props: any) => (
<div
{...props.innerProps}
......@@ -216,7 +262,28 @@ export function SelectBase<T>({
{props.children}
</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: () => <></>,
Control: CustomControl,
Option: SelectMenuOptions,
......
......@@ -9,6 +9,7 @@ export interface SelectCommonProps<T> {
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<T> {
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 */
......
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