Skip to content

Commit

Permalink
Fix internal query state of AutoComplete
Browse files Browse the repository at this point in the history
  • Loading branch information
wongchito committed Jan 7, 2024
1 parent cff1cf7 commit ed2f00f
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 35 deletions.
34 changes: 19 additions & 15 deletions src/rmg-auto-complete/rmg-auto-complete.stories.tsx
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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 (
<RmgAutoComplete
data={data}
displayValue={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={value}
onChange={item => {
setValue(item);
alert(JSON.stringify(item));
}}
/>
<HStack>
<RmgAutoComplete
data={data}
displayHandler={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));
}}
/>
<Button onClick={() => setSelectedItem(data[0])}>set</Button>
</HStack>
);
};
39 changes: 32 additions & 7 deletions src/rmg-auto-complete/rmg-auto-complete.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: '上海' },
Expand All @@ -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(
<RmgAutoComplete
data={mockData}
displayValue={item => 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}
/>
);
Expand Down Expand Up @@ -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(<RmgAutoComplete data={mockData} filter={filter} {...mockCallbacks} />);

// 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(<RmgAutoComplete data={mockData} filter={filter} value="Guangzhou" {...mockCallbacks} />);
expect(inputEl).toHaveDisplayValue('Guangzhou');
expect(mockCallbacks.onChange).toBeCalledTimes(1);
});
});
38 changes: 27 additions & 11 deletions src/rmg-auto-complete/rmg-auto-complete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> extends Omit<AutoCompleteProps, 'children'> {
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<T extends { id: string }>(props: RmgAutoCompleteProps<T>) {
const { data, displayValue, displayHandler, filter, value, onChange, InputProps, ListProps, ItemProps, ...others } =
props;
export default function RmgAutoComplete<T extends { id: string; value: string }>(props: RmgAutoCompleteProps<T>) {
const { data, displayHandler, filter, value, onChange, InputProps, ListProps, ItemProps, ...others } = props;

const handleFilter = (query: string, optionValue: string) => {
if (!filter) return undefined;
Expand All @@ -35,33 +34,50 @@ export default function RmgAutoComplete<T extends { id: string }>(props: RmgAuto

return (
<AutoComplete
defaultValue={value && displayValue(value)}
value={value}
filter={handleFilter}
onChange={(_: string, item: Item) => onChange?.(item.originalValue)}
suggestWhenEmpty
openOnFocus
{...others}
>
<AutoCompleteInput variant="flushed" size="sm" h={6} autoComplete="off" {...InputProps} />
<AutoCompleteInputWrapper
variant="flushed"
size="sm"
h={6}
autoComplete="off"
value={value}
{...InputProps}
/>
<AutoCompleteList role="menu" py={1} {...ListProps}>
{data.map(item => {
const label = displayValue(item);
return (
<AutoCompleteItem
key={item.id}
value={item}
label={label}
label={item.value}
role="menuitem"
fontSize="sm"
p={1}
mx={1}
{...ItemProps}
>
{displayHandler ? displayHandler(item) : label}
{displayHandler ? displayHandler(item) : item.value}
</AutoCompleteItem>
);
})}
</AutoCompleteList>
</AutoComplete>
);
}

const AutoCompleteInputWrapper = ({ value, ...props }: AutoCompleteInputProps) => {
const { setQuery } = useAutoCompleteContext();

// override query to fix input field display value
useEffect(() => {
setQuery(value ?? '');
}, [value]);

return <AutoCompleteInput {...props} />;
};
2 changes: 1 addition & 1 deletion src/rmg-layout/rmg-layout.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/rmg-loader/rmg-loader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ export default function RmgLoader(props: RmgLoaderProps) {
<CircularProgress isIndeterminate={isIndeterminate} value={value} color={loaderColour} {...others} />
</Flex>
);
};
}
1 change: 1 addition & 0 deletions src/setupTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ global.fetch = vi.fn().mockImplementation((...args: any[]) => {
return originalFetch(args[0], args[1]);
}
});
Element.prototype.scrollIntoView = vi.fn();

0 comments on commit ed2f00f

Please sign in to comment.