Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions packages/autocomplete/src/lib/AsyncAutocomplete.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,85 @@ describe('AsyncAutocomplete', () => {
});
});

test('prependedOptions should be available', async () => {
const client = new QueryClient();

render(
<QueryClientProvider client={client}>
<AsyncAutocomplete
queryKey="prepended-options"
prependOptions={[{ label: 'Option 0', value: 0 }]}
loadOptions={loadOptions}
FieldProps={{ label: 'Test' }}
/>
</QueryClientProvider>
);

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(
<QueryClientProvider client={client}>
<AsyncAutocomplete
queryKey="prepended-options-unique"
prependOptions={[{ label: 'Option 1', value: 1 }]}
loadOptions={loadOptions}
FieldProps={{ label: 'Test' }}
/>
</QueryClientProvider>
);

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(
<QueryClientProvider client={client}>
<AsyncAutocomplete
queryKey="test-duplicates"
prependOptions={[{ label: 'Option 1', value: 1 }]}
isOptionEqualToValue={(option, value) => option.value === value.value}
loadOptions={loadOptions}
FieldProps={{ label: 'Test' }}
/>
</QueryClientProvider>
);

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();

Expand Down
18 changes: 17 additions & 1 deletion packages/autocomplete/src/lib/AsyncAutocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export interface AsyncAutocompleteProps<
watchParams?: Record<string, unknown>;
/** 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 = <
Expand All @@ -51,6 +53,7 @@ export const AsyncAutocomplete = <
debounceTimeout = 350,
FieldProps,
onInputChange,
prependOptions = [],
...rest
}: AsyncAutocompleteProps<Option, Multiple, DisableClearable, FreeSolo, ChipComponent>) => {
const [inputValue, setInputValue] = useState('');
Expand All @@ -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<HTMLInputElement>,
value: string,
Expand All @@ -99,7 +115,7 @@ export const AsyncAutocomplete = <
},
}}
loading={isFetching}
options={options}
options={finalOptions}
ListboxProps={{
...ListboxProps,
onScroll: async (event: React.SyntheticEvent) => {
Expand Down
40 changes: 30 additions & 10 deletions packages/controlled-form/src/lib/Types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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 = {
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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)[];