diff --git a/docs/pages/material-ui/api/autocomplete.json b/docs/pages/material-ui/api/autocomplete.json index 5e12636af35030..d0922e78f8ff63 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, noOptionsContainer?: 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, noOptionsContainer?: elementType, paper?: elementType, popper?: elementType, popupIndicator?: elementType, root?: elementType }" }, "default": "{}" }, @@ -211,6 +211,12 @@ "default": "'ul'", "class": "MuiAutocomplete-listbox" }, + { + "name": "noOptionsContainer", + "description": "The component used to render the \"no options\" container.", + "default": "'div'", + "class": "MuiAutocomplete-noOptionsContainer" + }, { "name": "paper", "description": "The component used to render the body of the popup.", diff --git a/docs/translations/api-docs/autocomplete/autocomplete.json b/docs/translations/api-docs/autocomplete/autocomplete.json index 7eb570fb34d844..46016b6f5ac057 100644 --- a/docs/translations/api-docs/autocomplete/autocomplete.json +++ b/docs/translations/api-docs/autocomplete/autocomplete.json @@ -319,6 +319,7 @@ "slotDescriptions": { "clearIndicator": "The component used to render the clear indicator element.", "listbox": "The component used to render the listbox.", + "noOptionsContainer": "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..4bddc19567a1fe 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 AutocompleteNoOptionsContainerSlotPropsOverrides {} 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' + */ + noOptionsContainer: React.ElementType; /** * The component used to render the body of the popup. * @default Paper @@ -185,6 +191,11 @@ export type AutocompleteSlotsAndSlotProps< {}, AutocompleteOwnerState >; + noOptionsContainer: SlotProps< + 'div', + AutocompleteNoOptionsContainerSlotPropsOverrides, + 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..c3135e29314a98 100644 --- a/packages/mui-material/src/Autocomplete/Autocomplete.js +++ b/packages/mui-material/src/Autocomplete/Autocomplete.js @@ -59,6 +59,7 @@ const useUtilityClasses = (ownerState) => { listbox: ['listbox'], loading: ['loading'], noOptions: ['noOptions'], + noOptionsContainer: ['noOptionsContainer'], option: ['option'], groupLabel: ['groupLabel'], groupUl: ['groupUl'], @@ -604,6 +605,18 @@ const Autocomplete = React.forwardRef(function Autocomplete(inProps, ref) { className: classes.paper, }); + const [NoOptionsSlot, noOptionsProps] = useSlot('noOptionsContainer', { + elementType: 'div', + externalForwardedProps, + ownerState, + className: classes.noOptionsContainer, + additionalProps: { + role: 'status', + 'aria-live': 'polite', + 'aria-atomic': 'true', + }, + }); + const [PopperSlot, popperProps] = useSlot('popper', { elementType: Popper, externalForwardedProps, @@ -796,19 +809,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 +1246,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]), + noOptionsContainer: 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 +1259,7 @@ Autocomplete.propTypes /* remove-proptypes */ = { slots: PropTypes.shape({ clearIndicator: PropTypes.elementType, listbox: PropTypes.elementType, + noOptionsContainer: 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..cd037cc40d891e 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' }, + noOptionsContainer: { '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={{ noOptionsContainer: 'div' }} + slotProps={{ noOptionsContainer: { 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..79788d5feecfbb 100644 --- a/packages/mui-material/src/Autocomplete/Autocomplete.test.js +++ b/packages/mui-material/src/Autocomplete/Autocomplete.test.js @@ -93,6 +93,7 @@ describe('', () => { slots: { clearIndicator: { expectedClassName: classes.clearIndicator }, popupIndicator: { expectedClassName: classes.popupIndicator }, + noOptionsContainer: { expectedClassName: classes.noOptionsContainer }, }, only: [ 'slotsProp', @@ -4970,6 +4971,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', diff --git a/packages/mui-material/src/Autocomplete/autocompleteClasses.ts b/packages/mui-material/src/Autocomplete/autocompleteClasses.ts index 863a1719e46490..2910da37a3164c 100644 --- a/packages/mui-material/src/Autocomplete/autocompleteClasses.ts +++ b/packages/mui-material/src/Autocomplete/autocompleteClasses.ts @@ -48,6 +48,8 @@ export interface AutocompleteClasses { loading: string; /** Styles applied to the no option wrapper. */ noOptions: string; + /** Styles applied to the no option container. */ + noOptionsContainer: string; /** Styles applied to the option elements. */ option: string; /** Styles applied to the group's label elements. */ @@ -86,6 +88,7 @@ const autocompleteClasses: AutocompleteClasses = generateUtilityClasses('MuiAuto 'listbox', 'loading', 'noOptions', + 'noOptionsContainer', 'option', 'groupLabel', 'groupUl',