mirror of
https://github.com/grafana/grafana.git
synced 2025-02-20 11:48:34 -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 = () => {
|
||||
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 */
|
||||
|
Loading…
Reference in New Issue
Block a user