5.9 KiB
MUI Autocomplete Dropdown Close - Failed Strategies
Problem Statement
MUI Autocomplete multi-select dropdowns stay open by design for multiple selections. After selecting options, the dropdown must be closed before interacting with other form fields. Otherwise, subsequent fields see the wrong listbox options.
Environment
- Browser automation via Chrome Extension (WebSocket-based, not CDP)
- Extension uses
dispatchEventfor all interactions (events haveisTrusted: false) - MUI v5 Autocomplete with multi-select enabled
- React controlled components
Failed Strategies
1. Simple Blur
input.blur();
document.body.focus();
Result: Dropdown stays open. MUI Autocomplete does not close on blur alone for multi-select.
2. Tab Key Event
const eventProps = {
key: 'Tab',
code: 'Tab',
keyCode: 9,
bubbles: true,
cancelable: true,
composed: true
};
input.dispatchEvent(new KeyboardEvent('keydown', eventProps));
input.dispatchEvent(new KeyboardEvent('keyup', eventProps));
input.blur();
Result: Dropdown stays open. Tab key alone doesn't trigger close for multi-select autocomplete.
3. Click Popup Indicator (Toggle)
await page.click(f"{field_selector} .MuiAutocomplete-popupIndicator")
Result: Inconsistent. Sometimes works, sometimes opens a different dropdown. The popup indicator is a toggle button - if the dropdown is somehow in a weird state, clicking it may re-open instead of close.
4. Click Neutral Element (h2, Dialog Title)
await page.click("h2")
await page.click(".MuiDialogTitle-root")
await page.click(".MuiDialogContent-root")
Result: Dropdown stays open. The click events are dispatched but MUI ClickAwayListener doesn't respond. Likely because programmatic events have isTrusted: false.
5. Mousedown Event on Document (ClickAwayListener)
const event = new MouseEvent('mousedown', {
bubbles: true,
cancelable: true,
view: window,
clientX: rect.left + 5,
clientY: rect.top + 5,
button: 0
});
target.dispatchEvent(event); // target = h2, dialog, body
Result: Dropdown stays open. MUI ClickAwayListener likely checks event.isTrusted which is false for programmatically dispatched events.
6. Escape Key to Input
input.focus();
const eventProps = {
key: 'Escape',
code: 'Escape',
keyCode: 27,
bubbles: true,
cancelable: true,
composed: true
};
input.dispatchEvent(new KeyboardEvent('keydown', eventProps));
input.dispatchEvent(new KeyboardEvent('keyup', eventProps));
input.blur();
Result: CLOSES THE PARENT MODAL. The Escape event bubbles up and closes the dialog, not just the dropdown. This breaks the entire form flow.
7. Escape Key to Active Element (from _open_dropdown)
const active = document.activeElement;
active.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', ... }));
Result: Same as #6 - closes the parent modal.
8. Remove Listbox from DOM
const listbox = document.querySelector('[role="listbox"]');
listbox.remove();
Result: CRASHES REACT. React expects to manage the DOM - removing elements it controls causes "Cannot read properties of null" errors and breaks the form.
9. Set aria-expanded to false
input.setAttribute('aria-expanded', 'false');
Result: Visual state doesn't change. MUI manages this attribute internally via React state. Setting it directly has no effect on the actual dropdown visibility.
Key Insights
-
isTrustedCheck: MUI ClickAwayListener likely checksevent.isTrusted. All programmatically dispatched events haveisTrusted: false, so they're ignored. -
React State Ownership: MUI Autocomplete dropdown visibility is controlled by React state, not DOM attributes. Direct DOM manipulation doesn't work.
-
Multi-select Behavior: Unlike single-select, multi-select autocomplete is designed to stay open for selecting multiple items. Standard close mechanisms are intentionally disabled.
-
Escape Bubbling: Escape key events bubble to parent elements, closing modals instead of just dropdowns.
-
Extension Limitation: Chrome extension automation via
dispatchEventcannot produce trusted events. Only actual user input or CDP Input.dispatchMouseEvent can create trusted events.
Successful Solution ✅
Mark ALL listboxes as closed with data attribute + CSS hiding + filter in queries
// In close logic (after select_multi completes):
const listboxes = document.querySelectorAll('[role="listbox"]:not([data-dropdown-closed])');
listboxes.forEach(listbox => {
listbox.setAttribute('data-dropdown-closed', 'true');
listbox.style.setProperty('display', 'none', 'important');
listbox.style.setProperty('visibility', 'hidden', 'important');
listbox.style.setProperty('pointer-events', 'none', 'important');
});
// In all listbox queries:
const listbox = document.querySelector('[role="listbox"]:not([data-dropdown-closed])');
Why it works:
- Data attribute marking:
data-dropdown-closed="true"survives React re-renders (React doesn't remove unknown attributes) - CSS with !important: Prevents MUI styles from overwriting our hiding
- Filter in ALL queries:
check_listbox_visible,_get_options,_get_listbox_optionsall use:not([data-dropdown-closed])selector - Mark ALL listboxes: Not just the one for current field - catches any orphaned/stale listboxes
Key insight: The issue wasn't that we couldn't close the dropdown - it's that we couldn't prevent subsequent code from finding the stale listbox. By marking and filtering, we make stale listboxes invisible to our automation code.
Previously Attempted (Did Not Work)
CSS Hiding Only (Without Data Attribute)
listbox.style.display = 'none';
Result: React re-renders could remove the style, and queries would still find the element.