Skip to content

Commit 2ad262a

Browse files
Typeahead | fit menu content feature (#161)
This PR aims to introduce a property that allows to fit the menu (MUI paper) with the larger unbroken word option, in case is needed.
1 parent 007ee1e commit 2ad262a

8 files changed

Lines changed: 108 additions & 1 deletion

File tree

CHANGELOG.md

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

88
## [Unreleased]
99

10+
### Added
11+
12+
- support to allow menu size to fit the longest option into `StaticTypeahead` and `AsyncTypeahead`, via `fitMenuContent`.
13+
1014
## [3.15.1] - 2026-01-12
1115

1216
### Fix

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

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -931,7 +931,6 @@ it("works with fixed options excluded", () => {
931931
});
932932

933933
it("innerRef works correctly", () => {
934-
const { simpleOptions } = generateOptions();
935934
const name = faker.random.alpha(10);
936935
const options = generateOptions();
937936

@@ -964,3 +963,39 @@ it("innerRef works correctly", () => {
964963
cy.get("button[title=focus]").click();
965964
cy.get(`#${name}`).should("be.focused");
966965
});
966+
967+
it("works with fitContentMenu", () => {
968+
const name = faker.random.alpha(10);
969+
const specificOptions = [
970+
{ label: "A Very Long Movie Title That Exceeds Normal Lengths", value: "1" },
971+
{ label: "The Lord of the Rings: The Return of the King", value: "2" },
972+
];
973+
974+
cy.mount(
975+
<div className="p-4">
976+
<Form
977+
onSubmit={() => {
978+
// Nothing to do
979+
}}
980+
>
981+
<AsyncTypeaheadInput
982+
style={{ width: 300 }}
983+
queryFn={async (query: string) => await fetchMock(specificOptions, query, true)}
984+
name={name}
985+
label={name}
986+
fitMenuContent
987+
/>
988+
</Form>
989+
</div>,
990+
);
991+
992+
cy.get(`#${name}`).click();
993+
cy.focused().type(specificOptions[0].label);
994+
cy.get(".MuiInputBase-root").should("have.css", "width", "300px");
995+
cy.get("div[role='presentation']").should(($div) => {
996+
const popperWidth = $div.width() ?? 0;
997+
const paperWidth = $div.find("div.MuiPaper-root").width() ?? 0;
998+
expect(paperWidth).to.be.equal(popperWidth);
999+
expect(popperWidth).to.be.greaterThan(300);
1000+
});
1001+
});

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,3 +621,37 @@ it("innerRef works correctly", () => {
621621
cy.get("button[title=focus]").click();
622622
cy.get(`#${name}`).should("be.focused");
623623
});
624+
625+
it("works with fitContentMenu", () => {
626+
const { simpleOptions } = generateOptions();
627+
const name = faker.random.alpha(10);
628+
const specificOptions = ["A Very Long Movie Title That Exceeds Normal Lengths", "The Lord of the Rings: The Return of the King"];
629+
630+
cy.mount(
631+
<div className="p-4">
632+
<Form
633+
onSubmit={() => {
634+
// Nothing to do
635+
}}
636+
>
637+
<StaticTypeaheadInput
638+
style={{ width: 300 }}
639+
multiple
640+
name={name}
641+
label={name}
642+
options={[...specificOptions, ...simpleOptions]}
643+
fitMenuContent
644+
/>
645+
</Form>
646+
</div>,
647+
);
648+
649+
cy.get(`#${name}`).click();
650+
cy.get(".MuiInputBase-root").should("have.css", "width", "300px");
651+
cy.get("div[role='presentation']").should(($div) => {
652+
const popperWidth = $div.width() ?? 0;
653+
const paperWidth = $div.find("div.MuiPaper-root").width() ?? 0;
654+
expect(paperWidth).to.be.equal(popperWidth);
655+
expect(popperWidth).to.be.greaterThan(300);
656+
});
657+
});

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export * from "./lib/types/Form";
1010
export * from "./lib/DatePickerInput";
1111
export * from "./lib/ColorPickerInput";
1212
export * from "./lib/RatingInput";
13+
export * from "./lib/components/Typeahead/TypeaheadFitMenuPopper";
1314
export * from "./lib/helpers/dateUtils";
1415
export * from "./lib/helpers/form";
1516
export * from "./lib/helpers/mui";

src/lib/AsyncTypeaheadInput.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { useFormContext } from "./context/FormContext";
1919
import { TypeaheadTextField } from "./components/Typeahead/TypeaheadTextField";
2020
import { FormGroupLayout } from "./FormGroupLayout";
2121
import { LabelValueOption } from "./types/LabelValueOption";
22+
import { TypeaheadFitMenuPopper } from "./components/Typeahead/TypeaheadFitMenuPopper";
2223

2324
interface AsyncTypeaheadInputRef {
2425
resetValues: () => void;
@@ -75,6 +76,7 @@ const AsyncTypeaheadInput = <T extends FieldValues>(props: AsyncTypeaheadInputPr
7576
autocompleteProps,
7677
fixedOptions,
7778
withFixedOptionsInValue = true,
79+
fitMenuContent,
7880
} = props;
7981

8082
const [options, setOptions] = useState<TypeaheadOptions>(defaultOptions);
@@ -145,6 +147,10 @@ const AsyncTypeaheadInput = <T extends FieldValues>(props: AsyncTypeaheadInputPr
145147
<Autocomplete<TypeaheadOption, boolean, boolean, boolean>
146148
{...autocompleteProps}
147149
{...field}
150+
slots={{
151+
popper: fitMenuContent ? TypeaheadFitMenuPopper : undefined,
152+
...autocompleteProps?.slots,
153+
}}
148154
id={id}
149155
multiple={multiple}
150156
loading={isLoading}

src/lib/StaticTypeaheadInput.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
import { TypeaheadTextField } from "./components/Typeahead/TypeaheadTextField";
2323
import { FormGroupLayout } from "./FormGroupLayout";
2424
import { LabelValueOption } from "./types/LabelValueOption";
25+
import { TypeaheadFitMenuPopper } from "./components/Typeahead/TypeaheadFitMenuPopper";
2526

2627
interface StaticTypeaheadInputProps<T extends FieldValues> extends CommonTypeaheadProps<T> {
2728
options: TypeaheadOptions;
@@ -66,6 +67,7 @@ const StaticTypeaheadInput = <T extends FieldValues>(props: StaticTypeaheadInput
6667
fixedOptions,
6768
withFixedOptionsInValue = true,
6869
innerRef,
70+
fitMenuContent,
6971
} = props;
7072

7173
const [page, setPage] = useState(1);
@@ -118,6 +120,10 @@ const StaticTypeaheadInput = <T extends FieldValues>(props: StaticTypeaheadInput
118120
<Autocomplete<TypeaheadOption, boolean, boolean, boolean>
119121
{...autocompleteProps}
120122
{...field}
123+
slots={{
124+
popper: fitMenuContent ? TypeaheadFitMenuPopper : undefined,
125+
...autocompleteProps?.slots,
126+
}}
121127
id={id}
122128
multiple={multiple}
123129
groupBy={useGroupBy ? groupOptions : undefined}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import Popper, { PopperProps } from "@mui/material/Popper";
2+
3+
const TypeaheadFitMenuPopper = (props: PopperProps) => {
4+
const { anchorEl } = props;
5+
6+
return (
7+
<Popper
8+
{...props}
9+
style={{
10+
// ensure popper is at least as wide as the input field
11+
minWidth: (anchorEl as HTMLElement)?.clientWidth,
12+
// ensure popper fits the longest option width
13+
width: "fit-content",
14+
}}
15+
placement="bottom-start"
16+
/>
17+
);
18+
};
19+
20+
export { TypeaheadFitMenuPopper };

src/lib/types/Typeahead.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ interface CommonTypeaheadProps<T extends FieldValues>
2525
fixedOptions?: TypeaheadOptions;
2626
withFixedOptionsInValue?: boolean;
2727
innerRef?: MutableRefObject<HTMLInputElement | null>;
28+
fitMenuContent?: boolean;
2829
getOptionDisabled?: (option: TypeaheadOption) => boolean;
2930
onChange?: (selected: string | string[]) => void;
3031
onInputChange?: (text: string, reason: AutocompleteInputChangeReason) => void;

0 commit comments

Comments
 (0)