Skip to content

[autocomplete] Complete automatically highlighted options inline - #49318

Merged
ZeeshanTamboli merged 5 commits into
mui:masterfrom
dvd233:fix/autocomplete-auto-highlight-inline-completion
Oct 13, 2026
Merged

ZeeshanTamboli merged 5 commits into
mui:masterfrom
dvd233:fix/autocomplete-auto-highlight-inline-completion

Conversation

@dvd233

@dvd233 dvd233 commented Oct 5, 2026 •

Copy link
Copy Markdown

Fixes #22648.

With autoComplete and autoHighlight, a growing prefix edit at the end of the input now displays the automatically highlighted option with its remaining suffix selected. The displayed completion stays separate from typed input state and change callbacks, preserving existing freeSolo Enter behavior.

Completion runs after React restores the input. It checks the current input, caret, highlight, and option before writing, and cancels stale work after editing or selection is interrupted. Composition, controlled resets, blur/close, disabled/read-only inputs, and unmount are covered. A check against the existing highlighted option prevents completing a mismatched option without changing highlight synchronization.

Tests cover prefix growth, suffix deletion/replacement, caret edits, callbacks, controlled input, freeSolo, composition guards, and delayed options. Five E2E cases use real keyboard editing and selection assertions with isolated fixtures.

Source-bound validation passed for feature 31385d4a:

  • Full E2E: 20 passed.
  • Autocomplete/useAutocomplete in Chromium, Firefox, and WebKit: 1,415 passed, 10 skipped. Focused JSDOM: 462 passed, 13 skipped.
  • Full default unit suite: 13,520 passed, 1,091 skipped.
  • Repository formatting, full lint, and all 19 TypeScript projects passed.
  • On the unchanged baseline, the exact browser completion assertion failed as expected and the existing navigation control passed.

Native OS IME behavior remains unverified. Implementation, tests, and this description were prepared with AI assistance.

Before: https://stackblitz.com/edit/github-hkaknblh?file=src%2FApp.tsx
After: https://stackblitz.com/edit/7xwz6u6q?file=src%2FApp.tsx

@code-infra-dashboard

code-infra-dashboard Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Deploy preview

https://deploy-preview-49318--material-ui.netlify.app/
QR code for https://deploy-preview-49318--material-ui.netlify.app/

Bundle size

Bundle Parsed size Gzip size
@mui/material 🔺+232B(+0.04%) 🔺+74B(+0.05%)
@mui/lab 0B(0.00%) 0B(0.00%)
@mui/private-theming 0B(0.00%) 0B(0.00%)
@mui/system 0B(0.00%) 0B(0.00%)
@mui/utils 0B(0.00%) 0B(0.00%)

Details of bundle changes


Check out the code infra dashboard for more information about this PR.

@dvd233
dvd233 marked this pull request as ready for review October 5, 2026 23:00
@zannager zannager added the scope: autocomplete Changes related to the autocomplete. This includes ComboBox. label Oct 6, 2026
@zannager
zannager requested a review from silviuaavram October 6, 2026 14:31
@ZeeshanTamboli ZeeshanTamboli added the type: bug It doesn't behave as expected. label Oct 12, 2026

@ZeeshanTamboli ZeeshanTamboli left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this! The behavior is what we want, but the implementation and the tests are much bigger than this issue needs. I'd like to keep the fix simple.

Also, I can't push these changes to your branch myself: maintainers aren't allowed to make changes to this PR ("Allow edits by maintainers" is disabled). Could you apply them, or enable that option?

Fix

The completion only needs to run after the user types forward, once React has restored the controlled input after the change event. Scheduling it from handleInputChange with useTimeout is enough:

  • useEventCallback reads the latest filteredOptions and highlighted index when the timeout fires.
  • useTimeout clears on unmount, and start() replaces any pending call.

So the pending value ref, the composition ref and handlers, the DOM class lookup, the extra effect, and the cancelInputCompletion() calls in ten handlers can all go:

 import usePreviousProps from '@mui/utils/usePreviousProps';
+import useTimeout from '@mui/utils/useTimeout';
 import useOptionValue from './utils/useOptionValue';
+  const inlineCompletionTimeout = useTimeout();
+  const completeInline = useEventCallback(() => {
+    const option = filteredOptions[highlightedIndexRef.current];
+    if (!popupOpen || option === undefined) {
+      return;
+    }
+
+    const optionLabel = getOptionLabel(option);
+    if (optionLabel.toLowerCase().startsWith(inputValue.toLowerCase())) {
+      inputRef.current.value = optionLabel;
+      inputRef.current.setSelectionRange(inputValue.length, optionLabel.length);
+    }
+  });
+
   const handleInputChange = (event) => {
     const newValue = event.target.value;
     const valueChanged = inputValue !== newValue;

+    // Complete the auto highlighted option inline, only when typing forward so that characters
+    // can still be deleted. Deferred until React has restored the controlled input value.
+    if (autoComplete && autoHighlight && newValue.length > inputValue.length) {
+      inlineCompletionTimeout.start(0, completeInline);
+    }
+
     if (valueChanged) {

With this change, the existing Autocomplete and useAutocomplete tests pass in JSDOM and Chromium. If options arrive asynchronously after a keystroke, the completion appears on the next keystroke. That's fine for this issue.

Tests

  • Please remove the useAutocomplete.test.js block and the e2e fixture and tests. Autocomplete.test.js already runs in real browsers (pnpm test:browser), so they duplicate it.
  • Please replace the automatic inline completion block with these two tests in the existing prop: autoComplete describe (and keep the waitFor import). Both fail on master:
    it('should add a completion string for the auto highlighted option while typing', async () => {
      const { user } = render(
        <Autocomplete
          autoComplete
          autoHighlight
          options={['one', 'two']}
          renderInput={(params) => <TextField {...params} />}
        />,
      );
      const textbox = screen.getByRole('combobox');

      await user.type(textbox, 'o');

      await waitFor(() => {
        expect(textbox.value).to.equal('one');
      });
      expect(textbox.selectionStart).to.equal(1);
      expect(textbox.selectionEnd).to.equal(3);

      await user.keyboard('n');

      await waitFor(() => {
        expect(textbox.value).to.equal('one');
      });
      expect(textbox.selectionStart).to.equal(2);
      expect(textbox.selectionEnd).to.equal(3);
    });

    it('should not add a completion string when deleting', async () => {
      const { user } = render(
        <Autocomplete
          autoComplete
          autoHighlight
          options={['one', 'two']}
          renderInput={(params) => <TextField {...params} />}
        />,
      );
      const textbox = screen.getByRole('combobox');

      await user.type(textbox, 'on');
      await waitFor(() => {
        expect(textbox.value).to.equal('one');
      });

      await user.keyboard('{Backspace}');
      expect(textbox.value).to.equal('on');

      await user.keyboard('{Backspace}');
      await act(async () => {
        await new Promise((resolve) => {
          setTimeout(resolve);
        });
      });
      expect(textbox.value).to.equal('o');
    });

Comment thread test/e2e/index.test.ts Resolved
@ZeeshanTamboli ZeeshanTamboli added needs cherry-pick The PR should be cherry-picked to master after merge. v9.x labels Oct 13, 2026
@ZeeshanTamboli

ZeeshanTamboli commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

@dvd233 Thanks for the update! One more simplification in the tests, which is my mistake from the suggested code: the waitFor calls and the act flush aren't needed. Every user.* call already ends by waiting one setTimeout(0) (user-event's default delay: 0), so the 0ms completion timer has already run when await user.type() or await user.keyboard() resolves.

Could you:

  • Remove the waitFor import.
  • Replace the three await waitFor(() => { expect(textbox.value).to.equal('one'); }); with expect(textbox.value).to.equal('one');.
  • Remove the await act(async () => { ... }) block in the deletion test.

Without them, both tests still fail on master, and the deletion test still fails if the completion runs on deletion.

@ZeeshanTamboli ZeeshanTamboli left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Thanks for the PR.

@ZeeshanTamboli
ZeeshanTamboli merged commit 247c238 into mui:master Oct 13, 2026
19 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

Cherry-pick PRs will be created targeting branches: v9.x

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs cherry-pick The PR should be cherry-picked to master after merge. scope: autocomplete Changes related to the autocomplete. This includes ComboBox. type: bug It doesn't behave as expected. v9.x

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[autocomplete] With autoComplete + autoHighlight the highlighted option should appears inline

3 participants