From b1c557b9f874b947f96c127e755d7c9c242eaf6b Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Thu, 18 Jun 2026 15:56:34 +0300 Subject: [PATCH 1/3] [autocomplete] Wrap the no results message in a status element --- docs/pages/material-ui/api/autocomplete.json | 16 ++--- .../api-docs/autocomplete/autocomplete.json | 5 +- .../src/Autocomplete/Autocomplete.d.ts | 11 +++ .../src/Autocomplete/Autocomplete.js | 43 +++++++---- .../src/Autocomplete/Autocomplete.spec.tsx | 13 ++++ .../src/Autocomplete/Autocomplete.test.js | 72 +++++++++++++++++++ 6 files changed, 135 insertions(+), 25 deletions(-) diff --git a/docs/pages/material-ui/api/autocomplete.json b/docs/pages/material-ui/api/autocomplete.json index 5e12636af35030..162c7c504cb332 100644 --- a/docs/pages/material-ui/api/autocomplete.json +++ b/docs/pages/material-ui/api/autocomplete.json @@ -161,14 +161,14 @@ "slotProps": { "type": { "name": "shape", - "description": "{ chip?: func
| object, clearIndicator?: func
| object, listbox?: func
| object, paper?: func
| object, popper?: func
| object, popupIndicator?: func
| object, root?: func
| object }" + "description": "{ chip?: func
| object, clearIndicator?: func
| object, listbox?: func
| object, noOptions?: func
| object, paper?: func
| object, popper?: func
| object, popupIndicator?: func
| object, root?: func
| object }" }, "default": "{}" }, "slots": { "type": { "name": "shape", - "description": "{ clearIndicator?: elementType, listbox?: elementType, paper?: elementType, popper?: elementType, popupIndicator?: elementType, root?: elementType }" + "description": "{ clearIndicator?: elementType, listbox?: elementType, noOptions?: elementType, paper?: elementType, popper?: elementType, popupIndicator?: elementType, root?: elementType }" }, "default": "{}" }, @@ -211,6 +211,12 @@ "default": "'ul'", "class": "MuiAutocomplete-listbox" }, + { + "name": "noOptions", + "description": "The component used to render the \"no options\" container.", + "default": "'div'", + "class": "MuiAutocomplete-noOptions" + }, { "name": "paper", "description": "The component used to render the body of the popup.", @@ -303,12 +309,6 @@ "description": "Styles applied to the loading wrapper.", "isGlobal": false }, - { - "key": "noOptions", - "className": "MuiAutocomplete-noOptions", - "description": "Styles applied to the no option wrapper.", - "isGlobal": false - }, { "key": "option", "className": "MuiAutocomplete-option", diff --git a/docs/translations/api-docs/autocomplete/autocomplete.json b/docs/translations/api-docs/autocomplete/autocomplete.json index 7eb570fb34d844..3eb8d8d3b5fd94 100644 --- a/docs/translations/api-docs/autocomplete/autocomplete.json +++ b/docs/translations/api-docs/autocomplete/autocomplete.json @@ -282,10 +282,6 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the loading wrapper" }, - "noOptions": { - "description": "Styles applied to {{nodeName}}.", - "nodeName": "the no option wrapper" - }, "option": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the option elements" @@ -319,6 +315,7 @@ "slotDescriptions": { "clearIndicator": "The component used to render the clear indicator element.", "listbox": "The component used to render the listbox.", + "noOptions": "The component used to render the "no options" container.", "paper": "The component used to render the body of the popup.", "popper": "The component used to position the popup.", "popupIndicator": "The component used to render the popup indicator element.", diff --git a/packages/mui-material/src/Autocomplete/Autocomplete.d.ts b/packages/mui-material/src/Autocomplete/Autocomplete.d.ts index bbd514a792a6d0..0e43d4c979807f 100644 --- a/packages/mui-material/src/Autocomplete/Autocomplete.d.ts +++ b/packages/mui-material/src/Autocomplete/Autocomplete.d.ts @@ -23,6 +23,7 @@ import { CreateSlotsAndSlotProps, SlotProps } from '../utils/types'; export interface AutocompletePaperSlotPropsOverrides {} export interface AutocompletePopperSlotPropsOverrides {} +export interface AutocompleteNoOptionsSlotPropsOverrides {} export { AutocompleteChangeDetails, @@ -136,6 +137,11 @@ export interface AutocompleteSlots { * @default 'ul' */ listbox: React.JSXElementConstructor>; + /** + * The component used to render the "no options" container. + * @default 'div' + */ + noOptions: React.ElementType; /** * The component used to render the body of the popup. * @default Paper @@ -185,6 +191,11 @@ export type AutocompleteSlotsAndSlotProps< {}, AutocompleteOwnerState >; + noOptions: SlotProps< + 'div', + AutocompleteNoOptionsSlotPropsOverrides, + AutocompleteOwnerState + >; paper: SlotProps< React.ElementType>, AutocompletePaperSlotPropsOverrides, diff --git a/packages/mui-material/src/Autocomplete/Autocomplete.js b/packages/mui-material/src/Autocomplete/Autocomplete.js index 8470ab477a273c..2bb57c9a554351 100644 --- a/packages/mui-material/src/Autocomplete/Autocomplete.js +++ b/packages/mui-material/src/Autocomplete/Autocomplete.js @@ -322,6 +322,8 @@ const AutocompleteNoOptions = styled('div', { })), ); +const AutocompleteNoOptionsContainer = styled('div', {})(); + const AutocompleteListbox = styled('ul', { name: 'MuiAutocomplete', slot: 'Listbox', @@ -604,6 +606,18 @@ const Autocomplete = React.forwardRef(function Autocomplete(inProps, ref) { className: classes.paper, }); + const [NoOptionsSlot, noOptionsProps] = useSlot('noOptions', { + elementType: AutocompleteNoOptionsContainer, + externalForwardedProps, + ownerState, + className: classes.noOptions, + additionalProps: { + role: 'status', + 'aria-live': 'polite', + 'aria-atomic': 'true', + }, + }); + const [PopperSlot, popperProps] = useSlot('popper', { elementType: Popper, externalForwardedProps, @@ -796,19 +810,20 @@ const Autocomplete = React.forwardRef(function Autocomplete(inProps, ref) { {loadingText} ) : null} - {renderedOptions.length === 0 && !freeSolo && !loading ? ( - { - // Prevent input blur when interacting with the "no options" content - event.preventDefault(); - }} - > - {noOptionsText} - - ) : null} + + {renderedOptions.length === 0 && !freeSolo && !loading ? ( + { + // Prevent input blur when interacting with the "no options" content + event.preventDefault(); + }} + > + {noOptionsText} + + ) : null} + {renderedOptions.length > 0 ? ( {renderedOptions.map((option, index) => { @@ -1232,6 +1247,7 @@ Autocomplete.propTypes /* remove-proptypes */ = { chip: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), clearIndicator: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), listbox: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + noOptions: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), paper: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), popper: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), popupIndicator: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), @@ -1244,6 +1260,7 @@ Autocomplete.propTypes /* remove-proptypes */ = { slots: PropTypes.shape({ clearIndicator: PropTypes.elementType, listbox: PropTypes.elementType, + noOptions: PropTypes.elementType, paper: PropTypes.elementType, popper: PropTypes.elementType, popupIndicator: PropTypes.elementType, diff --git a/packages/mui-material/src/Autocomplete/Autocomplete.spec.tsx b/packages/mui-material/src/Autocomplete/Autocomplete.spec.tsx index cf9baa121a2e52..4ea9c518f58655 100644 --- a/packages/mui-material/src/Autocomplete/Autocomplete.spec.tsx +++ b/packages/mui-material/src/Autocomplete/Autocomplete.spec.tsx @@ -151,6 +151,7 @@ function AutocompleteComponentsProps() { renderInput={(params) => } slotProps={{ clearIndicator: { size: 'large' }, + noOptions: { 'aria-label': 'no results' }, paper: { elevation: 2 }, popper: { placement: 'bottom-end' }, popupIndicator: { size: 'large' }, @@ -170,6 +171,18 @@ function CustomListboxRef() { ); } +function CustomNoOptionsSlot() { + const ref = React.useRef(null); + return ( + } + options={['one', 'two', 'three']} + slots={{ noOptions: 'div' }} + slotProps={{ noOptions: { ref } }} + /> + ); +} + // Tests presence of defaultMuiPrevented in event } diff --git a/packages/mui-material/src/Autocomplete/Autocomplete.test.js b/packages/mui-material/src/Autocomplete/Autocomplete.test.js index c64005ad1c64da..709ffc05a4458f 100644 --- a/packages/mui-material/src/Autocomplete/Autocomplete.test.js +++ b/packages/mui-material/src/Autocomplete/Autocomplete.test.js @@ -4970,6 +4970,78 @@ describe('', () => { expect(screen.getByTestId('label')).to.have.attribute('data-shrink', 'false'); }); + describe('prop: noOptionsText', () => { + it('should render the no options text when there are no options', () => { + render( + } + />, + ); + + expect(screen.getByText('No options')).not.to.equal(null); + }); + + it('should render the custom no options text when there are no options', () => { + render( + } + />, + ); + + expect(screen.getByText('No results')).not.to.equal(null); + }); + + it('should not render the no options text when loading and there are no options', () => { + render( + } + />, + ); + + expect(screen.queryByText('No options')).to.equal(null); + }); + + it('should not render the no options text when freeSolo is true and there are no options', () => { + render( + } + />, + ); + + expect(screen.queryByText('No options')).to.equal(null); + }); + + it('should always render a status message container for no options', async () => { + const { user } = render( + } + />, + ); + + const status = screen.getByRole('status'); + expect(status).to.have.attribute('aria-live', 'polite'); + expect(status).to.have.attribute('aria-atomic', 'true'); + expect(status.children).to.have.length(0); + + await user.type(screen.getByRole('combobox'), 'three'); + + expect(status.children).to.have.length(1); + }); + }); + // https://github.com/mui/material-ui/issues/47203 it.skipIf(isJsdom())( 'should not scroll the listbox to the top when listbox is scrolled down and one of the end option is clicked', From 3ddda9773d25dd947f26010fdb2c570f746073b8 Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Thu, 18 Jun 2026 19:43:55 +0300 Subject: [PATCH 2/3] no need to style container --- packages/mui-material/src/Autocomplete/Autocomplete.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/mui-material/src/Autocomplete/Autocomplete.js b/packages/mui-material/src/Autocomplete/Autocomplete.js index 2bb57c9a554351..dd72d217534b8f 100644 --- a/packages/mui-material/src/Autocomplete/Autocomplete.js +++ b/packages/mui-material/src/Autocomplete/Autocomplete.js @@ -610,7 +610,6 @@ const Autocomplete = React.forwardRef(function Autocomplete(inProps, ref) { elementType: AutocompleteNoOptionsContainer, externalForwardedProps, ownerState, - className: classes.noOptions, additionalProps: { role: 'status', 'aria-live': 'polite', From 65622b9e0514caf1aaf6c9383fc463139234a0d5 Mon Sep 17 00:00:00 2001 From: Silviu Alexandru Avram Date: Thu, 18 Jun 2026 19:47:20 +0300 Subject: [PATCH 3/3] just use a div for wrapper --- packages/mui-material/src/Autocomplete/Autocomplete.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/mui-material/src/Autocomplete/Autocomplete.js b/packages/mui-material/src/Autocomplete/Autocomplete.js index dd72d217534b8f..577433a2b46e13 100644 --- a/packages/mui-material/src/Autocomplete/Autocomplete.js +++ b/packages/mui-material/src/Autocomplete/Autocomplete.js @@ -322,8 +322,6 @@ const AutocompleteNoOptions = styled('div', { })), ); -const AutocompleteNoOptionsContainer = styled('div', {})(); - const AutocompleteListbox = styled('ul', { name: 'MuiAutocomplete', slot: 'Listbox', @@ -607,7 +605,7 @@ const Autocomplete = React.forwardRef(function Autocomplete(inProps, ref) { }); const [NoOptionsSlot, noOptionsProps] = useSlot('noOptions', { - elementType: AutocompleteNoOptionsContainer, + elementType: 'div', externalForwardedProps, ownerState, additionalProps: {