From ed2f00f22e1c78777a0e4f9a3022a57b6a4e8014 Mon Sep 17 00:00:00 2001 From: Chito Wong Date: Sun, 7 Jan 2024 15:14:17 +0800 Subject: [PATCH] Fix internal query state of AutoComplete --- .../rmg-auto-complete.stories.tsx | 34 +++++++++------- .../rmg-auto-complete.test.tsx | 39 +++++++++++++++---- src/rmg-auto-complete/rmg-auto-complete.tsx | 38 ++++++++++++------ src/rmg-layout/rmg-layout.tsx | 2 +- src/rmg-loader/rmg-loader.tsx | 2 +- src/setupTests.ts | 1 + 6 files changed, 81 insertions(+), 35 deletions(-) diff --git a/src/rmg-auto-complete/rmg-auto-complete.stories.tsx b/src/rmg-auto-complete/rmg-auto-complete.stories.tsx index fa246f2..1da0223 100644 --- a/src/rmg-auto-complete/rmg-auto-complete.stories.tsx +++ b/src/rmg-auto-complete/rmg-auto-complete.stories.tsx @@ -1,5 +1,6 @@ import RmgAutoComplete from './rmg-auto-complete'; import { useState } from 'react'; +import { Button, HStack } from '@chakra-ui/react'; export default { title: 'RmgAutoComplete', @@ -11,23 +12,26 @@ export const Basic = () => { { id: 'gz', flag: '🇨🇳', name: { en: 'Guangzhou', zh: '廣州' } }, { id: 'hk', flag: '🇭🇰', name: { en: 'Hong Kong', zh: '香港' } }, { id: 'london', flag: '🇬🇧', name: { en: 'London', zh: '倫敦' } }, - ]; + ].map(item => ({ ...item, value: item.name.en })); - const [value, setValue] = useState(data[2]); + const [selectedItem, setSelectedItem] = useState(data[2]); return ( - `${item.flag} ${item.name.en}`} - filter={(query, item) => - item.id.toLowerCase().includes(query.toLowerCase()) || - Object.values(item.name).some(name => name.toLowerCase().includes(query.toLowerCase())) - } - value={value} - onChange={item => { - setValue(item); - alert(JSON.stringify(item)); - }} - /> + + `${item.flag} ${item.name.en}`} + filter={(query, item) => + item.id.toLowerCase().includes(query.toLowerCase()) || + Object.values(item.name).some(name => name.toLowerCase().includes(query.toLowerCase())) + } + value={selectedItem.value} + onChange={item => { + setSelectedItem(item); + alert(JSON.stringify(item)); + }} + /> + + ); }; diff --git a/src/rmg-auto-complete/rmg-auto-complete.test.tsx b/src/rmg-auto-complete/rmg-auto-complete.test.tsx index 5cf2041..6daf5f7 100644 --- a/src/rmg-auto-complete/rmg-auto-complete.test.tsx +++ b/src/rmg-auto-complete/rmg-auto-complete.test.tsx @@ -3,7 +3,12 @@ import RmgAutoComplete from './rmg-auto-complete'; import { screen } from '@testing-library/react'; import { userEvent } from '@testing-library/user-event'; -const mockData = [ +type DataType = { + id: string; + value: string; + additionalValue: string; +}; +const mockData: DataType[] = [ { id: 'gz', value: 'Guangzhou', additionalValue: '廣州' }, { id: 'hk', value: 'Hong Kong', additionalValue: '香港' }, { id: 'sh', value: 'Shanghai', additionalValue: '上海' }, @@ -12,17 +17,17 @@ const mockData = [ const mockCallbacks = { onChange: vi.fn(), }; -Element.prototype.scrollIntoView = vi.fn(); + +const filter = (input: string, item: DataType) => + item.value.toLowerCase().includes(input.toLowerCase()) || + item.additionalValue.toLowerCase().includes(input.toLowerCase()); const setup = () => render( item.value + ' (' + item.value[0] + ')'} // Guangzhou (G) - filter={(input, item) => - item.value.toLowerCase().includes(input.toLowerCase()) || - item.additionalValue.toLowerCase().includes(input.toLowerCase()) - } + displayHandler={item => item.value + ' (' + item.value[0] + ')'} // Guangzhou (G) + filter={filter} {...mockCallbacks} /> ); @@ -74,4 +79,24 @@ describe('RmgAutoComplete', () => { expect(mockCallbacks.onChange).toBeCalledTimes(1); expect(mockCallbacks.onChange).toBeCalledWith(expect.objectContaining({ id: 'gz' })); }); + + it('Both autocomplete and input elements are controlled', async () => { + const user = userEvent.setup(); + const { rerender } = render(); + + // initial state + const inputEl = screen.getByRole('textbox'); + expect(inputEl).toHaveDisplayValue(''); + + // select Hong Kong + await user.type(inputEl, 'hong'); + await user.click(screen.getByRole('menuitem', { name: 'Hong Kong' })); + expect(inputEl).toHaveDisplayValue('Hong Kong'); + expect(mockCallbacks.onChange).toBeCalledTimes(1); + + // set state Guangzhou + rerender(); + expect(inputEl).toHaveDisplayValue('Guangzhou'); + expect(mockCallbacks.onChange).toBeCalledTimes(1); + }); }); diff --git a/src/rmg-auto-complete/rmg-auto-complete.tsx b/src/rmg-auto-complete/rmg-auto-complete.tsx index 57c7866..572ef9b 100644 --- a/src/rmg-auto-complete/rmg-auto-complete.tsx +++ b/src/rmg-auto-complete/rmg-auto-complete.tsx @@ -8,24 +8,23 @@ import { AutoCompleteListProps, AutoCompleteProps, Item, + useAutoCompleteContext, } from '@choc-ui/chakra-autocomplete'; -import { ReactElement } from 'react'; +import { ReactElement, useEffect } from 'react'; interface RmgAutoCompleteProps extends Omit { data: T[]; - displayValue: (item: T) => string; displayHandler?: (item: T) => ReactElement | string | number; filter?: (query: string, item: T) => boolean; - value?: T; + value?: string; onChange?: (item: T) => void; InputProps?: AutoCompleteInputProps; ListProps?: AutoCompleteListProps; ItemProps?: AutoCompleteItemProps; } -export default function RmgAutoComplete(props: RmgAutoCompleteProps) { - const { data, displayValue, displayHandler, filter, value, onChange, InputProps, ListProps, ItemProps, ...others } = - props; +export default function RmgAutoComplete(props: RmgAutoCompleteProps) { + const { data, displayHandler, filter, value, onChange, InputProps, ListProps, ItemProps, ...others } = props; const handleFilter = (query: string, optionValue: string) => { if (!filter) return undefined; @@ -35,29 +34,35 @@ export default function RmgAutoComplete(props: RmgAuto return ( onChange?.(item.originalValue)} suggestWhenEmpty openOnFocus {...others} > - + {data.map(item => { - const label = displayValue(item); return ( - {displayHandler ? displayHandler(item) : label} + {displayHandler ? displayHandler(item) : item.value} ); })} @@ -65,3 +70,14 @@ export default function RmgAutoComplete(props: RmgAuto ); } + +const AutoCompleteInputWrapper = ({ value, ...props }: AutoCompleteInputProps) => { + const { setQuery } = useAutoCompleteContext(); + + // override query to fix input field display value + useEffect(() => { + setQuery(value ?? ''); + }, [value]); + + return ; +}; diff --git a/src/rmg-layout/rmg-layout.tsx b/src/rmg-layout/rmg-layout.tsx index a474422..b86ddbb 100644 --- a/src/rmg-layout/rmg-layout.tsx +++ b/src/rmg-layout/rmg-layout.tsx @@ -1,4 +1,4 @@ -import { BoxProps, chakra, Flex, FlexProps, useStyleConfig } from "@chakra-ui/react"; +import { BoxProps, chakra, Flex, FlexProps, useStyleConfig } from '@chakra-ui/react'; export const RmgWindow = (props: FlexProps) => { const { sx, className, ...others } = props; diff --git a/src/rmg-loader/rmg-loader.tsx b/src/rmg-loader/rmg-loader.tsx index 2fe2e72..ad22367 100644 --- a/src/rmg-loader/rmg-loader.tsx +++ b/src/rmg-loader/rmg-loader.tsx @@ -15,4 +15,4 @@ export default function RmgLoader(props: RmgLoaderProps) { ); -}; +} diff --git a/src/setupTests.ts b/src/setupTests.ts index 03ec314..fc7d6a6 100644 --- a/src/setupTests.ts +++ b/src/setupTests.ts @@ -26,3 +26,4 @@ global.fetch = vi.fn().mockImplementation((...args: any[]) => { return originalFetch(args[0], args[1]); } }); +Element.prototype.scrollIntoView = vi.fn();