Skip to content

feat!: decouple selection from DOM focus in TreeView and GridView#512

Merged
willeastcott merged 5 commits into
mainfrom
decouple-selection-focus
Feb 28, 2026
Merged

feat!: decouple selection from DOM focus in TreeView and GridView#512
willeastcott merged 5 commits into
mainfrom
decouple-selection-focus

Conversation

@willeastcott

@willeastcott willeastcott commented Feb 28, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Decouple selection state from DOM focus in TreeViewItem and GridViewItem. Setting selected no longer calls focus() or blur() as a side effect.
  • Add explicit focus() calls in keyboard navigation handlers for TreeView and GridView so arrow key navigation still moves focus correctly.
  • Click interactions continue to work because the browser natively focuses the clicked element (tabIndex: 0).

Motivation

The selected setter previously called focus() on select and blur() 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

File Change
TreeViewItem/index.ts Remove focus()/blur() from selected setter
GridViewItem/index.ts Remove focus() from selected setter
TreeView/index.ts Add focus() after selection in _onChildKeyDown (ArrowLeft, ArrowRight, ArrowUp, ArrowDown)
GridView/index.ts Add focus() after selection in _onKeyDown

Net: -16 lines removed, +6 lines added.

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.

Test Plan

  • npm run lint passes
  • npm test passes (29/29)
  • Manual: TreeView arrow key navigation still moves focus and selection together
  • Manual: GridView arrow key navigation still moves focus and selection together
  • Manual: Clicking a TreeViewItem/GridViewItem still focuses and selects it
  • Manual: Programmatically setting item.selected = true updates visual state without stealing focus

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 the selected setters in TreeViewItem and GridViewItem
  • Add explicit focus() calls to each arrow-key navigation branch in TreeView._onChildKeyDown and GridView._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.

Comment thread test/components/treeview.mjs Outdated
Comment thread test/components/treeview.mjs Outdated
TreeView: add ArrowUp, ArrowRight (into child), ArrowLeft (to parent),
and ArrowLeft collapse (stays focused without moving selection).
GridView: add ArrowLeft navigation.

Made-with: Cursor
@willeastcott willeastcott merged commit b68339d into main Feb 28, 2026
5 checks passed
@willeastcott willeastcott deleted the decouple-selection-focus branch February 28, 2026 20:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants