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
16 changes: 8 additions & 8 deletions docs/pages/material-ui/api/autocomplete.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,14 +161,14 @@
"slotProps": {
"type": {
"name": "shape",
"description": "{ chip?: func<br>&#124;&nbsp;object, clearIndicator?: func<br>&#124;&nbsp;object, listbox?: func<br>&#124;&nbsp;object, paper?: func<br>&#124;&nbsp;object, popper?: func<br>&#124;&nbsp;object, popupIndicator?: func<br>&#124;&nbsp;object, root?: func<br>&#124;&nbsp;object }"
"description": "{ chip?: func<br>&#124;&nbsp;object, clearIndicator?: func<br>&#124;&nbsp;object, listbox?: func<br>&#124;&nbsp;object, noOptions?: func<br>&#124;&nbsp;object, paper?: func<br>&#124;&nbsp;object, popper?: func<br>&#124;&nbsp;object, popupIndicator?: func<br>&#124;&nbsp;object, root?: func<br>&#124;&nbsp;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": "{}"
},
Expand Down Expand Up @@ -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"
},
Comment on lines +214 to +219
{
"name": "paper",
"description": "The component used to render the body of the popup.",
Expand Down Expand Up @@ -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",
Expand Down
5 changes: 1 addition & 4 deletions docs/translations/api-docs/autocomplete/autocomplete.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 &quot;no options&quot; 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.",
Expand Down
11 changes: 11 additions & 0 deletions packages/mui-material/src/Autocomplete/Autocomplete.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { CreateSlotsAndSlotProps, SlotProps } from '../utils/types';

export interface AutocompletePaperSlotPropsOverrides {}
export interface AutocompletePopperSlotPropsOverrides {}
export interface AutocompleteNoOptionsSlotPropsOverrides {}

export {
AutocompleteChangeDetails,
Expand Down Expand Up @@ -136,6 +137,11 @@ export interface AutocompleteSlots {
* @default 'ul'
*/
listbox: React.JSXElementConstructor<React.HTMLAttributes<HTMLElement>>;
/**
* 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
Expand Down Expand Up @@ -185,6 +191,11 @@ export type AutocompleteSlotsAndSlotProps<
{},
AutocompleteOwnerState<Value, Multiple, DisableClearable, FreeSolo, ChipComponent>
>;
noOptions: SlotProps<
'div',
AutocompleteNoOptionsSlotPropsOverrides,
AutocompleteOwnerState<Value, Multiple, DisableClearable, FreeSolo, ChipComponent>
>;
paper: SlotProps<
React.ElementType<Partial<PaperProps>>,
AutocompletePaperSlotPropsOverrides,
Expand Down
40 changes: 27 additions & 13 deletions packages/mui-material/src/Autocomplete/Autocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
});
Comment on lines +607 to +616
Comment on lines +611 to +616

const [PopperSlot, popperProps] = useSlot('popper', {
elementType: Popper,
externalForwardedProps,
Expand Down Expand Up @@ -796,19 +807,20 @@ const Autocomplete = React.forwardRef(function Autocomplete(inProps, ref) {
{loadingText}
</AutocompleteLoading>
) : null}
{renderedOptions.length === 0 && !freeSolo && !loading ? (
<AutocompleteNoOptions
className={classes.noOptions}
ownerState={ownerState}
role="presentation"
onMouseDown={(event) => {
// Prevent input blur when interacting with the "no options" content
event.preventDefault();
}}
>
{noOptionsText}
</AutocompleteNoOptions>
) : null}
<NoOptionsSlot {...noOptionsProps}>
{renderedOptions.length === 0 && !freeSolo && !loading ? (
<AutocompleteNoOptions
className={classes.noOptions}
ownerState={ownerState}
onMouseDown={(event) => {
// Prevent input blur when interacting with the "no options" content
event.preventDefault();
}}
>
{noOptionsText}
</AutocompleteNoOptions>
) : null}
</NoOptionsSlot>
{renderedOptions.length > 0 ? (
<ListboxSlot {...listboxProps}>
{renderedOptions.map((option, index) => {
Expand Down Expand Up @@ -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]),
Expand All @@ -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,
Expand Down
13 changes: 13 additions & 0 deletions packages/mui-material/src/Autocomplete/Autocomplete.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ function AutocompleteComponentsProps() {
renderInput={(params) => <TextField {...params} />}
slotProps={{
clearIndicator: { size: 'large' },
noOptions: { 'aria-label': 'no results' },
paper: { elevation: 2 },
popper: { placement: 'bottom-end' },
popupIndicator: { size: 'large' },
Expand All @@ -170,6 +171,18 @@ function CustomListboxRef() {
);
}

function CustomNoOptionsSlot() {
const ref = React.useRef<HTMLDivElement>(null);
return (
<Autocomplete
renderInput={(params) => <TextField {...params} />}
options={['one', 'two', 'three']}
slots={{ noOptions: 'div' }}
slotProps={{ noOptions: { ref } }}
/>
);
}

// Tests presence of defaultMuiPrevented in event
<Autocomplete
renderInput={(params) => <TextField {...params} />}
Expand Down
72 changes: 72 additions & 0 deletions packages/mui-material/src/Autocomplete/Autocomplete.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4970,6 +4970,78 @@ describe('<Autocomplete />', () => {
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(
<Autocomplete
open
options={[]}
renderInput={(params) => <TextField {...params} autoFocus />}
/>,
);

expect(screen.getByText('No options')).not.to.equal(null);
});

it('should render the custom no options text when there are no options', () => {
render(
<Autocomplete
open
options={[]}
noOptionsText="No results"
renderInput={(params) => <TextField {...params} autoFocus />}
/>,
);

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(
<Autocomplete
open
options={[]}
loading
renderInput={(params) => <TextField {...params} autoFocus />}
/>,
);

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(
<Autocomplete
open
options={[]}
freeSolo
renderInput={(params) => <TextField {...params} autoFocus />}
/>,
);

expect(screen.queryByText('No options')).to.equal(null);
});

it('should always render a status message container for no options', async () => {
const { user } = render(
<Autocomplete
open
options={['one', 'two']}
renderInput={(params) => <TextField {...params} autoFocus />}
/>,
);

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',
Expand Down
Loading