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:
Vicky Lee 2021-03-11 16:41:08 +00:00 committed by GitHub
parent 3139a60012
commit 6fe2baf168
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 103 additions and 36 deletions

View File

@ -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} />;
};

View File

@ -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'));

View File

@ -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>