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..577433a2b46e13 100644 --- a/packages/mui-material/src/Autocomplete/Autocomplete.js +++ b/packages/mui-material/src/Autocomplete/Autocomplete.js @@ -604,6 +604,17 @@ const Autocomplete = React.forwardRef(function Autocomplete(inProps, ref) { className: classes.paper, }); + const [NoOptionsSlot, noOptionsProps] = useSlot('noOptions', { + elementType: 'div', + externalForwardedProps, + ownerState, + additionalProps: { + role: 'status', + 'aria-live': 'polite', + 'aria-atomic': 'true', + }, + }); + const [PopperSlot, popperProps] = useSlot('popper', { elementType: Popper, externalForwardedProps, @@ -796,19 +807,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 +1244,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 +1257,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',