mirror of
https://github.com/grafana/grafana.git
synced 2025-02-15 01:53:33 -06:00
Grafana/ui: fix searchable options for Cascader with options update (#31906)
* update searchableOptions when this.props.options has been updated * add tests for using state to update options for cascader component * update story for cascader * use memoizeOne
This commit is contained in:
parent
3139a60012
commit
6fe2baf168
@ -2,10 +2,36 @@ import { Story } from '@storybook/react';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { NOOP_CONTROL } from '../../utils/storybook/noopControl';
|
||||
import { Cascader } from '@grafana/ui';
|
||||
import { CascaderProps } from './Cascader';
|
||||
import { CascaderOption, CascaderProps } from './Cascader';
|
||||
import mdx from './Cascader.mdx';
|
||||
import React from 'react';
|
||||
|
||||
const onSelect = (val: string) => console.log(val);
|
||||
const options = [
|
||||
{
|
||||
label: 'First',
|
||||
value: '1',
|
||||
items: [
|
||||
{
|
||||
label: 'Second',
|
||||
value: '2',
|
||||
},
|
||||
{
|
||||
label: 'Third',
|
||||
value: '3',
|
||||
},
|
||||
{
|
||||
label: 'Fourth',
|
||||
value: '4',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'FirstFirst',
|
||||
value: '5',
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
title: 'Forms/Cascader',
|
||||
component: Cascader,
|
||||
@ -19,31 +45,8 @@ export default {
|
||||
},
|
||||
},
|
||||
args: {
|
||||
onSelect: (val: string) => console.log(val),
|
||||
options: [
|
||||
{
|
||||
label: 'First',
|
||||
value: '1',
|
||||
items: [
|
||||
{
|
||||
label: 'Second',
|
||||
value: '2',
|
||||
},
|
||||
{
|
||||
label: 'Third',
|
||||
value: '3',
|
||||
},
|
||||
{
|
||||
label: 'Fourth',
|
||||
value: '4',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'FirstFirst',
|
||||
value: '5',
|
||||
},
|
||||
],
|
||||
onSelect,
|
||||
options,
|
||||
},
|
||||
argTypes: {
|
||||
width: { control: { type: 'range', min: 0, max: 70 } },
|
||||
@ -77,3 +80,16 @@ WithDisplayAllSelectedLevels.args = {
|
||||
displayAllSelectedLevels: true,
|
||||
separator: ',',
|
||||
};
|
||||
|
||||
export const WithOptionsStateUpdate = () => {
|
||||
const [updatedOptions, setOptions] = React.useState<CascaderOption[]>([
|
||||
{
|
||||
label: 'Initial state option',
|
||||
value: 'initial',
|
||||
},
|
||||
]);
|
||||
|
||||
setTimeout(() => setOptions(options), 2000);
|
||||
|
||||
return <Cascader options={updatedOptions} onSelect={onSelect} />;
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Cascader } from './Cascader';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { Cascader, CascaderOption, CascaderProps } from './Cascader';
|
||||
import { render, screen, act } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
const options = [
|
||||
@ -28,11 +28,59 @@ const options = [
|
||||
},
|
||||
];
|
||||
|
||||
const CascaderWithOptionsStateUpdate = (props: Omit<CascaderProps, 'options'>) => {
|
||||
const [updatedOptions, setOptions] = React.useState<CascaderOption[]>([
|
||||
{
|
||||
label: 'Initial state option',
|
||||
value: 'initial',
|
||||
},
|
||||
]);
|
||||
|
||||
setTimeout(() => setOptions(options), 1000);
|
||||
|
||||
return <Cascader options={updatedOptions} {...props} />;
|
||||
};
|
||||
|
||||
describe('Cascader', () => {
|
||||
const placeholder = 'cascader-placeholder';
|
||||
|
||||
describe('options from state change', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
it('displays updated options', () => {
|
||||
render(<CascaderWithOptionsStateUpdate placeholder={placeholder} onSelect={jest.fn()} />);
|
||||
|
||||
userEvent.click(screen.getByPlaceholderText(placeholder));
|
||||
|
||||
expect(screen.getByText('Initial state option')).toBeInTheDocument();
|
||||
expect(screen.queryByText('First')).not.toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
userEvent.click(screen.getByPlaceholderText(placeholder));
|
||||
expect(screen.queryByText('Initial state option')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('First')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('filters updated results when searching', () => {
|
||||
render(<CascaderWithOptionsStateUpdate placeholder={placeholder} onSelect={jest.fn()} />);
|
||||
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
userEvent.type(screen.getByPlaceholderText(placeholder), 'Third');
|
||||
expect(screen.queryByText('Second')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('First / Third')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('filters results when searching', () => {
|
||||
render(<Cascader placeholder={placeholder} options={options} onSelect={() => {}} />);
|
||||
render(<Cascader placeholder={placeholder} options={options} onSelect={jest.fn()} />);
|
||||
|
||||
userEvent.type(screen.getByPlaceholderText(placeholder), 'Third');
|
||||
|
||||
@ -78,7 +126,7 @@ describe('Cascader', () => {
|
||||
|
||||
it('displays last level selected when displayAllSelectedLevels is false', () => {
|
||||
render(
|
||||
<Cascader displayAllSelectedLevels={false} placeholder={placeholder} options={options} onSelect={() => {}} />
|
||||
<Cascader displayAllSelectedLevels={false} placeholder={placeholder} options={options} onSelect={jest.fn()} />
|
||||
);
|
||||
|
||||
userEvent.click(screen.getByPlaceholderText(placeholder));
|
||||
@ -89,7 +137,7 @@ describe('Cascader', () => {
|
||||
});
|
||||
|
||||
it('displays last level selected when displayAllSelectedLevels is not passed in', () => {
|
||||
render(<Cascader placeholder={placeholder} options={options} onSelect={() => {}} />);
|
||||
render(<Cascader placeholder={placeholder} options={options} onSelect={jest.fn()} />);
|
||||
|
||||
userEvent.click(screen.getByPlaceholderText(placeholder));
|
||||
userEvent.click(screen.getByText('First'));
|
||||
|
@ -7,6 +7,7 @@ import { Input } from '../Input/Input';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { css } from 'emotion';
|
||||
import { onChangeCascader } from './optionMappings';
|
||||
import memoizeOne from 'memoize-one';
|
||||
|
||||
export interface CascaderProps {
|
||||
/** The separator between levels in the search */
|
||||
@ -27,7 +28,6 @@ export interface CascaderProps {
|
||||
|
||||
interface CascaderState {
|
||||
isSearching: boolean;
|
||||
searchableOptions: Array<SelectableValue<string[]>>;
|
||||
focusCascade: boolean;
|
||||
//Array for cascade navigation
|
||||
rcValue: SelectableValue<string[]>;
|
||||
@ -63,12 +63,11 @@ const DEFAULT_SEPARATOR = '/';
|
||||
export class Cascader extends React.PureComponent<CascaderProps, CascaderState> {
|
||||
constructor(props: CascaderProps) {
|
||||
super(props);
|
||||
const searchableOptions = this.flattenOptions(props.options);
|
||||
const searchableOptions = this.getSearchableOptions(props.options);
|
||||
const { rcValue, activeLabel } = this.setInitialValue(searchableOptions, props.initialValue);
|
||||
this.state = {
|
||||
isSearching: false,
|
||||
focusCascade: false,
|
||||
searchableOptions,
|
||||
rcValue,
|
||||
activeLabel,
|
||||
};
|
||||
@ -94,6 +93,8 @@ export class Cascader extends React.PureComponent<CascaderProps, CascaderState>
|
||||
return selectOptions;
|
||||
};
|
||||
|
||||
getSearchableOptions = memoizeOne((options: CascaderOption[]) => this.flattenOptions(options));
|
||||
|
||||
setInitialValue(searchableOptions: Array<SelectableValue<string[]>>, initValue?: string) {
|
||||
if (!initValue) {
|
||||
return { rcValue: [], activeLabel: '' };
|
||||
@ -183,8 +184,10 @@ export class Cascader extends React.PureComponent<CascaderProps, CascaderState>
|
||||
};
|
||||
|
||||
render() {
|
||||
const { allowCustomValue, placeholder, width, changeOnSelect } = this.props;
|
||||
const { focusCascade, isSearching, searchableOptions, rcValue, activeLabel } = this.state;
|
||||
const { allowCustomValue, placeholder, width, changeOnSelect, options } = this.props;
|
||||
const { focusCascade, isSearching, rcValue, activeLabel } = this.state;
|
||||
|
||||
const searchableOptions = this.getSearchableOptions(options);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
Loading…
Reference in New Issue
Block a user