feat!: decouple selection from DOM focus in TreeView and GridView#512
Merged
Conversation
Move focus and blur calls out of the selected setter in TreeViewItem and GridViewItem so that selection is pure state with no DOM focus side effects. Focus is now handled explicitly by the interaction handlers (keyboard arrow keys) that need it, while click interactions rely on the browser native focus behavior. This follows the convention used by React Aria, Angular CDK and other major component libraries where selection state and DOM focus are independent concerns. It eliminates focus-stealing when selection is changed programmatically (e.g. syncing a tree panel with a grid panel). BREAKING CHANGE: Setting selected on TreeViewItem or GridViewItem no longer moves DOM focus. Consumers that relied on this implicit behavior must call item.focus() explicitly after setting selection. Made-with: Cursor
Verify that setting selected programmatically does not move DOM focus, and that arrow key navigation still moves both selection and focus. Made-with: Cursor
slimbuck
approved these changes
Feb 28, 2026
Contributor
There was a problem hiding this comment.
Pull request overview
This PR decouples DOM focus management from selection state in TreeViewItem and GridViewItem. Previously, setting item.selected = true would implicitly call focus(), causing focus-stealing in multi-panel layouts. Now focus is only moved by explicit user interactions (keyboard navigation).
Changes:
- Remove implicit
focus()/blur()calls from theselectedsetters inTreeViewItemandGridViewItem - Add explicit
focus()calls to each arrow-key navigation branch inTreeView._onChildKeyDownandGridView._onKeyDown - Add new test files/cases verifying the new behavior
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/components/TreeViewItem/index.ts |
Removes focus() on select and blur() on deselect from the selected setter |
src/components/GridViewItem/index.ts |
Removes focus() call at the top of the selected setter; also removes two inline comments |
src/components/TreeView/index.ts |
Adds parent.focus(), firstChild.focus(), next.focus(), and prev.focus() after each selection change in the keyboard handler |
src/components/GridView/index.ts |
Adds target.focus() after _selectSingleItem in the keyboard handler |
test/components/treeview.mjs |
Adds two new tests: no-focus-steal on programmatic selection, and focus-moves on ArrowDown navigation |
test/components/gridview.mjs |
New test file with two tests: no-focus-steal on programmatic selection, and focus-moves on ArrowRight navigation |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
TreeView: add ArrowUp, ArrowRight (into child), ArrowLeft (to parent), and ArrowLeft collapse (stays focused without moving selection). GridView: add ArrowLeft navigation. Made-with: Cursor
Made-with: Cursor
…ent in test Made-with: Cursor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
TreeViewItemandGridViewItem. Settingselectedno longer callsfocus()orblur()as a side effect.focus()calls in keyboard navigation handlers forTreeViewandGridViewso arrow key navigation still moves focus correctly.tabIndex: 0).Motivation
The
selectedsetter previously calledfocus()on select andblur()on deselect. This caused focus-stealing in multi-panel layouts (e.g. a Windows Explorer-style UI with a TreeView and GridView side by side). When one panel programmatically updated selection on the other, DOM focus would jump unexpectedly.This follows the convention used by React Aria, Angular CDK, and other major component libraries where selection is state and focus is an interaction concern. Only user interactions (click, keyboard) should move DOM focus.
Changes
TreeViewItem/index.tsfocus()/blur()fromselectedsetterGridViewItem/index.tsfocus()fromselectedsetterTreeView/index.tsfocus()after selection in_onChildKeyDown(ArrowLeft, ArrowRight, ArrowUp, ArrowDown)GridView/index.tsfocus()after selection in_onKeyDownNet: -16 lines removed, +6 lines added.
Breaking Change
Setting
selectedonTreeViewItemorGridViewItemno longer moves DOM focus. Consumers that relied on this implicit behavior must callitem.focus()explicitly after setting selection.Test Plan
npm run lintpassesnpm testpasses (29/29)item.selected = trueupdates visual state without stealing focus