Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ CustomAutocomplete.propTypes = {
*
* If used in free solo mode, it must accept both the type of the options and a string.
*
* @param {Value} option
* @param {Value|string} option
* @returns {string}
* @default (option) => option.label ?? option
*/
Expand Down
17 changes: 17 additions & 0 deletions docs/data/material/components/autocomplete/FreeSolo.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,23 @@ export default function FreeSolo() {
/>
)}
/>
<Autocomplete
id="free-solo-demo3"
freeSolo
options={top100Films}
renderInput={(params) => <TextField {...params} label="freeSolo" />}
getOptionLabel={(option) =>
typeof option === 'string' ? option : option.title
}
// this demo demonstrates how the value parameter can be either an object (same type as option) or a string
// it could become a string if, for example, you press "Enter" in the input field
isOptionEqualToValue={(option, value) => {
if (typeof value === 'string') {
return option.title === value;
}
return option.title === value.title;
}}
/>
</Stack>
);
}
Expand Down
17 changes: 17 additions & 0 deletions docs/data/material/components/autocomplete/FreeSolo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,23 @@ export default function FreeSolo() {
/>
)}
/>
<Autocomplete
id="free-solo-demo3"
freeSolo
options={top100Films}
renderInput={(params) => <TextField {...params} label="freeSolo" />}
Copy link
Member

@ZeeshanTamboli ZeeshanTamboli Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to have a different placeholder than others (https://deploy-preview-47801--material-ui.netlify.app/material-ui/react-autocomplete/#search-input):

Suggested change
renderInput={(params) => <TextField {...params} label="freeSolo" />}
renderInput={(params) => <TextField {...params} label="freeSolo (handle string values)" />}

getOptionLabel={(option) =>
typeof option === 'string' ? option : option.title
}
// this demo demonstrates how the value parameter can be either an object (same type as option) or a string
// it could become a string if, for example, you press "Enter" in the input field
isOptionEqualToValue={(option, value) => {
if (typeof value === 'string') {
return option.title === value;
}
return option.title === value.title;
}}
/>
</Stack>
);
}
Expand Down
30 changes: 30 additions & 0 deletions docs/data/material/migration/upgrade-to-v9/upgrade-to-v9.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,33 @@ in the ButtonBase keyboard handlers. This is actually the expected behavior.
#### Listbox toggle on right click

The listbox does not toggle anymore when using right click on the input. The left click toggle behavior remains unchanged.

#### freeSolo type related changes

When the `freeSolo` prop is passed as `true`, the `getOptionLabel` and `isOptionEqualToValue` props
accept `string` as well for their `option` and, respectively, `value` arguments:

```diff
- isOptionEqualToValue?: (option: Value, value: Value) => boolean;
+ isOptionEqualToValue?: (
+ option: Value,
+ value: AutocompleteValueOrFreeSoloValueMapping<Value, FreeSolo>,
+ ) => boolean;
```

```diff
- getOptionLabel?: (option: Value | AutocompleteFreeSoloValueMapping<FreeSolo>) => string;
+ getOptionLabel?: (option: AutocompleteValueOrFreeSoloValueMapping<Value, FreeSolo>) => string;
```

For reference:

```ts
type AutocompleteFreeSoloValueMapping<FreeSolo> = FreeSolo extends true
? string
: never;

type AutocompleteValueOrFreeSoloValueMapping<Value, FreeSolo> = FreeSolo extends true
? Value | string
: Value;
```
4 changes: 2 additions & 2 deletions docs/pages/material-ui/api/autocomplete.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
"getOptionLabel": {
"type": { "name": "func" },
"default": "(option) => option.label ?? option",
"signature": { "type": "function(option: Value) => string", "describedArgs": [] }
"signature": { "type": "function(option: Value | string) => string", "describedArgs": [] }
},
"groupBy": {
"type": { "name": "func" },
Expand All @@ -92,7 +92,7 @@
"isOptionEqualToValue": {
"type": { "name": "func" },
"signature": {
"type": "function(option: Value, value: Value) => boolean",
"type": "function(option: Value, value: Value | string) => boolean",
"describedArgs": ["option", "value"]
}
},
Expand Down
3 changes: 2 additions & 1 deletion packages/mui-material/src/Autocomplete/Autocomplete.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import useAutocomplete, {
createFilterOptions,
UseAutocompleteProps,
AutocompleteFreeSoloValueMapping,
AutocompleteValueOrFreeSoloValueMapping,
} from '../useAutocomplete';
import { AutocompleteClasses } from './autocompleteClasses';
import { CreateSlotsAndSlotProps, SlotProps } from '../utils/types';
Expand Down Expand Up @@ -43,7 +44,7 @@ export type AutocompleteOwnerState<
expanded: boolean;
focused: boolean;
fullWidth: boolean;
getOptionLabel: (option: Value | AutocompleteFreeSoloValueMapping<FreeSolo>) => string;
getOptionLabel: (option: AutocompleteValueOrFreeSoloValueMapping<Value, FreeSolo>) => string;
hasClearIcon: boolean;
hasPopupIcon: boolean;
inputFocused: boolean;
Expand Down
4 changes: 2 additions & 2 deletions packages/mui-material/src/Autocomplete/Autocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -970,7 +970,7 @@ Autocomplete.propTypes /* remove-proptypes */ = {
*
* If used in free solo mode, it must accept both the type of the options and a string.
*
* @param {Value} option
* @param {Value|string} option
* @returns {string}
* @default (option) => option.label ?? option
*/
Expand Down Expand Up @@ -1009,7 +1009,7 @@ Autocomplete.propTypes /* remove-proptypes */ = {
* ⚠️ Both arguments need to be handled, an option can only match with one value.
*
* @param {Value} option The option to test.
* @param {Value} value The value to test against.
* @param {Value|string} value The value to test against.
* @returns {boolean}
*/
isOptionEqualToValue: PropTypes.func,
Expand Down
40 changes: 40 additions & 0 deletions packages/mui-material/src/Autocomplete/Autocomplete.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Autocomplete, {
} from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
import { ChipTypeMap } from '@mui/material/Chip';
import { AutocompleteValueOrFreeSoloValueMapping } from '../useAutocomplete';

interface MyAutocompleteProps<
T,
Expand Down Expand Up @@ -182,3 +183,42 @@ function CustomListboxRef() {
>(event);
}}
/>;

// freeSolo prop adds string to the getOptionLabel and isOptionEqualToValue value argument type
<MyAutocomplete
options={[{ label: '1' }, { label: '2' }]}
renderInput={() => null}
freeSolo
getOptionLabel={(option) => {
expectType<AutocompleteValueOrFreeSoloValueMapping<{ label: string }, true>, typeof option>(
option,
);

return typeof option === 'string' ? option : option.label;
}}
isOptionEqualToValue={(option, value) => {
expectType<AutocompleteValueOrFreeSoloValueMapping<{ label: string }, true>, typeof value>(
value,
);
expectType<{ label: string }, typeof option>(option);

return typeof value === 'string' ? option.label === value : option.label === value.label;
}}
/>;

// getOptionLabel and isOptionEqualToValue value argument type should not include string when freeSolo is false
<MyAutocomplete
options={[{ label: '1' }, { label: '2' }]}
renderInput={() => null}
getOptionLabel={(option) => {
expectType<{ label: string }, typeof option>(option);

return option.label;
}}
isOptionEqualToValue={(option, value) => {
expectType<{ label: string }, typeof value>(value);
expectType<{ label: string }, typeof option>(option);

return option.label === value.label;
}}
/>;
Copy link
Member

@siriwatknp siriwatknp Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/>;
/>;
<Autocomplete
id="free-solo-demo3"
freeSolo
options={top100Films} // array of object
renderInput={() => null}
// @ts-expect-error option could be a string
getOptionLabel={(option) => option.title}
isOptionEqualToValue={(option, value) => {
// @ts-expect-error option could be a string
return option.title === value?.title;
}}
/>

If my understanding is correct, this should be added to ensure typesafety right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the tests, but I'm not sure I follow what you're saying.

To me, when freeSolo is passed, I need to check if the option in getOptionLabel and value in isOptionEqualToValue can accept string as prop. If no freeSolo, they should only be of type Value. And use expectType function to make this check

  getOptionLabel={(option) => {
    expectType<AutocompleteValueOrFreeSoloValueMapping<{ label: string }, true>, typeof option>(
      option,
    );

    return typeof option === 'string' ? option : option.label;
  }}
  isOptionEqualToValue={(option, value) => {
    expectType<AutocompleteValueOrFreeSoloValueMapping<{ label: string }, true>, typeof value>(
      value,
    );
    expectType<{ label: string }, typeof option>(option);

    return typeof value === 'string' ? option.label === value : option.label === value.label;
  }}

and

  getOptionLabel={(option) => {
    expectType<{ label: string }, typeof option>(option);

    return option.label;
  }}
  isOptionEqualToValue={(option, value) => {
    expectType<{ label: string }, typeof value>(value);
    expectType<{ label: string }, typeof option>(option);

    return option.label === value.label;
  }}

14 changes: 10 additions & 4 deletions packages/mui-material/src/useAutocomplete/useAutocomplete.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ export function createFilterOptions<Value>(

export type AutocompleteFreeSoloValueMapping<FreeSolo> = FreeSolo extends true ? string : never;

export type AutocompleteValueOrFreeSoloValueMapping<Value, FreeSolo> = FreeSolo extends true
? Value | string
: Value;

export type AutocompleteValue<Value, Multiple, DisableClearable, FreeSolo> = Multiple extends true
? Array<Value | AutocompleteFreeSoloValueMapping<FreeSolo>>
: DisableClearable extends true
Expand Down Expand Up @@ -175,12 +179,12 @@ export interface UseAutocompleteProps<
*
* If used in free solo mode, it must accept both the type of the options and a string.
*
* @param {Value} option
* @param {Value|string} option
* @returns {string}
* @default (option) => option.label ?? option
*/
getOptionLabel?:
| ((option: Value | AutocompleteFreeSoloValueMapping<FreeSolo>) => string)
| ((option: AutocompleteValueOrFreeSoloValueMapping<Value, FreeSolo>) => string)
| undefined;
/**
* If provided, the options will be grouped under the returned string.
Expand Down Expand Up @@ -217,10 +221,12 @@ export interface UseAutocompleteProps<
* ⚠️ Both arguments need to be handled, an option can only match with one value.
*
* @param {Value} option The option to test.
* @param {Value} value The value to test against.
* @param {Value|string} value The value to test against.
* @returns {boolean}
*/
isOptionEqualToValue?: ((option: Value, value: Value) => boolean) | undefined;
isOptionEqualToValue?:
| ((option: Value, value: AutocompleteValueOrFreeSoloValueMapping<Value, FreeSolo>) => boolean)
| undefined;
/**
* If `true`, `value` must be an array and the menu will support multiple selections.
* @default false
Expand Down
Loading