Skip to content

fix: preserve focused index across sibling page-loads in combo-box#11815

Merged
DiegoCardoso merged 7 commits into
mainfrom
fix/combo-box-preserve-focused-index-after-page-load
May 28, 2026
Merged

fix: preserve focused index across sibling page-loads in combo-box#11815
DiegoCardoso merged 7 commits into
mainfrom
fix/combo-box-preserve-focused-index-after-page-load

Conversation

@DiegoCardoso

@DiegoCardoso DiegoCardoso commented May 25, 2026

Copy link
Copy Markdown
Contributor

Why

scrollToIndex(N) can land _focusedIndex on an unloaded (ComboBoxPlaceholder) row in data-provider mode. If the items array is then re-pushed — for example by clearCache() or a mid-scroll Flow-connector update — and the slot at _focusedIndex is still a placeholder in the new array, _setDropdownItems reruns its value-based focus lookup:

const focusedItemIndex = this.__getItemIndexByValue(newItems, this._getItemValue(focusedItem));

_getItemValue called on a placeholder returns its stringified form, which doesn't match any real item, so __getItemIndexByValue returns -1 and _focusedIndex collapses. The user loses their keyboard position and Arrow Down advances from the wrong row. MultiSelectComboBox has the same lookup in __setDropdownItems and the same bug.

Reproduction

  1. A ComboBox with a paged DataProvider.
  2. Open the dropdown — only the first page is loaded.
  3. Press while the input is empty so focus wraps to the last index, which is still a placeholder.
  4. Trigger a re-push that keeps that slot a placeholder (clearCache() is the simplest path; the Flow connector's mid-scroll updates do the same).
  5. Pre-fix: _focusedIndex becomes -1. The next Arrow Down advances from row 0.

Changes

Short-circuit _setDropdownItems (and __setDropdownItems) when the entries at _focusedIndex in both the old and the new arrays are ComboBoxPlaceholder instances. That transition carries no information about where the focused row actually moved to, so preserve _focusedIndex and skip the value-lookup. Filter changes and items resets — where the new array has real items at that index — still run the fallback as before.

Related to #11851

Follow-up to #11552, Part of #6061

When a dataProvider returns object items without itemValuePath, calling
scrollToIndex(N) sets _focusedIndex=N correctly, but a subsequent sibling
page-load drops it. _setDropdownItems re-derives _focusedIndex via
__getItemIndexByValue(newItems, _getItemValue(focusedItem)); for objects
without a value path _getItemValue falls back to item.toString() which
collapses to '[object Object]', so the lookup matches the first object
in the new array and focus snaps to index 0. Keyboard users pressing
Arrow Down next jump from item 1 instead of N+1.

Short-circuit when the same item reference still occupies _focusedIndex
in the new array: __onDataProviderPageLoaded replaces only the array
reference, not the item references at unchanged indices, so this
matches the page-load mutation without affecting filter changes / item
resets where references differ and the value-lookup fallback still runs.

Apply the same change to MultiSelectComboBox's __setDropdownItems which
has the same logic and the same bug.
@DiegoCardoso DiegoCardoso requested review from vursen and web-padawan and removed request for web-padawan May 25, 2026 13:49
Comment thread packages/combo-box/src/vaadin-combo-box-mixin.js Outdated
Replace the ref-equality short-circuit added in the previous commit with
a two-strategy lookup. When `itemIdPath` is set and the focused item has
a resolvable id, search the new array by id and update `_focusedIndex`
to the new position — this also covers the case where an item moves to
a different index between data-provider updates (vursen's review
suggestion on #11815). When `itemIdPath` is unset or the focused entry
is a `ComboBoxPlaceholder` with no id, fall back to reference equality
at the same index so the original bug — focus collapsing to 0 after a
sibling page-load with placeholder refs — stays fixed.

Apply the same shape to `MultiSelectComboBox`'s `__setDropdownItems`.
vursen's follow-up review pointed out that the actual bug only fires
when `oldItems[focusedIndex]` and `newItems[focusedIndex]` are both
`ComboBoxPlaceholder` instances. The previous commit's two-strategy
block (`itemIdPath` search + reference equality) was over-engineered
for that. Replace both branches with a single explicit `instanceof`
check on the two slots.

Update the unit tests in both packages to reproduce the actual
placeholder-at-focusedIndex scenario (drain page 0, set focus to an
unloaded slot, re-push items with a fresh placeholder at the same
position) — the previous setup exercised a different toString-collapse
path that is no longer the scope of this PR.
Comment thread packages/combo-box/src/vaadin-combo-box-mixin.js Outdated
Comment thread packages/combo-box/src/vaadin-combo-box-mixin.js
DiegoCardoso and others added 4 commits May 26, 2026 11:24
…cusedItem

Trim the comment to describe just the scenario and intended outcome,
and reuse the existing `focusedItem` local for the first instanceof
check (per vursen's review on #11815).
Using `focusedItem` relied on `_getItemElements()` finding a rendered
row, which fails when the focused index is outside the virtualizer's
rendered range. Check `oldItems[_focusedIndex]` directly so the guard
holds regardless of scroll position.
Replace the direct `comboBox._focusedIndex = 200` write with an
`arrowUpKeyDown` keyboard event that wraps focus to the last index —
a placeholder slot when only page 0 is drained. Uses a real combo-box
API instead of touching the private property, with no behavior change
to `scrollToIndex`.

Same shape applied to the MSCB test.
@DiegoCardoso DiegoCardoso force-pushed the fix/combo-box-preserve-focused-index-after-page-load branch from 5884e39 to d8f173a Compare May 26, 2026 17:13
@sonarqubecloud

Copy link
Copy Markdown

DiegoCardoso added a commit that referenced this pull request May 28, 2026
…#11851)

The previous implementation special-cased placeholder targets by calling
`ensureFlatIndexLoaded` directly, and short-circuited the entire call
when `loading` was true. That meant two separate paths handled the "page
not loaded yet" case (the explicit branch and the natural
`index-requested` chain), and a scroll requested mid-loading was
deferred wholesale even when items already existed.

This reworks the method into a single uniform flow:

- Defer only when the dropdown isn't open or the items array is empty.
- Move the viewport and set the focused row in one path, regardless of
whether the target is a placeholder or a real item.
- Queue post-attempt when `loading` flips true (a page request was
triggered by the new viewport), so the call re-fires after the page
lands.

Setting `_focusedIndex` propagates to the scroller and drives viewport
rendering; placeholder rows in the new viewport fire `index-requested`
and load any missing pages through the data-provider chain — there's no
longer a need for `scrollToIndex` itself to drive page loading.

> [!NOTE]
> On placeholder→real transitions, the value-based focus lookup in
`_setDropdownItems` may briefly reset `_focusedIndex` before the
post-attempt re-fire restores it. Part of #11815 hardens
`_setDropdownItems` against this transition; that PR should land
alongside or before this one.

Related to #11815

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Sergey Vinogradov <mr.vursen@gmail.com>
@DiegoCardoso DiegoCardoso merged commit f60edaa into main May 28, 2026
10 checks passed
@DiegoCardoso DiegoCardoso deleted the fix/combo-box-preserve-focused-index-after-page-load branch May 28, 2026 08:21
@vaadin-bot

Copy link
Copy Markdown
Collaborator

This ticket/PR has been released with Vaadin 25.2.0-beta2.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants