diff --git a/packages/autocomplete/src/lib/AsyncAutocomplete.test.tsx b/packages/autocomplete/src/lib/AsyncAutocomplete.test.tsx index 141eeb1ca..a0d0a1794 100644 --- a/packages/autocomplete/src/lib/AsyncAutocomplete.test.tsx +++ b/packages/autocomplete/src/lib/AsyncAutocomplete.test.tsx @@ -103,6 +103,85 @@ describe('AsyncAutocomplete', () => { }); }); + test('prependedOptions should be available', async () => { + const client = new QueryClient(); + + render( + + + + ); + + const input = screen.getByRole('combobox'); + fireEvent.click(input); + fireEvent.keyDown(input, { key: 'ArrowDown' }); + + waitFor(() => { + expect(screen.getByText('Option 0')).toBeDefined(); + expect(screen.getByText('Option 1')).toBeDefined(); + }); + + fireEvent.click(await screen.findByText('Option 0')); + + waitFor(() => { + expect(screen.getByText('Option 0')).toBeDefined(); + }); + }); + + test('prependedOptions should not be duplicative', async () => { + const client = new QueryClient(); + + render( + + + + ); + + const input = screen.getByRole('combobox'); + fireEvent.click(input); + fireEvent.keyDown(input, { key: 'ArrowDown' }); + + waitFor(() => { + expect(screen.getAllByText('Option 1').length).toBe(1); + }); + }); + + test('should filter duplicates using custom isOptionEqualToValue', async () => { + const client = new QueryClient(); + + render( + + option.value === value.value} + loadOptions={loadOptions} + FieldProps={{ label: 'Test' }} + /> + + ); + + const input = screen.getByRole('combobox'); + fireEvent.click(input); + fireEvent.keyDown(input, { key: 'ArrowDown' }); + + await waitFor(() => { + expect(screen.getByText('Option 1')).toBeDefined(); + expect(screen.getByText('Option 2')).toBeDefined(); + expect(screen.getAllByText('Option 1')).toHaveLength(1); + }); + }); + test('should call loadOptions when scroll to the bottom', async () => { const client = new QueryClient(); diff --git a/packages/autocomplete/src/lib/AsyncAutocomplete.tsx b/packages/autocomplete/src/lib/AsyncAutocomplete.tsx index 0a9c76fde..3af092b3c 100644 --- a/packages/autocomplete/src/lib/AsyncAutocomplete.tsx +++ b/packages/autocomplete/src/lib/AsyncAutocomplete.tsx @@ -33,6 +33,8 @@ export interface AsyncAutocompleteProps< watchParams?: Record; /** Time to wait before searching with the input value typed into the component */ debounceTimeout?: number; + /** Options to prepend to the list (e.g., frequently used items). These will be filtered from loadOptions results to avoid duplicates. */ + prependOptions?: Option[]; } export const AsyncAutocomplete = < @@ -51,6 +53,7 @@ export const AsyncAutocomplete = < debounceTimeout = 350, FieldProps, onInputChange, + prependOptions = [], ...rest }: AsyncAutocompleteProps) => { const [inputValue, setInputValue] = useState(''); @@ -73,6 +76,19 @@ export const AsyncAutocomplete = < const options = data?.pages ? data.pages.map((page) => page.options).flat() : []; + const finalOptions = + prependOptions.length > 0 + ? [ + ...prependOptions, + ...options.filter( + (option) => + !prependOptions.some((prepended) => + rest.isOptionEqualToValue ? rest.isOptionEqualToValue(option, prepended) : option === prepended + ) + ), + ] + : options; + const handleOnInputChange = ( event: React.ChangeEvent, value: string, @@ -99,7 +115,7 @@ export const AsyncAutocomplete = < }, }} loading={isFetching} - options={options} + options={finalOptions} ListboxProps={{ ...ListboxProps, onScroll: async (event: React.SyntheticEvent) => { diff --git a/packages/controlled-form/src/lib/Types.tsx b/packages/controlled-form/src/lib/Types.tsx index 1942d864d..80278b1e1 100644 --- a/packages/controlled-form/src/lib/Types.tsx +++ b/packages/controlled-form/src/lib/Types.tsx @@ -240,7 +240,7 @@ export const TextFieldPropsCategorized: TextFieldPropsObject = { slots: { table: { category: 'Input Props' } }, slotProps: { table: { category: 'Input Props' } }, showCharacterCount: { table: { category: 'Input Props' } }, - displayOverflowMaxLength: { table: { category: 'Input Props' } } + displayOverflowMaxLength: { table: { category: 'Input Props' } }, }; export const RadioGroupPropsCategorized: RadioGroupPropsObject = { @@ -251,7 +251,7 @@ export const RadioGroupPropsCategorized: RadioGroupPropsObject = { label: { table: { category: 'Input Props' } }, ref: { table: { category: 'Input Props' } }, helperText: { table: { category: 'Input Props' } }, - row: { table: { category: 'Input Props' } } + row: { table: { category: 'Input Props' } }, }; export const ProviderAutocompletePropsCategorized: ProviderAutocompletePropsObject = { @@ -309,7 +309,12 @@ export const ProviderAutocompletePropsCategorized: ProviderAutocompletePropsObje defaultToFirstOption: { table: { category: 'Input Props' } }, defaultToOnlyOption: { table: { category: 'Input Props' } }, customerId: { table: { category: 'Input Props' } }, - apiConfig: { table: { category: 'Input Props' } } + apiConfig: { table: { category: 'Input Props' } }, + prependOptions: { + table: { + category: 'Input Props', + }, + }, }; export const OrganizationAutocompletePropsCategorized: OrganizationAutocompletePropsObject = { @@ -366,7 +371,12 @@ export const OrganizationAutocompletePropsCategorized: OrganizationAutocompleteP debounceTimeout: { table: { category: 'Input Props' } }, defaultToFirstOption: { table: { category: 'Input Props' } }, defaultToOnlyOption: { table: { category: 'Input Props' } }, - apiConfig: { table: { category: 'Input Props' } } + apiConfig: { table: { category: 'Input Props' } }, + prependOptions: { + table: { + category: 'Input Props', + }, + }, }; export const DatepickerPropsCategorized: DatepickerPropsObject = { @@ -416,7 +426,7 @@ export const DatepickerPropsCategorized: DatepickerPropsObject = { format: { table: { category: 'Input Props' } }, disableOpenPicker: { table: { category: 'Input Props' } }, placement: { table: { category: 'Input Props' } }, - clearable: { table: { category: 'Input Props' } } + clearable: { table: { category: 'Input Props' } }, }; export const CodesAutocompletePropsCategorized: CodesAutocompletePropsObject = { @@ -474,7 +484,12 @@ export const CodesAutocompletePropsCategorized: CodesAutocompletePropsObject = { debounceTimeout: { table: { category: 'Input Props' } }, defaultToFirstOption: { table: { category: 'Input Props' } }, defaultToOnlyOption: { table: { category: 'Input Props' } }, - apiConfig: { table: { category: 'Input Props' } } + apiConfig: { table: { category: 'Input Props' } }, + prependOptions: { + table: { + category: 'Input Props', + }, + }, }; export const AsyncAutocompletePropsCategorized: AsyncAutocompletePropsObject = { @@ -531,7 +546,12 @@ export const AsyncAutocompletePropsCategorized: AsyncAutocompletePropsObject = { watchParams: { table: { category: 'Input Props' } }, debounceTimeout: { table: { category: 'Input Props' } }, defaultToFirstOption: { table: { category: 'Input Props' } }, - defaultToOnlyOption: { table: { category: 'Input Props' } } + defaultToOnlyOption: { table: { category: 'Input Props' } }, + prependOptions: { + table: { + category: 'Input Props', + }, + }, }; export const AutocompletePropsCategorized: AutocompletePropsObject = { @@ -583,7 +603,7 @@ export const AutocompletePropsCategorized: AutocompletePropsObject = { isOptionEqualToValue: { table: { category: 'Input Props' } }, onHighlightChange: { table: { category: 'Input Props' } }, onInputChange: { table: { category: 'Input Props' } }, - FieldProps: { table: { category: 'Input Props' } } + FieldProps: { table: { category: 'Input Props' } }, }; export const CheckboxPropsCategorized: CheckboxPropsObject = { @@ -620,7 +640,7 @@ export const CheckboxPropsCategorized: CheckboxPropsObject = { checked: { table: { category: 'Input Props' } }, edge: { table: { category: 'Input Props' } }, indeterminate: { table: { category: 'Input Props' } }, - indeterminateIcon: { table: { category: 'Input Props' } } + indeterminateIcon: { table: { category: 'Input Props' } }, }; export const InputPropsCategorized: InputPropsObject = { @@ -715,7 +735,7 @@ export const SelectPropsCategorized: SelectPropsObject = { onClose: { table: { category: 'Input Props' } }, onOpen: { table: { category: 'Input Props' } }, renderValue: { table: { category: 'Input Props' } }, - SelectDisplayProps: { table: { category: 'Input Props' } } + SelectDisplayProps: { table: { category: 'Input Props' } }, }; export const AllControllerPropsList = Object.keys(AllControllerPropertiesCategorized) as (keyof ControllerProps)[];