Skip to content

feat: add autoFocusPartialMatch to combo boxes - #12438

Merged
vursen merged 31 commits into
mainfrom
feat/combo-box-auto-select-mode
Aug 18, 2026
Merged

feat: add autoFocusPartialMatch to combo boxes#12438
vursen merged 31 commits into
mainfrom
feat/combo-box-auto-select-mode

Conversation

@vursen

@vursen vursen commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Typing a filter and committing it with Tab, Enter, or an outside click only selects an item when the input text matches an item label exactly. Users filling long forms want to type a partial filter and commit it without pressing Arrow Down first (#1280), or have the only remaining item selected automatically (#9636).

This adds an opt-in autoFocusPartialMatch property (none | first-match | only-match, default none) to combo-box and multi-select-combo-box. With first-match, the first item in the filtered results is focused; with only-match, the item is focused when filtering narrows the results to a single item.

The focused item is highlighted in the dropdown while typing and is committed through the existing commit paths, so Enter, Tab, blur, and outside click all work. In multi-select-combo-box the selection happens on Enter or item click, since that component intentionally never commits on blur.

An exact label match is always focused regardless of the property, and partial matches are not focused when allowCustomValue is enabled. A partial match is also only focused while the dropdown is open: with autoOpenDisabled, typing alone does not focus or commit a match, and the match is focused once the user opens the dropdown.

Example for manual testing
<!doctype html>
<html lang="en">
  <head>
    <title>Combo Box Auto Focus Partial Match</title>
    <script type="module" src="./common.js"></script>
  </head>
  <body>
    <p>
      Items: apple, banana, grapefruit, grape. Type a filter (e.g. <code>gra</code>, <code>grape</code>,
      <code>ban</code>) and commit it with Tab, Enter, or an outside click.
    </p>

    <vaadin-combo-box label="none (default)"></vaadin-combo-box>
    <vaadin-combo-box label="first-match" auto-focus-partial-match="first-match"></vaadin-combo-box>
    <vaadin-combo-box label="only-match" auto-focus-partial-match="only-match"></vaadin-combo-box>
    <vaadin-combo-box
      label="first-match + allow-custom-value"
      auto-focus-partial-match="first-match"
      allow-custom-value
    ></vaadin-combo-box>
    <vaadin-combo-box
      label="first-match + auto-open-disabled"
      auto-focus-partial-match="first-match"
      auto-open-disabled
    ></vaadin-combo-box>
    <vaadin-combo-box
      id="lazy"
      label="first-match + lazy countries"
      auto-focus-partial-match="first-match"
    ></vaadin-combo-box>
    <vaadin-multi-select-combo-box
      label="multi-select first-match"
      auto-focus-partial-match="first-match"
    ></vaadin-multi-select-combo-box>
    <vaadin-multi-select-combo-box
      label="multi-select only-match"
      auto-focus-partial-match="only-match"
    ></vaadin-multi-select-combo-box>

    <script type="module">
      import '@vaadin/combo-box';
      import '@vaadin/multi-select-combo-box';

      const items = ['apple', 'banana', 'grapefruit', 'grape'];

      document.querySelectorAll('vaadin-combo-box, vaadin-multi-select-combo-box').forEach((comboBox) => {
        if (comboBox.id !== 'lazy') {
          comboBox.items = items;
        }
      });

      // Lazy data provider with emulated network latency, for testing
      // the deferred commit when blurring before data is loaded.
      document.querySelector('#lazy').dataProvider = async (params, callback) => {
        const index = params.page * params.pageSize;
        const response = await fetch(
          `https://demo.vaadin.com/demo-data/1.0/filtered-countries?index=${index}&count=${params.pageSize}&filter=${params.filter}`,
        );
        if (response.ok) {
          const { result, size } = await response.json();
          setTimeout(() => callback(result, size), 500);
        }
      };
    </script>
  </body>
</html>

Part of #1280

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vursen vursen changed the title feat: add autoSelectMode to combo-box and multi-select-combo-box feat: add autoSelectMode to combo-boxes Aug 14, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vursen
vursen force-pushed the feat/combo-box-auto-select-mode branch from e959494 to 6153891 Compare August 14, 2026 11:53
vursen and others added 8 commits August 14, 2026 15:56
Value commit for a focused item is already covered by other suites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The focused-index behavior they cover is generic and already tested
in keyboard.test.js.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vursen
vursen force-pushed the feat/combo-box-auto-select-mode branch from a7430cf to 49e5e85 Compare August 14, 2026 17:07
vursen and others added 2 commits August 14, 2026 21:12
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Align with the agreed Flow API: setAutoFocusPartialMatch with
NONE, FIRST_MATCH, and ONLY_MATCH values.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vursen vursen changed the title feat: add autoSelectMode to combo-boxes feat: add autoFocusPartialMatch to combo-boxes Aug 17, 2026
@vursen
vursen force-pushed the feat/combo-box-auto-select-mode branch 4 times, most recently from 35b16c4 to a67e298 Compare August 17, 2026 10:49
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vursen
vursen force-pushed the feat/combo-box-auto-select-mode branch from a67e298 to 9890206 Compare August 17, 2026 10:51
The example lives in the PR description instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vursen
vursen marked this pull request as ready for review August 17, 2026 10:59

@vaadin-review-bot vaadin-review-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reviewed the changes — left 5 comments.

Finding
⚠️ In multi-select-combo-box, pressing Enter on an auto-focused partial match that is already selected removes it instead of adding a selection.
⚠️ With first-match, Escape needs two presses to close the dropdown after typing a partial filter.
🧹 New multi-select test duplicates shared mixin behavior instead of testing it once in the mixin's package.
👀 Shared property docstring says the partial match is committed on blur, which is false for multi-select-combo-box.
🧹 !items || items.length === 0 can be written as !items?.length.

Comment thread packages/combo-box/src/vaadin-combo-box-items-mixin.js
Comment thread packages/combo-box/src/vaadin-combo-box-items-mixin.js Outdated
Comment thread packages/combo-box/src/vaadin-combo-box-items-mixin.js Outdated
vursen added a commit to vaadin/flow-components that referenced this pull request Aug 17, 2026
Mirrors the autoFocusPartialMatch property added to the web
components in vaadin/web-components#12438.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vursen vursen changed the title feat: add autoFocusPartialMatch to combo-boxes feat: add autoFocusPartialMatch to combo boxes Aug 17, 2026

@web-padawan web-padawan 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.

Question: how should this work together with autoOpenDisabled? When typing partial value and pressing Enter while overlay is closed, first-match commits it. This is probably acceptable, but IMO it should be documented - currently, JSDoc only mentions an item highlighter in the dropdown while typing.

Comment thread packages/combo-box/test/auto-focus-partial-match.test.js
Comment thread packages/combo-box/test/auto-focus-partial-match.test.js
Comment thread packages/combo-box/test/auto-focus-partial-match.test.js Outdated
@web-padawan

Copy link
Copy Markdown
Member

🤖 AI-generated

Adversarial Review: feat: add autoFocusPartialMatch to combo boxes

The mechanism is minimal and the default path is provably unchanged, which is the right shape for this feature. My concern is that the autoFocusPartialMatch name frames this as a focus/highlight change, while the observable effect is that a value the user never typed gets committed — and two of those commit consequences reach further than the existing thread covers. Excluding everything already raised by @vaadin-review-bot and @web-padawan, three new findings below.


🟠 Medium

Enter submits the surrounding form when combined with autoOpenDisabled

Building on @web-padawan's autoOpenDisabled question — there is a second consequence beyond the missing documentation. In _onEnter (vaadin-combo-box-base-mixin.js:533), _hasValidInputValue() now returns true for a partial filter because _focusedIndex > -1, so the early preventDefault() / stopPropagation() branch is skipped. With autoOpenDisabled the overlay is closed, so the if (this.opened) branch that would also preventDefault() is skipped too. Enter therefore commits and propagates.

Verified inside a <form> with auto-open-disabled, items ['apple','banana','grapefruit','grape'], typing gra:

  • none → no submit event (baseline)
  • first-matchsubmit fires, and value is grapefruit

The "valid value → Enter submits" contract is pre-existing (an exact-match filter behaves this way today), so this is a widening rather than a new bug. But the motivating use case in #1280 is users filling long forms: one keystroke now replaces their typed text with an item they never saw and submits the form. Worth deciding explicitly, not just documenting.

Typing a prefix of the already-selected item replaces the selection

With grape selected, typing gra and clicking outside yields grapefruit. Verified:

  • nonevalue stays grape (baseline: no exact match, so _commitValue reverts)
  • first-matchvalue becomes grapefruit

Exact-match precedence does not help here, since gra is not an exact match for anything. Distinct from the multi-select toggle issue already on the thread: different component, and the mechanism is silent replacement of an existing committed value rather than deselection. Typing a prefix of your own current selection to re-open and browse the list is a normal thing to do, and it now changes the value. If this is accepted as inherent to first-match, it deserves a test pinning it so it is a decision rather than an accident.


🟡 Low / Nitpicks

No observer, so the property is stale while a filter is active

autoFocusPartialMatch has no observer and nothing recomputes _focusedIndex when it changes; only a filter or items change re-enters __getItemIndexByFilter. Verified in both directions with a filter already applied: switching nonefirst-match leaves _focusedIndex at -1, and first-matchnone leaves it at 0. Relevant if the Flow connector or an app sets the property after the user has typed. Every test sets it in beforeEach before typing, so this is unexercised.

Unknown values silently disable the feature

A typo such as autoFocusPartialMatch = 'firstmatch' falls through to return -1 and behaves as none, with no warning. CONVENTIONS.md calls for issueWarning on invalid configuration rather than silent fallback. Caveat: I did not find another enum-valued property in the repo that validates, so consistency arguably favors leaving it — raising it only because a silently-ignored typo on an opt-in feature is hard to debug.

Coverage asymmetry between the two modes

first-match has "should not highlight the first match when custom values are allowed"; only-match has no equivalent, even though it goes through the same allowCustomValue guard.


✅ What is done well

  • The default none path is behaviorally identical to the old code: exact match returns the same index, and every other branch returns -1 exactly where __getItemIndexByLabel did. Opt-in with a genuinely inert default.
  • Exact-match precedence sits above the mode check rather than inside it, so the pre-existing contract holds for all three values instead of being re-derived per mode.
  • The ComboBoxPlaceholder guard on items[0] is the correct call for lazy loading, and the comment explains why -1 is safe rather than restating the code. Both lazy cases are tested.
  • Excluding partial matches under allowCustomValue is the right precedence — the typed text is meaningful there and should not be overwritten.
  • aria-autocomplete="list" plus aria-activedescendant following the auto-focused item matches the APG "list autoselect" pattern, so the added highlight is announced correctly without any a11y changes needed.

Summary: Not mergeable as-is, but the blocker is already on the thread — of my new findings, the autoOpenDisabled form submission is the one worth resolving before this API ships.

vursen and others added 7 commits August 17, 2026 16:40
Committing on Enter toggles the focused item, and the existing guard
only covered typing the exact label. With autoFocusPartialMatch, an
already selected partial match got silently unselected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each mode is now tested against Enter, outside click, and item click.
Also replace the private _focusedIndex assertion with a commit-based
test per review feedback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Multi-select combo box never commits the focused item on blur or
outside click, so the shared docstring only mentions Enter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With autoOpenDisabled, typing a partial value and pressing Enter
committed an item that was never shown. The partial match is now
focused only while the dropdown is open, and gets focused once the
dropdown is opened after typing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread packages/combo-box/src/vaadin-combo-box-items-mixin.js Outdated
Comment thread packages/combo-box/test/auto-focus-partial-match.test.js Outdated
Comment thread packages/combo-box/test/auto-focus-partial-match.test.js Outdated
Comment thread packages/multi-select-combo-box/src/vaadin-multi-select-combo-box-mixin.js Outdated
vursen and others added 5 commits August 18, 2026 09:59
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Test commit gestures extensively for the exact match and first-match
mode, keep only-match Enter-only, and drop item click tests since the
feature does not affect item click behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vursen
vursen force-pushed the feat/combo-box-auto-select-mode branch from 06e4d78 to 7572c9f Compare August 18, 2026 06:41
In Firefox, Tab does not move focus out of the input unless the page
has another focusable element, so the blur commit never ran.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread packages/combo-box/test/auto-focus-partial-match.test.js Outdated
@sonarqubecloud

Copy link
Copy Markdown

@vursen
vursen merged commit 86ecb3f into main Aug 18, 2026
11 checks passed
@vursen
vursen deleted the feat/combo-box-auto-select-mode branch August 18, 2026 11:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants