Files
guide/docs/failures.md

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 dispatchEvent for all interactions (events have isTrusted: 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

  1. isTrusted Check: MUI ClickAwayListener likely checks event.isTrusted. All programmatically dispatched events have isTrusted: false, so they're ignored.

  2. React State Ownership: MUI Autocomplete dropdown visibility is controlled by React state, not DOM attributes. Direct DOM manipulation doesn't work.

  3. Multi-select Behavior: Unlike single-select, multi-select autocomplete is designed to stay open for selecting multiple items. Standard close mechanisms are intentionally disabled.

  4. Escape Bubbling: Escape key events bubble to parent elements, closing modals instead of just dropdowns.

  5. Extension Limitation: Chrome extension automation via dispatchEvent cannot 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:

  1. Data attribute marking: data-dropdown-closed="true" survives React re-renders (React doesn't remove unknown attributes)
  2. CSS with !important: Prevents MUI styles from overwriting our hiding
  3. Filter in ALL queries: check_listbox_visible, _get_options, _get_listbox_options all use :not([data-dropdown-closed]) selector
  4. 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.