Repository navigation
[autocomplete] Complete automatically highlighted options inline - #49318
ZeeshanTamboli merged 5 commits into
Conversation
Deploy previewhttps://deploy-preview-49318--material-ui.netlify.app/Bundle size
Check out the code infra dashboard for more information about this PR. |
ZeeshanTamboli
left a comment
There was a problem hiding this comment.
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:
useEventCallbackreads the latestfilteredOptionsand highlighted index when the timeout fires.useTimeoutclears on unmount, andstart()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.jsblock and the e2e fixture and tests.Autocomplete.test.jsalready runs in real browsers (pnpm test:browser), so they duplicate it. - Please replace the
automatic inline completionblock with these two tests in the existingprop: autoCompletedescribe (and keep thewaitForimport). 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');
});|
@dvd233 Thanks for the update! One more simplification in the tests, which is my mistake from the suggested code: the Could you:
Without them, both tests still fail on master, and the deletion test still fails if the completion runs on deletion. |
ZeeshanTamboli
left a comment
There was a problem hiding this comment.
Looks good! Thanks for the PR.
|
Cherry-pick PRs will be created targeting branches: v9.x |
Fixes #22648.
With
autoCompleteandautoHighlight, 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 existingfreeSoloEnter 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: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