Skip to content

Commit 799e754

Browse files
Typeaheads: export typeahead utils and hide placeholder on multiple selection (#129)
This PR aims to: 1- export typeahead utils and debounce hook 2- fix placeholder on both static and async typeahead: the expected behavior is to hide the placeholder when at least one option has been selected 3- adjust a bit the typescript on the typeahead helpers, since types should not be mixed
1 parent 6b151ab commit 799e754

8 files changed

Lines changed: 42 additions & 28 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- Export typeahead helpers and `useDebounceHook` hook.
13+
14+
### Fixed
15+
16+
- Hide placeholder on multiple `AsyncTypeAheadInput` and `StaticTypeAheadInput` when at least one option is selected.
17+
- Typeahead helpers return type from `TypeaheadOption[]` to `TypeaheadOptions`, since typeaheads are not intended to be used with mixed `string` and `LabelValueOption` options.
18+
1019
## [3.1.1] - 2025-04-10
1120

1221
### Fixed

cypress/cypress/component/Typeahead/AsyncTypeaheadInput.cy.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -641,9 +641,6 @@ it("placeholder", () => {
641641
onSubmit={() => {
642642
// Nothing to do
643643
}}
644-
defaultValues={{
645-
[name]: simpleOptions,
646-
}}
647644
>
648645
<AsyncTypeaheadInput
649646
multiple
@@ -662,7 +659,10 @@ it("placeholder", () => {
662659
</Form>
663660
</div>,
664661
);
662+
665663
cy.get(`#${name}`).should("have.attr", "placeholder", placeholder);
664+
simpleOptions.slice(0, 2).forEach((option) => selectOption(name, option));
665+
cy.get(`#${name}`).should("not.have.attr", "placeholder");
666666
});
667667

668668
it("test on input change", () => {

cypress/cypress/component/Typeahead/StaticTypeaheadInput.cy.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -398,19 +398,16 @@ it("placeholder", () => {
398398

399399
cy.mount(
400400
<div className="p-4">
401-
<Form
402-
onSubmit={cy.spy().as("onSubmitSpy")}
403-
defaultValues={{
404-
[name]: simpleOptions,
405-
}}
406-
>
401+
<Form onSubmit={cy.spy().as("onSubmitSpy")}>
407402
<StaticTypeaheadInput multiple name={name} label={name} options={simpleOptions} placeholder={placeholder} />
408403
<input type="submit" className="mt-4" />
409404
</Form>
410405
</div>,
411406
);
412407

413408
cy.get(`#${name}`).should("have.attr", "placeholder", placeholder);
409+
simpleOptions.slice(0, 2).forEach((option) => selectOption(name, option));
410+
cy.get(`#${name}`).should("not.have.attr", "placeholder");
414411
});
415412

416413
it("test on input change", () => {

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,8 @@ export * from "./lib/types/LabelValueOption";
88
export * from "./lib/DatePickerInput";
99
export * from "./lib/ColorPickerInput";
1010
export * from "./lib/helpers/dateUtils";
11+
export * from "./lib/helpers/mui";
12+
export * from "./lib/helpers/typeahead";
13+
export * from "./lib/hooks/useDebounceHook";
1114

1215
export { useFormContext } from "./lib/context/FormContext";

src/lib/AsyncTypeaheadInput.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ const AsyncTypeaheadInput = <T extends FieldValues>(props: AsyncTypeaheadInputPr
6969
autocompleteProps,
7070
} = props;
7171

72-
const [options, setOptions] = useState<TypeaheadOption[]>(defaultOptions);
73-
const [value, setValue] = useState<TypeaheadOption[]>(defaultSelected);
72+
const [options, setOptions] = useState<TypeaheadOptions>(defaultOptions);
73+
const [value, setValue] = useState<TypeaheadOptions>(defaultSelected);
7474
const [page, setPage] = useState(1);
7575
const [loadMoreOptions, setLoadMoreOptions] = useState(limitResults !== undefined && limitResults < defaultOptions.length);
7676
const { name, id } = useSafeNameId(props.name ?? "", props.id);
@@ -107,7 +107,7 @@ const AsyncTypeaheadInput = <T extends FieldValues>(props: AsyncTypeaheadInputPr
107107
defaultSelected.map((x) => (typeof x === "string" ? x : x.value)),
108108
);
109109
},
110-
updateValues: (options: TypeaheadOption[]) => {
110+
updateValues: (options: TypeaheadOptions) => {
111111
const values = convertAutoCompleteOptionsToStringArray(options);
112112
const finalValue = multiple ? values : values[0];
113113
setValue(options);
@@ -171,7 +171,9 @@ const AsyncTypeaheadInput = <T extends FieldValues>(props: AsyncTypeaheadInputPr
171171
field.onBlur();
172172
}}
173173
onChange={(_e, value) => {
174-
const optionsArray = value ? (Array.isArray(value) ? value : [value]) : undefined;
174+
// value is typed as Autocomplete<Value> (aka TypeaheadOption) or an array of Autocomplete<Value> (aka TypeaheadOption[])
175+
// however, the component is not intended to be used with mixed types
176+
const optionsArray = value ? ((Array.isArray(value) ? value : [value]) as TypeaheadOptions) : undefined;
175177
setValue(optionsArray ?? []);
176178
const values = convertAutoCompleteOptionsToStringArray(optionsArray);
177179
const finalValue = multiple ? values : values[0];
@@ -206,7 +208,7 @@ const AsyncTypeaheadInput = <T extends FieldValues>(props: AsyncTypeaheadInputPr
206208
hideValidationMessage={hideValidationMessage}
207209
useBootstrapStyle={useBootstrapStyle}
208210
helpText={helpText}
209-
placeholder={placeholder}
211+
placeholder={multiple && value.length > 0 ? undefined : placeholder}
210212
paginationIcon={paginationIcon}
211213
paginationText={paginationText}
212214
variant={variant}

src/lib/StaticTypeaheadInput.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,9 @@ const StaticTypeaheadInput = <T extends FieldValues>(props: StaticTypeaheadInput
140140
field.onBlur();
141141
}}
142142
onChange={(_, value) => {
143-
const optionsArray = value ? (Array.isArray(value) ? value : [value]) : undefined;
143+
// value is typed as Autocomplete<Value> (aka TypeaheadOption) or an array of Autocomplete<Value> (aka TypeaheadOption[])
144+
// however, the component is not intended to be used with mixed types
145+
const optionsArray = value ? ((Array.isArray(value) ? value : [value]) as TypeaheadOptions) : undefined;
144146
const values = convertAutoCompleteOptionsToStringArray(optionsArray);
145147
const finalValue = multiple ? values : values[0];
146148
clearErrors(field.name);
@@ -169,7 +171,7 @@ const StaticTypeaheadInput = <T extends FieldValues>(props: StaticTypeaheadInput
169171
hideValidationMessage={hideValidationMessage}
170172
useBootstrapStyle={useBootstrapStyle}
171173
helpText={helpText}
172-
placeholder={placeholder}
174+
placeholder={multiple && value.length > 0 ? undefined : placeholder}
173175
paginationIcon={paginationIcon}
174176
paginationText={paginationText}
175177
variant={variant}

src/lib/helpers/typeahead.tsx

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { AutocompleteRenderOptionState } from "@mui/material/Autocomplete";
22
import { LabelValueOption } from "../types/LabelValueOption";
3-
import { TypeaheadOption } from "../types/Typeahead";
3+
import { TypeaheadOption, TypeaheadOptions } from "../types/Typeahead";
44
import AutosuggestHighlightMatch from "autosuggest-highlight/match";
55
import AutosuggestHighlightParse from "autosuggest-highlight/parse";
66

7-
const isStringArray = (options: TypeaheadOption[]): boolean => options.length > 0 && options.every((value) => typeof value === "string");
7+
const isStringArray = (options: TypeaheadOptions): boolean =>
8+
options.length > 0 && (options as TypeaheadOption[]).every((value) => typeof value === "string");
89

9-
const convertAutoCompleteOptionsToStringArray = (options: TypeaheadOption[] | undefined): string[] => {
10+
const convertAutoCompleteOptionsToStringArray = (options: TypeaheadOptions | undefined): string[] => {
1011
if (!options) {
1112
return [];
1213
}
@@ -18,29 +19,29 @@ const convertAutoCompleteOptionsToStringArray = (options: TypeaheadOption[] | un
1819
return (options as LabelValueOption[]).map((option) => option.value) as string[];
1920
};
2021

21-
const getSingleAutoCompleteValue = (options: TypeaheadOption[], fieldValue: string | number | undefined): TypeaheadOption[] => {
22+
const getSingleAutoCompleteValue = (options: TypeaheadOptions, fieldValue: string | number | undefined): TypeaheadOptions => {
2223
if (fieldValue == undefined) {
2324
return [];
2425
}
25-
return options.filter((x) =>
26+
return (options as TypeaheadOption[]).filter((x) =>
2627
// loose equality check to handle different types between form value and option value
2728
typeof x === "string" ? x == fieldValue : x.value == fieldValue,
28-
);
29+
) as TypeaheadOptions;
2930
};
3031

31-
const getMultipleAutoCompleteValue = (options: TypeaheadOption[], fieldValue: (string | number)[] | undefined): TypeaheadOption[] => {
32+
const getMultipleAutoCompleteValue = (options: TypeaheadOptions, fieldValue: (string | number)[] | undefined): TypeaheadOptions => {
3233
if (fieldValue == undefined) {
3334
return [];
3435
}
35-
return options.filter((x) =>
36+
return (options as TypeaheadOption[]).filter((x) =>
3637
typeof x === "string"
3738
? fieldValue.includes(x)
3839
: // ensure that form values matches options values even if they are of different types
3940
fieldValue.map(String).includes(String(x.value as string | number)),
40-
);
41+
) as TypeaheadOptions;
4142
};
4243

43-
const sortOptionsByGroup = (options: TypeaheadOption[]): TypeaheadOption[] =>
44+
const sortOptionsByGroup = (options: TypeaheadOptions): TypeaheadOptions =>
4445
options.sort((x, y) => (typeof x === "string" ? x : x.group?.name ?? "").localeCompare(typeof y === "string" ? y : y.group?.name ?? ""));
4546

4647
const groupOptions = (option: TypeaheadOption): string => (typeof option === "string" ? option : option.group?.name ?? "");

src/lib/hooks/useDebounceHook.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { useEffect, useRef, useState } from "react";
2-
import { TypeaheadOption, TypeaheadOptions } from "../types/Typeahead";
2+
import { TypeaheadOptions } from "../types/Typeahead";
33

44
interface DebounceSearch {
55
query: string;
66
delay: number;
77
}
88

9-
const useDebounceHook = (queryFn: (query: string) => Promise<TypeaheadOptions>, setOptions: (results: TypeaheadOption[]) => void) => {
9+
const useDebounceHook = (queryFn: (query: string) => Promise<TypeaheadOptions>, setOptions: (results: TypeaheadOptions) => void) => {
1010
const queryRef = useRef("");
1111
const [isLoading, setIsLoading] = useState(false);
1212
const [debounceSearch, setDebounceSearch] = useState<DebounceSearch | undefined>(undefined);

0 commit comments

Comments
 (0)