Skip to content

feat: preserve focused list rows in DOM until they lose focus#302624

Draft
Copilot wants to merge 3 commits intomainfrom
copilot/prevent-deleting-focused-elements
Draft

feat: preserve focused list rows in DOM until they lose focus#302624
Copilot wants to merge 3 commits intomainfrom
copilot/prevent-deleting-focused-elements

Conversation

Copy link
Contributor

Copilot AI commented Mar 18, 2026

When a ListView splices out an item whose DOM subtree contains the active element, the row is immediately removed — abruptly stealing focus from the user.

Changes

  • rowCache.ts: In releaseRow, use isAncestorOfActiveElement(domNode) (handles multi-window and shadow DOM scenarios) to detect whether the row holds focus before removal. If true, a trackFocus(domNode) / IFocusTracker is created and its onDidBlur event defers both the DOM removal and cache re-entry until focus truly leaves the row. Deferred rows are tracked in pendingFocusedRows: Map<IRow, IDisposable>. Using IFocusTracker is the idiomatic VS Code approach and its internal setTimeout(0) debounce ensures onDidBlur fires only when focus has genuinely moved away, not when it shifts between child elements within the same row.

    • releaseFocusedRow — disposes the tracker, removes the node, and returns the row to the reuse cache.
    • dispose — cleans up any rows still pending release (calls disposeTemplate, removes DOM node).
  • listView.test.ts: Two new async tests:

    1. Focused row stays in DOM after splice, is removed after blur() + await timeout(0) (accounting for FocusTracker's async onDidBlur).
    2. Template count reaches zero when the list is disposed while a row still has focus.
Original prompt

I noticed the listWidget will delete elements even if they have focus. I think what would be nice is if focus is current on a child of an element inside of the list widget, then that item can be saved from being deleted/disposed/destroyed until it loses focus. Can you play around with this?


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

…osal

When a list item is spliced (removed) from a ListView/List widget while
one of its child elements has DOM focus, the row's DOM node is now kept
in the document until focus moves away. This prevents an abrupt focus
loss and preserves the focused element's interactive state until the
user naturally moves focus elsewhere.

- rowCache.ts: Added `pendingFocusedRows` map; `releaseRow` now checks
  if the row's domNode contains `document.activeElement` and defers the
  DOM removal + cache re-entry by registering a `focusout` listener.
  `releaseFocusedRow` completes the release when focus leaves.
  `dispose()` handles any rows still pending release.
- listView.test.ts: Added two tests covering the focus-protection
  behavior and correct template disposal on list disposal while a
  row has focus.

Co-authored-by: TylerLeonhardt <2644648+TylerLeonhardt@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix listWidget deleting focused elements feat: preserve focused list rows in DOM until they lose focus Mar 18, 2026
Copilot AI requested a review from TylerLeonhardt March 18, 2026 01:27
Comment on lines +92 to +93
const ownerDocument = domNode.ownerDocument;
if (ownerDocument && domNode.contains(ownerDocument.activeElement)) {
Copy link
Member

Choose a reason for hiding this comment

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

@copilot should we use isAncestorOfActiveElement?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, switched to isAncestorOfActiveElement(domNode) in d753d06. It correctly handles multi-window and shadow DOM scenarios.

const ownerDocument = domNode.ownerDocument;
if (ownerDocument && domNode.contains(ownerDocument.activeElement)) {
if (!this.pendingFocusedRows.has(row)) {
const listener = addDisposableListener(domNode, 'focusout', (e: FocusEvent) => {
Copy link
Member

Choose a reason for hiding this comment

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

Should we use trackFocus using an IFocusTracker? is there something like that already tracking focus of lists?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in d753d06 — now using trackFocus(domNode) with onDidBlur instead of a raw focusout listener. FocusTracker debounces the blur event via setTimeout(0), so it fires only when focus has truly left the row rather than when it moves between child elements. Also updated the test to await timeout(0) to account for the async fire.

- Replace manual ownerDocument.activeElement check with
  isAncestorOfActiveElement(), which correctly handles multi-window
  focus and shadow DOM scenarios.
- Replace raw focusout listener with trackFocus()/IFocusTracker, the
  idiomatic VS Code approach. FocusTracker debounces onDidBlur via
  setTimeout(0) so it fires only when focus has truly left the row,
  not when it moves between child elements.
- Update the test to await timeout(0) after blur() because onDidBlur
  fires asynchronously.

Co-authored-by: TylerLeonhardt <2644648+TylerLeonhardt@users.noreply.github.com>
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.

2 participants