mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
871ad73414
commit
3d23ab549b
@ -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 = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
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 = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 = () => {
|
|||||||
setValue(v);
|
setValue(v);
|
||||||
}}
|
}}
|
||||||
{...getDynamicProps()}
|
{...getDynamicProps()}
|
||||||
|
{...getMultiSelectKnobs()}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -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', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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';
|
|||||||
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>({
|
|||||||
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>({
|
|||||||
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>({
|
|||||||
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>({
|
|||||||
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>({
|
|||||||
isMulti,
|
isMulti,
|
||||||
isSearchable,
|
isSearchable,
|
||||||
maxMenuHeight,
|
maxMenuHeight,
|
||||||
|
maxVisibleValues,
|
||||||
menuIsOpen: isOpen,
|
menuIsOpen: isOpen,
|
||||||
menuPlacement,
|
menuPlacement,
|
||||||
menuPosition,
|
menuPosition,
|
||||||
@ -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>({
|
|||||||
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>({
|
|||||||
{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> {
|
|||||||
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> {
|
|||||||
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 */
|
||||||
|
Loading…
Reference in New Issue
Block a user