From 8a72d3a50d09a4d9cb8b84f65325975cc80071a4 Mon Sep 17 00:00:00 2001 From: Michele Masciave Date: Fri, 11 Apr 2025 08:40:14 +0200 Subject: [PATCH 01/10] export typeahead utils --- src/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/index.ts b/src/index.ts index 1aed903..434f057 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,5 +8,9 @@ export * from "./lib/types/LabelValueOption"; export * from "./lib/DatePickerInput"; export * from "./lib/ColorPickerInput"; export * from "./lib/helpers/dateUtils"; +export * from "./lib/helpers/mui"; +export * from "./lib/helpers/typeahead"; +export * from "./lib/hooks/useDebounceHook"; +export * from "./lib/hooks/useSafeNameId"; export { useFormContext } from "./lib/context/FormContext"; From f8dc2b0a903de315cb0b7d6e9032d320b15674a3 Mon Sep 17 00:00:00 2001 From: Michele Masciave Date: Wed, 16 Apr 2025 12:38:57 +0200 Subject: [PATCH 02/10] integrate placeholder fix and changelog --- CHANGELOG.md | 8 ++++++++ .../component/Typeahead/AsyncTypeaheadInput.cy.tsx | 6 +++--- .../component/Typeahead/StaticTypeaheadInput.cy.tsx | 9 +++------ src/index.ts | 1 - src/lib/AsyncTypeaheadInput.tsx | 2 +- src/lib/StaticTypeaheadInput.tsx | 2 +- 6 files changed, 16 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4503f74..c71d00b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Export typeahead helpers and `useDebounceHook` hook. + +### Fixed + +- Hide placeholder on multiple `AsyncTypeAheadInput` and `StaticTypeAheadInput` when at least one option is selected. + ## [3.1.1] - 2025-04-10 ### Fixed diff --git a/cypress/cypress/component/Typeahead/AsyncTypeaheadInput.cy.tsx b/cypress/cypress/component/Typeahead/AsyncTypeaheadInput.cy.tsx index 99dbe00..05f8998 100644 --- a/cypress/cypress/component/Typeahead/AsyncTypeaheadInput.cy.tsx +++ b/cypress/cypress/component/Typeahead/AsyncTypeaheadInput.cy.tsx @@ -641,9 +641,6 @@ it("placeholder", () => { onSubmit={() => { // Nothing to do }} - defaultValues={{ - [name]: simpleOptions, - }} > { , ); + cy.get(`#${name}`).should("have.attr", "placeholder", placeholder); + simpleOptions.slice(0, 2).forEach((option) => selectOption(name, option)); + cy.get(`#${name}`).should("not.have.attr", "placeholder"); }); it("test on input change", () => { diff --git a/cypress/cypress/component/Typeahead/StaticTypeaheadInput.cy.tsx b/cypress/cypress/component/Typeahead/StaticTypeaheadInput.cy.tsx index 8ec1066..ad64378 100644 --- a/cypress/cypress/component/Typeahead/StaticTypeaheadInput.cy.tsx +++ b/cypress/cypress/component/Typeahead/StaticTypeaheadInput.cy.tsx @@ -398,12 +398,7 @@ it("placeholder", () => { cy.mount(
-
+ @@ -411,6 +406,8 @@ it("placeholder", () => { ); cy.get(`#${name}`).should("have.attr", "placeholder", placeholder); + simpleOptions.slice(0, 2).forEach((option) => selectOption(name, option)); + cy.get(`#${name}`).should("not.have.attr", "placeholder"); }); it("test on input change", () => { diff --git a/src/index.ts b/src/index.ts index 434f057..8831d0a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,5 @@ export * from "./lib/helpers/dateUtils"; export * from "./lib/helpers/mui"; export * from "./lib/helpers/typeahead"; export * from "./lib/hooks/useDebounceHook"; -export * from "./lib/hooks/useSafeNameId"; export { useFormContext } from "./lib/context/FormContext"; diff --git a/src/lib/AsyncTypeaheadInput.tsx b/src/lib/AsyncTypeaheadInput.tsx index 42f8662..c2892a2 100644 --- a/src/lib/AsyncTypeaheadInput.tsx +++ b/src/lib/AsyncTypeaheadInput.tsx @@ -206,7 +206,7 @@ const AsyncTypeaheadInput = (props: AsyncTypeaheadInputPr hideValidationMessage={hideValidationMessage} useBootstrapStyle={useBootstrapStyle} helpText={helpText} - placeholder={placeholder} + placeholder={multiple && value.length > 0 ? undefined : placeholder} paginationIcon={paginationIcon} paginationText={paginationText} variant={variant} diff --git a/src/lib/StaticTypeaheadInput.tsx b/src/lib/StaticTypeaheadInput.tsx index b72e47c..ad59824 100644 --- a/src/lib/StaticTypeaheadInput.tsx +++ b/src/lib/StaticTypeaheadInput.tsx @@ -169,7 +169,7 @@ const StaticTypeaheadInput = (props: StaticTypeaheadInput hideValidationMessage={hideValidationMessage} useBootstrapStyle={useBootstrapStyle} helpText={helpText} - placeholder={placeholder} + placeholder={multiple && value.length > 0 ? undefined : placeholder} paginationIcon={paginationIcon} paginationText={paginationText} variant={variant} From a8573843aaf2a7d2b9f1de219c05ce930b328464 Mon Sep 17 00:00:00 2001 From: Michele Masciave Date: Wed, 16 Apr 2025 15:27:23 +0200 Subject: [PATCH 03/10] adjust typescript --- src/lib/AsyncTypeaheadInput.tsx | 10 ++++++---- src/lib/StaticTypeaheadInput.tsx | 4 +++- src/lib/helpers/typeahead.tsx | 20 ++++++++++---------- src/lib/hooks/useDebounceHook.ts | 4 ++-- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/lib/AsyncTypeaheadInput.tsx b/src/lib/AsyncTypeaheadInput.tsx index c2892a2..cab92da 100644 --- a/src/lib/AsyncTypeaheadInput.tsx +++ b/src/lib/AsyncTypeaheadInput.tsx @@ -69,8 +69,8 @@ const AsyncTypeaheadInput = (props: AsyncTypeaheadInputPr autocompleteProps, } = props; - const [options, setOptions] = useState(defaultOptions); - const [value, setValue] = useState(defaultSelected); + const [options, setOptions] = useState(defaultOptions); + const [value, setValue] = useState(defaultSelected); const [page, setPage] = useState(1); const [loadMoreOptions, setLoadMoreOptions] = useState(limitResults !== undefined && limitResults < defaultOptions.length); const { name, id } = useSafeNameId(props.name ?? "", props.id); @@ -107,7 +107,7 @@ const AsyncTypeaheadInput = (props: AsyncTypeaheadInputPr defaultSelected.map((x) => (typeof x === "string" ? x : x.value)), ); }, - updateValues: (options: TypeaheadOption[]) => { + updateValues: (options: TypeaheadOptions) => { const values = convertAutoCompleteOptionsToStringArray(options); const finalValue = multiple ? values : values[0]; setValue(options); @@ -171,7 +171,9 @@ const AsyncTypeaheadInput = (props: AsyncTypeaheadInputPr field.onBlur(); }} onChange={(_e, value) => { - const optionsArray = value ? (Array.isArray(value) ? value : [value]) : undefined; + // value is typed as Autocomplete (aka TypeaheadOption) or an array of Autocomplete (aka TypeaheadOption[]) + // however, the component is not intended to be used with mixed types + const optionsArray = value ? (Array.isArray(value) ? value : [value]) as TypeaheadOptions : undefined; setValue(optionsArray ?? []); const values = convertAutoCompleteOptionsToStringArray(optionsArray); const finalValue = multiple ? values : values[0]; diff --git a/src/lib/StaticTypeaheadInput.tsx b/src/lib/StaticTypeaheadInput.tsx index ad59824..715281a 100644 --- a/src/lib/StaticTypeaheadInput.tsx +++ b/src/lib/StaticTypeaheadInput.tsx @@ -140,7 +140,9 @@ const StaticTypeaheadInput = (props: StaticTypeaheadInput field.onBlur(); }} onChange={(_, value) => { - const optionsArray = value ? (Array.isArray(value) ? value : [value]) : undefined; + // value is typed as Autocomplete (aka TypeaheadOption) or an array of Autocomplete (aka TypeaheadOption[]) + // however, the component is not intended to be used with mixed types + const optionsArray = value ? (Array.isArray(value) ? value : [value]) as TypeaheadOptions : undefined; const values = convertAutoCompleteOptionsToStringArray(optionsArray); const finalValue = multiple ? values : values[0]; clearErrors(field.name); diff --git a/src/lib/helpers/typeahead.tsx b/src/lib/helpers/typeahead.tsx index 7aef6be..de85e7a 100644 --- a/src/lib/helpers/typeahead.tsx +++ b/src/lib/helpers/typeahead.tsx @@ -1,12 +1,12 @@ import { AutocompleteRenderOptionState } from "@mui/material/Autocomplete"; import { LabelValueOption } from "../types/LabelValueOption"; -import { TypeaheadOption } from "../types/Typeahead"; +import { TypeaheadOption, TypeaheadOptions } from "../types/Typeahead"; import AutosuggestHighlightMatch from "autosuggest-highlight/match"; import AutosuggestHighlightParse from "autosuggest-highlight/parse"; -const isStringArray = (options: TypeaheadOption[]): boolean => options.length > 0 && options.every((value) => typeof value === "string"); +const isStringArray = (options: TypeaheadOptions): boolean => options.length > 0 && (options as TypeaheadOption[]).every((value) => typeof value === "string"); -const convertAutoCompleteOptionsToStringArray = (options: TypeaheadOption[] | undefined): string[] => { +const convertAutoCompleteOptionsToStringArray = (options: TypeaheadOptions | undefined): string[] => { if (!options) { return []; } @@ -18,29 +18,29 @@ const convertAutoCompleteOptionsToStringArray = (options: TypeaheadOption[] | un return (options as LabelValueOption[]).map((option) => option.value) as string[]; }; -const getSingleAutoCompleteValue = (options: TypeaheadOption[], fieldValue: string | number | undefined): TypeaheadOption[] => { +const getSingleAutoCompleteValue = (options: TypeaheadOptions, fieldValue: string | number | undefined): TypeaheadOptions => { if (fieldValue == undefined) { return []; } - return options.filter((x) => + return (options as TypeaheadOption[]).filter((x) => // loose equality check to handle different types between form value and option value typeof x === "string" ? x == fieldValue : x.value == fieldValue, - ); + ) as TypeaheadOptions; }; -const getMultipleAutoCompleteValue = (options: TypeaheadOption[], fieldValue: (string | number)[] | undefined): TypeaheadOption[] => { +const getMultipleAutoCompleteValue = (options: TypeaheadOptions, fieldValue: (string | number)[] | undefined): TypeaheadOptions => { if (fieldValue == undefined) { return []; } - return options.filter((x) => + return (options as TypeaheadOption[]).filter((x: TypeaheadOption) => typeof x === "string" ? fieldValue.includes(x) : // ensure that form values matches options values even if they are of different types fieldValue.map(String).includes(String(x.value as string | number)), - ); + ) as TypeaheadOptions; }; -const sortOptionsByGroup = (options: TypeaheadOption[]): TypeaheadOption[] => +const sortOptionsByGroup = (options: TypeaheadOptions): TypeaheadOptions => options.sort((x, y) => (typeof x === "string" ? x : x.group?.name ?? "").localeCompare(typeof y === "string" ? y : y.group?.name ?? "")); const groupOptions = (option: TypeaheadOption): string => (typeof option === "string" ? option : option.group?.name ?? ""); diff --git a/src/lib/hooks/useDebounceHook.ts b/src/lib/hooks/useDebounceHook.ts index c97f72c..31d4595 100644 --- a/src/lib/hooks/useDebounceHook.ts +++ b/src/lib/hooks/useDebounceHook.ts @@ -1,12 +1,12 @@ import { useEffect, useRef, useState } from "react"; -import { TypeaheadOption, TypeaheadOptions } from "../types/Typeahead"; +import { TypeaheadOptions } from "../types/Typeahead"; interface DebounceSearch { query: string; delay: number; } -const useDebounceHook = (queryFn: (query: string) => Promise, setOptions: (results: TypeaheadOption[]) => void) => { +const useDebounceHook = (queryFn: (query: string) => Promise, setOptions: (results: TypeaheadOptions) => void) => { const queryRef = useRef(""); const [isLoading, setIsLoading] = useState(false); const [debounceSearch, setDebounceSearch] = useState(undefined); From e1267ef282dd50afce477920d143f4a4ee555a8c Mon Sep 17 00:00:00 2001 From: Michele Masciave Date: Mon, 28 Apr 2025 09:34:08 +0200 Subject: [PATCH 04/10] prepare-pr --- CHANGELOG.md | 1 + src/lib/AsyncTypeaheadInput.tsx | 2 +- src/lib/StaticTypeaheadInput.tsx | 2 +- src/lib/helpers/typeahead.tsx | 5 +++-- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c71d00b..dc11842 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Export typeahead helpers and `useDebounceHook` hook. +- Typeahead helpers return type from `TypeaheadOption[]` to `TypeaheadOptions`, since typeaheads are not intended to be used with mixed `string` and `LabelValueOption` options. ### Fixed diff --git a/src/lib/AsyncTypeaheadInput.tsx b/src/lib/AsyncTypeaheadInput.tsx index cab92da..c2850f3 100644 --- a/src/lib/AsyncTypeaheadInput.tsx +++ b/src/lib/AsyncTypeaheadInput.tsx @@ -173,7 +173,7 @@ const AsyncTypeaheadInput = (props: AsyncTypeaheadInputPr onChange={(_e, value) => { // value is typed as Autocomplete (aka TypeaheadOption) or an array of Autocomplete (aka TypeaheadOption[]) // however, the component is not intended to be used with mixed types - const optionsArray = value ? (Array.isArray(value) ? value : [value]) as TypeaheadOptions : undefined; + const optionsArray = value ? ((Array.isArray(value) ? value : [value]) as TypeaheadOptions) : undefined; setValue(optionsArray ?? []); const values = convertAutoCompleteOptionsToStringArray(optionsArray); const finalValue = multiple ? values : values[0]; diff --git a/src/lib/StaticTypeaheadInput.tsx b/src/lib/StaticTypeaheadInput.tsx index 715281a..9e29134 100644 --- a/src/lib/StaticTypeaheadInput.tsx +++ b/src/lib/StaticTypeaheadInput.tsx @@ -142,7 +142,7 @@ const StaticTypeaheadInput = (props: StaticTypeaheadInput onChange={(_, value) => { // value is typed as Autocomplete (aka TypeaheadOption) or an array of Autocomplete (aka TypeaheadOption[]) // however, the component is not intended to be used with mixed types - const optionsArray = value ? (Array.isArray(value) ? value : [value]) as TypeaheadOptions : undefined; + const optionsArray = value ? ((Array.isArray(value) ? value : [value]) as TypeaheadOptions) : undefined; const values = convertAutoCompleteOptionsToStringArray(optionsArray); const finalValue = multiple ? values : values[0]; clearErrors(field.name); diff --git a/src/lib/helpers/typeahead.tsx b/src/lib/helpers/typeahead.tsx index de85e7a..d552b32 100644 --- a/src/lib/helpers/typeahead.tsx +++ b/src/lib/helpers/typeahead.tsx @@ -4,7 +4,8 @@ import { TypeaheadOption, TypeaheadOptions } from "../types/Typeahead"; import AutosuggestHighlightMatch from "autosuggest-highlight/match"; import AutosuggestHighlightParse from "autosuggest-highlight/parse"; -const isStringArray = (options: TypeaheadOptions): boolean => options.length > 0 && (options as TypeaheadOption[]).every((value) => typeof value === "string"); +const isStringArray = (options: TypeaheadOptions): boolean => + options.length > 0 && (options as TypeaheadOption[]).every((value) => typeof value === "string"); const convertAutoCompleteOptionsToStringArray = (options: TypeaheadOptions | undefined): string[] => { if (!options) { @@ -32,7 +33,7 @@ const getMultipleAutoCompleteValue = (options: TypeaheadOptions, fieldValue: (st if (fieldValue == undefined) { return []; } - return (options as TypeaheadOption[]).filter((x: TypeaheadOption) => + return (options as TypeaheadOption[]).filter((x) => typeof x === "string" ? fieldValue.includes(x) : // ensure that form values matches options values even if they are of different types From 12211f1448a0b73a6d7b8e9795a8e7633706d4ae Mon Sep 17 00:00:00 2001 From: Michele Masciave Date: Wed, 30 Apr 2025 11:41:20 +0200 Subject: [PATCH 05/10] fixed --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc11842..99f5cb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Export typeahead helpers and `useDebounceHook` hook. -- Typeahead helpers return type from `TypeaheadOption[]` to `TypeaheadOptions`, since typeaheads are not intended to be used with mixed `string` and `LabelValueOption` options. ### Fixed - Hide placeholder on multiple `AsyncTypeAheadInput` and `StaticTypeAheadInput` when at least one option is selected. +- Typeahead helpers return type from `TypeaheadOption[]` to `TypeaheadOptions`, since typeaheads are not intended to be used with mixed `string` and `LabelValueOption` options. ## [3.1.1] - 2025-04-10 From d2e349834c9fdadc45a80d3a7a35a32ac9a816a9 Mon Sep 17 00:00:00 2001 From: Michele Masciave Date: Thu, 5 Feb 2026 11:37:26 +0100 Subject: [PATCH 06/10] solution --- CHANGELOG.md | 8 +++++ .../Typeahead/AsyncTypeaheadInput.cy.tsx | 35 ++++++++++++++++++- .../Typeahead/StaticTypeaheadInput.cy.tsx | 28 +++++++++++++++ src/lib/AsyncTypeaheadInput.tsx | 6 ++++ src/lib/StaticTypeaheadInput.tsx | 6 ++++ src/lib/helpers/mui.ts | 10 +++++- src/lib/types/Typeahead.ts | 1 + 7 files changed, 92 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3ae8a0..17d4e08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- support to allow menu size to fit the longest option into `StaticTypeahead` and `AsyncTypeahead`. + 1. Use `fitMenuContent` property, false by default. + 2. The menu still fits the content of the input whether options length is shorter then the input length; + 3. The menu breaks the option label whether composed by multiple words; + 4. The menu fits the larger option label whether it is a single unbroken word. + ## [3.15.1] - 2026-01-12 ### Fix diff --git a/cypress/cypress/component/Typeahead/AsyncTypeaheadInput.cy.tsx b/cypress/cypress/component/Typeahead/AsyncTypeaheadInput.cy.tsx index 662ce3b..1339041 100644 --- a/cypress/cypress/component/Typeahead/AsyncTypeaheadInput.cy.tsx +++ b/cypress/cypress/component/Typeahead/AsyncTypeaheadInput.cy.tsx @@ -931,7 +931,6 @@ it("works with fixed options excluded", () => { }); it("innerRef works correctly", () => { - const { simpleOptions } = generateOptions(); const name = faker.random.alpha(10); const options = generateOptions(); @@ -964,3 +963,37 @@ it("innerRef works correctly", () => { cy.get("button[title=focus]").click(); cy.get(`#${name}`).should("be.focused"); }); + +it("works with fitContentMenu", () => { + const name = faker.random.alpha(10); + const specificOptions = [ + { label: "AveryLongTitleForAMovieEspeciallyWithoutSpacesButWeWantToEnsureThatTheMenuFitsTheContent", value: "1" }, + { label: "The Lord of the Rings: The Return of the King", value: "2" }, + ]; + + cy.mount( +
+
{ + // Nothing to do + }} + > + await fetchMock(specificOptions, query, true)} + name={name} + label={name} + fitMenuContent + /> + +
, + ); + + cy.get(`#${name}`).click(); + cy.focused().type(specificOptions[0].label); + + cy.get("div[role='presentation']").should(($div) => { + const popperWidth = $div.width() ?? 0; + const paperWidth = $div.find("div.MuiPaper-root").width() ?? 0; + expect(paperWidth).to.be.greaterThan(popperWidth); + }); +}); diff --git a/cypress/cypress/component/Typeahead/StaticTypeaheadInput.cy.tsx b/cypress/cypress/component/Typeahead/StaticTypeaheadInput.cy.tsx index 72ff0cb..205e3b9 100644 --- a/cypress/cypress/component/Typeahead/StaticTypeaheadInput.cy.tsx +++ b/cypress/cypress/component/Typeahead/StaticTypeaheadInput.cy.tsx @@ -621,3 +621,31 @@ it("innerRef works correctly", () => { cy.get("button[title=focus]").click(); cy.get(`#${name}`).should("be.focused"); }); + +it("works with fitContentMenu", () => { + const { simpleOptions } = generateOptions(); + const name = faker.random.alpha(10); + const specificOptions = [ + "AveryLongTitleForAMovieEspeciallyWithoutSpacesButWeWantToEnsureThatTheMenuFitsTheContent", + "The Lord of the Rings: The Return of the King", + ]; + + cy.mount( +
+
{ + // Nothing to do + }} + > + + +
, + ); + + cy.get(`#${name}`).click(); + cy.get("div[role='presentation']").should(($div) => { + const popperWidth = $div.width() ?? 0; + const paperWidth = $div.find("div.MuiPaper-root").width() ?? 0; + expect(paperWidth).to.be.greaterThan(popperWidth); + }); +}); diff --git a/src/lib/AsyncTypeaheadInput.tsx b/src/lib/AsyncTypeaheadInput.tsx index 7c007cf..07cd25a 100644 --- a/src/lib/AsyncTypeaheadInput.tsx +++ b/src/lib/AsyncTypeaheadInput.tsx @@ -19,6 +19,7 @@ import { useFormContext } from "./context/FormContext"; import { TypeaheadTextField } from "./components/Typeahead/TypeaheadTextField"; import { FormGroupLayout } from "./FormGroupLayout"; import { LabelValueOption } from "./types/LabelValueOption"; +import { typeaheadFitContentMenuStyle } from "./helpers/mui"; interface AsyncTypeaheadInputRef { resetValues: () => void; @@ -75,6 +76,7 @@ const AsyncTypeaheadInput = (props: AsyncTypeaheadInputPr autocompleteProps, fixedOptions, withFixedOptionsInValue = true, + fitMenuContent, } = props; const [options, setOptions] = useState(defaultOptions); @@ -145,6 +147,10 @@ const AsyncTypeaheadInput = (props: AsyncTypeaheadInputPr {...autocompleteProps} {...field} + slotProps={{ + ...(fitMenuContent ? { paper: typeaheadFitContentMenuStyle } : {}), + ...autocompleteProps?.slotProps, + }} id={id} multiple={multiple} loading={isLoading} diff --git a/src/lib/StaticTypeaheadInput.tsx b/src/lib/StaticTypeaheadInput.tsx index 1e23df8..1d4c759 100644 --- a/src/lib/StaticTypeaheadInput.tsx +++ b/src/lib/StaticTypeaheadInput.tsx @@ -22,6 +22,7 @@ import { import { TypeaheadTextField } from "./components/Typeahead/TypeaheadTextField"; import { FormGroupLayout } from "./FormGroupLayout"; import { LabelValueOption } from "./types/LabelValueOption"; +import { typeaheadFitContentMenuStyle } from "./helpers/mui"; interface StaticTypeaheadInputProps extends CommonTypeaheadProps { options: TypeaheadOptions; @@ -66,6 +67,7 @@ const StaticTypeaheadInput = (props: StaticTypeaheadInput fixedOptions, withFixedOptionsInValue = true, innerRef, + fitMenuContent, } = props; const [page, setPage] = useState(1); @@ -118,6 +120,10 @@ const StaticTypeaheadInput = (props: StaticTypeaheadInput {...autocompleteProps} {...field} + slotProps={{ + ...(fitMenuContent ? { paper: typeaheadFitContentMenuStyle } : {}), + ...autocompleteProps?.slotProps, + }} id={id} multiple={multiple} groupBy={useGroupBy ? groupOptions : undefined} diff --git a/src/lib/helpers/mui.ts b/src/lib/helpers/mui.ts index d0d2715..3a1d0e7 100644 --- a/src/lib/helpers/mui.ts +++ b/src/lib/helpers/mui.ts @@ -1,3 +1,4 @@ +import { PaperProps } from "@mui/material"; import { SxProps } from "@mui/material/styles"; const textFieldBootstrapStyle: SxProps = { @@ -48,4 +49,11 @@ const textFieldBootstrapStyle: SxProps = { }, }; -export { textFieldBootstrapStyle }; +const typeaheadFitContentMenuStyle: PaperProps = { + sx: { + width: "fit-content !important", // override fit the longest option + minWidth: "100%", // ensure it doesn't get smaller than the input + }, +}; + +export { textFieldBootstrapStyle, typeaheadFitContentMenuStyle }; diff --git a/src/lib/types/Typeahead.ts b/src/lib/types/Typeahead.ts index 9ebb3c0..de2e3e3 100644 --- a/src/lib/types/Typeahead.ts +++ b/src/lib/types/Typeahead.ts @@ -25,6 +25,7 @@ interface CommonTypeaheadProps fixedOptions?: TypeaheadOptions; withFixedOptionsInValue?: boolean; innerRef?: MutableRefObject; + fitMenuContent?: boolean; getOptionDisabled?: (option: TypeaheadOption) => boolean; onChange?: (selected: string | string[]) => void; onInputChange?: (text: string, reason: AutocompleteInputChangeReason) => void; From ff3a6f35d5331d5431e3be2e575b42855715c8e5 Mon Sep 17 00:00:00 2001 From: Michele Masciave Date: Thu, 5 Feb 2026 13:08:24 +0100 Subject: [PATCH 07/10] solution #2 --- CHANGELOG.md | 6 +----- .../Typeahead/AsyncTypeaheadInput.cy.tsx | 11 +++++----- .../Typeahead/StaticTypeaheadInput.cy.tsx | 9 ++++++--- src/lib/AsyncTypeaheadInput.tsx | 8 ++++---- src/lib/StaticTypeaheadInput.tsx | 8 ++++---- .../Typeahead/TypeaheadFitMenuPopper.tsx | 20 +++++++++++++++++++ src/lib/helpers/mui.ts | 10 +--------- 7 files changed, 42 insertions(+), 30 deletions(-) create mode 100644 src/lib/components/Typeahead/TypeaheadFitMenuPopper.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 17d4e08..aa674cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,11 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- support to allow menu size to fit the longest option into `StaticTypeahead` and `AsyncTypeahead`. - 1. Use `fitMenuContent` property, false by default. - 2. The menu still fits the content of the input whether options length is shorter then the input length; - 3. The menu breaks the option label whether composed by multiple words; - 4. The menu fits the larger option label whether it is a single unbroken word. +- support to allow menu size to fit the longest option into `StaticTypeahead` and `AsyncTypeahead`, via `fitMenuContent`. ## [3.15.1] - 2026-01-12 diff --git a/cypress/cypress/component/Typeahead/AsyncTypeaheadInput.cy.tsx b/cypress/cypress/component/Typeahead/AsyncTypeaheadInput.cy.tsx index 1339041..16b1fb7 100644 --- a/cypress/cypress/component/Typeahead/AsyncTypeaheadInput.cy.tsx +++ b/cypress/cypress/component/Typeahead/AsyncTypeaheadInput.cy.tsx @@ -967,8 +967,8 @@ it("innerRef works correctly", () => { it("works with fitContentMenu", () => { const name = faker.random.alpha(10); const specificOptions = [ - { label: "AveryLongTitleForAMovieEspeciallyWithoutSpacesButWeWantToEnsureThatTheMenuFitsTheContent", value: "1" }, - { label: "The Lord of the Rings: The Return of the King", value: "2" }, + {label: "A Very Long Movie Title That Exceeds Normal Lengths", value: "1"}, + {label: "The Lord of the Rings: The Return of the King", value: "2"}, ]; cy.mount( @@ -979,6 +979,7 @@ it("works with fitContentMenu", () => { }} > await fetchMock(specificOptions, query, true)} name={name} label={name} @@ -989,11 +990,11 @@ it("works with fitContentMenu", () => { ); cy.get(`#${name}`).click(); - cy.focused().type(specificOptions[0].label); - + cy.get(".MuiInputBase-root").should("have.css", "width", "300px"); cy.get("div[role='presentation']").should(($div) => { const popperWidth = $div.width() ?? 0; const paperWidth = $div.find("div.MuiPaper-root").width() ?? 0; - expect(paperWidth).to.be.greaterThan(popperWidth); + expect(paperWidth).to.be.equal(popperWidth); + expect(popperWidth).to.be.greaterThan(300); }); }); diff --git a/cypress/cypress/component/Typeahead/StaticTypeaheadInput.cy.tsx b/cypress/cypress/component/Typeahead/StaticTypeaheadInput.cy.tsx index 205e3b9..f8b8ec5 100644 --- a/cypress/cypress/component/Typeahead/StaticTypeaheadInput.cy.tsx +++ b/cypress/cypress/component/Typeahead/StaticTypeaheadInput.cy.tsx @@ -626,7 +626,7 @@ it("works with fitContentMenu", () => { const { simpleOptions } = generateOptions(); const name = faker.random.alpha(10); const specificOptions = [ - "AveryLongTitleForAMovieEspeciallyWithoutSpacesButWeWantToEnsureThatTheMenuFitsTheContent", + "A Very Long Movie Title That Exceeds Normal Lengths", "The Lord of the Rings: The Return of the King", ]; @@ -637,15 +637,18 @@ it("works with fitContentMenu", () => { // Nothing to do }} > - +
, ); cy.get(`#${name}`).click(); + cy.get(".MuiInputBase-root").should("have.css", "width", "300px"); cy.get("div[role='presentation']").should(($div) => { const popperWidth = $div.width() ?? 0; const paperWidth = $div.find("div.MuiPaper-root").width() ?? 0; - expect(paperWidth).to.be.greaterThan(popperWidth); + expect(paperWidth).to.be.equal(popperWidth); + expect(popperWidth).to.be.greaterThan(300); }); }); + diff --git a/src/lib/AsyncTypeaheadInput.tsx b/src/lib/AsyncTypeaheadInput.tsx index 07cd25a..4f855f8 100644 --- a/src/lib/AsyncTypeaheadInput.tsx +++ b/src/lib/AsyncTypeaheadInput.tsx @@ -19,7 +19,7 @@ import { useFormContext } from "./context/FormContext"; import { TypeaheadTextField } from "./components/Typeahead/TypeaheadTextField"; import { FormGroupLayout } from "./FormGroupLayout"; import { LabelValueOption } from "./types/LabelValueOption"; -import { typeaheadFitContentMenuStyle } from "./helpers/mui"; +import { TypeaheadFitMenuPopper } from "./components/Typeahead/TypeaheadFitMenuPopper"; interface AsyncTypeaheadInputRef { resetValues: () => void; @@ -147,9 +147,9 @@ const AsyncTypeaheadInput = (props: AsyncTypeaheadInputPr {...autocompleteProps} {...field} - slotProps={{ - ...(fitMenuContent ? { paper: typeaheadFitContentMenuStyle } : {}), - ...autocompleteProps?.slotProps, + slots={{ + popper: fitMenuContent ? TypeaheadFitMenuPopper : undefined, + ...autocompleteProps?.slots, }} id={id} multiple={multiple} diff --git a/src/lib/StaticTypeaheadInput.tsx b/src/lib/StaticTypeaheadInput.tsx index 1d4c759..3c0faa7 100644 --- a/src/lib/StaticTypeaheadInput.tsx +++ b/src/lib/StaticTypeaheadInput.tsx @@ -22,7 +22,7 @@ import { import { TypeaheadTextField } from "./components/Typeahead/TypeaheadTextField"; import { FormGroupLayout } from "./FormGroupLayout"; import { LabelValueOption } from "./types/LabelValueOption"; -import { typeaheadFitContentMenuStyle } from "./helpers/mui"; +import { TypeaheadFitMenuPopper } from "./components/Typeahead/TypeaheadFitMenuPopper"; interface StaticTypeaheadInputProps extends CommonTypeaheadProps { options: TypeaheadOptions; @@ -120,9 +120,9 @@ const StaticTypeaheadInput = (props: StaticTypeaheadInput {...autocompleteProps} {...field} - slotProps={{ - ...(fitMenuContent ? { paper: typeaheadFitContentMenuStyle } : {}), - ...autocompleteProps?.slotProps, + slots={{ + popper: fitMenuContent ? TypeaheadFitMenuPopper : undefined, + ...autocompleteProps?.slots, }} id={id} multiple={multiple} diff --git a/src/lib/components/Typeahead/TypeaheadFitMenuPopper.tsx b/src/lib/components/Typeahead/TypeaheadFitMenuPopper.tsx new file mode 100644 index 0000000..ef272bc --- /dev/null +++ b/src/lib/components/Typeahead/TypeaheadFitMenuPopper.tsx @@ -0,0 +1,20 @@ +import Popper, { PopperProps } from "@mui/material/Popper"; + +const TypeaheadFitMenuPopper = (props: PopperProps) => { + const { anchorEl } = props; + + return ( + + ); +}; + +export { TypeaheadFitMenuPopper }; diff --git a/src/lib/helpers/mui.ts b/src/lib/helpers/mui.ts index 3a1d0e7..d0d2715 100644 --- a/src/lib/helpers/mui.ts +++ b/src/lib/helpers/mui.ts @@ -1,4 +1,3 @@ -import { PaperProps } from "@mui/material"; import { SxProps } from "@mui/material/styles"; const textFieldBootstrapStyle: SxProps = { @@ -49,11 +48,4 @@ const textFieldBootstrapStyle: SxProps = { }, }; -const typeaheadFitContentMenuStyle: PaperProps = { - sx: { - width: "fit-content !important", // override fit the longest option - minWidth: "100%", // ensure it doesn't get smaller than the input - }, -}; - -export { textFieldBootstrapStyle, typeaheadFitContentMenuStyle }; +export { textFieldBootstrapStyle }; From 513fda5c0a88f611c25fe40c47aab71eb92a6424 Mon Sep 17 00:00:00 2001 From: Michele Masciave Date: Thu, 5 Feb 2026 13:10:12 +0100 Subject: [PATCH 08/10] fix test --- cypress/cypress/component/Typeahead/AsyncTypeaheadInput.cy.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/cypress/cypress/component/Typeahead/AsyncTypeaheadInput.cy.tsx b/cypress/cypress/component/Typeahead/AsyncTypeaheadInput.cy.tsx index 16b1fb7..3909a07 100644 --- a/cypress/cypress/component/Typeahead/AsyncTypeaheadInput.cy.tsx +++ b/cypress/cypress/component/Typeahead/AsyncTypeaheadInput.cy.tsx @@ -990,6 +990,7 @@ it("works with fitContentMenu", () => { ); cy.get(`#${name}`).click(); + cy.focused().type(specificOptions[0].label); cy.get(".MuiInputBase-root").should("have.css", "width", "300px"); cy.get("div[role='presentation']").should(($div) => { const popperWidth = $div.width() ?? 0; From d20b103971dd02ebbc36774088cbdfbb95d39e93 Mon Sep 17 00:00:00 2001 From: Michele Masciave Date: Thu, 5 Feb 2026 13:11:05 +0100 Subject: [PATCH 09/10] prettier --- .../Typeahead/AsyncTypeaheadInput.cy.tsx | 4 ++-- .../Typeahead/StaticTypeaheadInput.cy.tsx | 15 +++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/cypress/cypress/component/Typeahead/AsyncTypeaheadInput.cy.tsx b/cypress/cypress/component/Typeahead/AsyncTypeaheadInput.cy.tsx index 3909a07..c0f1e7f 100644 --- a/cypress/cypress/component/Typeahead/AsyncTypeaheadInput.cy.tsx +++ b/cypress/cypress/component/Typeahead/AsyncTypeaheadInput.cy.tsx @@ -967,8 +967,8 @@ it("innerRef works correctly", () => { it("works with fitContentMenu", () => { const name = faker.random.alpha(10); const specificOptions = [ - {label: "A Very Long Movie Title That Exceeds Normal Lengths", value: "1"}, - {label: "The Lord of the Rings: The Return of the King", value: "2"}, + { label: "A Very Long Movie Title That Exceeds Normal Lengths", value: "1" }, + { label: "The Lord of the Rings: The Return of the King", value: "2" }, ]; cy.mount( diff --git a/cypress/cypress/component/Typeahead/StaticTypeaheadInput.cy.tsx b/cypress/cypress/component/Typeahead/StaticTypeaheadInput.cy.tsx index f8b8ec5..b808db5 100644 --- a/cypress/cypress/component/Typeahead/StaticTypeaheadInput.cy.tsx +++ b/cypress/cypress/component/Typeahead/StaticTypeaheadInput.cy.tsx @@ -625,10 +625,7 @@ it("innerRef works correctly", () => { it("works with fitContentMenu", () => { const { simpleOptions } = generateOptions(); const name = faker.random.alpha(10); - const specificOptions = [ - "A Very Long Movie Title That Exceeds Normal Lengths", - "The Lord of the Rings: The Return of the King", - ]; + const specificOptions = ["A Very Long Movie Title That Exceeds Normal Lengths", "The Lord of the Rings: The Return of the King"]; cy.mount(
@@ -637,7 +634,14 @@ it("works with fitContentMenu", () => { // Nothing to do }} > - +
, ); @@ -651,4 +655,3 @@ it("works with fitContentMenu", () => { expect(popperWidth).to.be.greaterThan(300); }); }); - From 650b43d7e824ba67da59469c96ab4b4d7f297d1b Mon Sep 17 00:00:00 2001 From: Michele Masciave Date: Thu, 5 Feb 2026 13:24:43 +0100 Subject: [PATCH 10/10] export also TypeaheadFitMenuPopper --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index cc0178a..7f5b4fe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ export * from "./lib/types/Form"; export * from "./lib/DatePickerInput"; export * from "./lib/ColorPickerInput"; export * from "./lib/RatingInput"; +export * from "./lib/components/Typeahead/TypeaheadFitMenuPopper"; export * from "./lib/helpers/dateUtils"; export * from "./lib/helpers/form"; export * from "./lib/helpers/mui";