From 876e18c105aa2a65f78eb437d78def9abc962e5c Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Fri, 12 Mar 2021 14:15:42 +0000 Subject: [PATCH] Grafana/ui: fix searchable options for Cascader with options update (#31906) (#31938) * 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 (cherry picked from commit 6fe2baf1689584d91dd97062afa018c29217fafa) Co-authored-by: Vicky Lee <36230812+vickyyyyyyy@users.noreply.github.com> --- .../components/Cascader/Cascader.story.tsx | 68 ++++++++++++------- .../src/components/Cascader/Cascader.test.tsx | 58 ++++++++++++++-- .../src/components/Cascader/Cascader.tsx | 13 ++-- 3 files changed, 103 insertions(+), 36 deletions(-) diff --git a/packages/grafana-ui/src/components/Cascader/Cascader.story.tsx b/packages/grafana-ui/src/components/Cascader/Cascader.story.tsx index 25a27ed5b9b..b02f48af68f 100644 --- a/packages/grafana-ui/src/components/Cascader/Cascader.story.tsx +++ b/packages/grafana-ui/src/components/Cascader/Cascader.story.tsx @@ -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([ + { + label: 'Initial state option', + value: 'initial', + }, + ]); + + setTimeout(() => setOptions(options), 2000); + + return ; +}; diff --git a/packages/grafana-ui/src/components/Cascader/Cascader.test.tsx b/packages/grafana-ui/src/components/Cascader/Cascader.test.tsx index c956a131b63..a49c383bbc4 100644 --- a/packages/grafana-ui/src/components/Cascader/Cascader.test.tsx +++ b/packages/grafana-ui/src/components/Cascader/Cascader.test.tsx @@ -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) => { + const [updatedOptions, setOptions] = React.useState([ + { + label: 'Initial state option', + value: 'initial', + }, + ]); + + setTimeout(() => setOptions(options), 1000); + + return ; +}; + describe('Cascader', () => { const placeholder = 'cascader-placeholder'; + describe('options from state change', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + it('displays updated options', () => { + render(); + + 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(); + + 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( {}} />); + render(); userEvent.type(screen.getByPlaceholderText(placeholder), 'Third'); @@ -78,7 +126,7 @@ describe('Cascader', () => { it('displays last level selected when displayAllSelectedLevels is false', () => { render( - {}} /> + ); userEvent.click(screen.getByPlaceholderText(placeholder)); @@ -89,7 +137,7 @@ describe('Cascader', () => { }); it('displays last level selected when displayAllSelectedLevels is not passed in', () => { - render( {}} />); + render(); userEvent.click(screen.getByPlaceholderText(placeholder)); userEvent.click(screen.getByText('First')); diff --git a/packages/grafana-ui/src/components/Cascader/Cascader.tsx b/packages/grafana-ui/src/components/Cascader/Cascader.tsx index 7f4eeff7345..c3e2fcd695e 100644 --- a/packages/grafana-ui/src/components/Cascader/Cascader.tsx +++ b/packages/grafana-ui/src/components/Cascader/Cascader.tsx @@ -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>; focusCascade: boolean; //Array for cascade navigation rcValue: SelectableValue; @@ -63,12 +63,11 @@ const DEFAULT_SEPARATOR = '/'; export class Cascader extends React.PureComponent { 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 return selectOptions; }; + getSearchableOptions = memoizeOne((options: CascaderOption[]) => this.flattenOptions(options)); + setInitialValue(searchableOptions: Array>, initValue?: string) { if (!initValue) { return { rcValue: [], activeLabel: '' }; @@ -183,8 +184,10 @@ export class Cascader extends React.PureComponent }; 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 (