Skip to content

feat: implement roving tabindex for TreeView and GridView#513

Merged
willeastcott merged 7 commits into
mainfrom
roving-tabindex
Mar 1, 2026
Merged

feat: implement roving tabindex for TreeView and GridView#513
willeastcott merged 7 commits into
mainfrom
roving-tabindex

Conversation

@willeastcott

@willeastcott willeastcott commented Mar 1, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Implements the WAI-ARIA roving tabindex pattern for TreeView and GridView so each widget acts as a single Tab stop with internal arrow key navigation
  • On keyboard Tab-in from outside, the focused item is automatically selected (detected via :focus-visible) and the active item tracks selection so Tab-back always returns to the last selected item
  • Adds :focus-visible CSS rules that mirror the selected style, providing a visual focus indicator for keyboard users
  • Removes Tab from the set of keys that TreeView intercepts, allowing native Tab behavior to move focus out of the widget
  • Adds a tabindex.html example page for manual testing

Test plan

  • Open the tabindex.html example and verify Tab moves into the TreeView (first item highlights and is selected), arrow keys navigate within it, and Tab moves out to the GridView
  • Verify the same roving behavior works in the GridView (Tab-in selects, arrows navigate, Tab exits)
  • Verify mouse clicks in both TreeView and GridView still work normally (click to select, click another to change selection)
  • Verify the examples browser TreeView on the left still works (click an example, it stays selected and loads)
  • Run npm test and confirm all tests pass

@willeastcott willeastcott requested a review from Copilot March 1, 2026 09:28
@willeastcott willeastcott self-assigned this Mar 1, 2026
@willeastcott willeastcott added the enhancement New feature or request label Mar 1, 2026
@willeastcott willeastcott force-pushed the roving-tabindex branch 2 times, most recently from 295583d to a2f0468 Compare March 1, 2026 09:32

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

Implements roving tabindex behavior for TreeView and GridView so each widget becomes a single Tab stop with internal arrow-key navigation, and adds an example page for manual verification.

Changes:

  • Add active-item tracking and focus-in logic to support roving tabindex and “Tab-in selects active item” behavior in TreeView and GridView.
  • Update selected styling to also apply on :focus-visible for clearer keyboard focus indication.
  • Add a new tabindex.html example and link it from the example browser.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/components/TreeView/style.scss Apply selected styling on :focus-visible for focused TreeView rows.
src/components/TreeView/index.ts Add _activeItem roving-tabindex state, focus-in handling, and adjust key handling (stop intercepting Tab).
src/components/GridViewItem/style.scss Apply selected styling on :focus-visible for GridView items.
src/components/GridView/index.ts Add _activeItem roving-tabindex state and focus-in handling; update active item on selection/removal.
examples/index.html Add “TabIndex” entry to examples list.
examples/elements/tabindex.html New demo page for manual testing of roving tabindex behavior.
Comments suppressed due to low confidence (1)

src/components/GridView/index.ts:162

  • _onAppendGridViewItem sets the active (tabbable) item before applying filterFn. If the first appended item gets hidden by the filter, it can remain the active item with tabIndex=0, leaving the GridView with no visible tabbable item under the new roving-tabindex behavior. Consider applying filtering first and/or re-homing _activeItem to the next visible item whenever the active item becomes hidden.
        if (!this._activeItem) {
            this._setActiveItem(item);
        } else {
            item.dom.tabIndex = -1;
        }

        let evtClick: EventHandle;
        if (this._clickFn) {
            evtClick = item.on('click', evt => this._clickFn(evt, item));
        } else {
            evtClick = item.on('click', evt => this._onClickItem(evt, item));
        }
        let evtSelect = item.on('select', () => this._onSelectItem(item));

        let evtDeselect: EventHandle;
        if (this._allowDeselect) {
            evtDeselect = item.on('deselect', () => this._onDeselectItem(item));
        }

        if (this._filterFn && !this._filterFn(item)) {
            item.hidden = true;
        }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/components/GridViewItem/style.scss Outdated
Comment thread src/components/TreeView/index.ts
Comment thread src/components/TreeView/index.ts Outdated
Comment thread src/components/TreeView/index.ts
Comment thread src/components/GridView/index.ts Outdated
Each widget now acts as a single Tab stop with internal arrow key navigation (WAI-ARIA roving tabindex pattern). On keyboard Tab-in, the focused item is auto-selected using :focus-visible detection. A :focus-visible CSS rule mirrors the selected style so the active item is always visually apparent. Adds a tabindex.html example for manual testing.

Made-with: Cursor
willeastcott and others added 5 commits March 1, 2026 09:39
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Covers single tabIndex=0 invariant, active item tracking selection, active item reassignment on removal, and focus-in-from-outside auto-selection. Also fixes GridView _onRemoveGridViewItem to find the next active item from remaining DOM children rather than the detached item's siblings.

Made-with: Cursor
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Matches the TreeView fix: only select the focused item if it is not already selected, avoiding collapse of an existing multi-selection when tabbing back into the widget.

Made-with: Cursor
During filtering, non-filter-result items are now skipped. Outside filtering, explicitly hidden items are skipped. Uses item.hidden instead of offsetParent for jsdom compatibility.

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

Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/components/GridView/index.ts
Comment thread src/components/GridView/index.ts
Comment thread src/components/TreeView/index.ts
Comment thread test/components/treeview.mjs Outdated
Comment thread src/components/GridViewItem/style.scss
Comment thread src/components/GridView/index.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@willeastcott willeastcott merged commit 0469e2d into main Mar 1, 2026
5 checks passed
@willeastcott willeastcott deleted the roving-tabindex branch March 1, 2026 10:28
willeastcott added a commit that referenced this pull request Mar 4, 2026
…and GridView

The roving tabindex PR (#513) added a :focus-visible CSS rule that shared the same background as selected items. When focus remained on a deselected item (e.g. after clicking a non-focusable element), the item appeared selected. Also simplifies _onFocusIn to only track the active item for roving tabindex without auto-selecting.

Made-with: Cursor
willeastcott added a commit that referenced this pull request Mar 4, 2026
…and GridView

The roving tabindex PR (#513) added a :focus-visible CSS rule that shared the same background as selected items. When focus remained on a deselected item (e.g. after clicking a non-focusable element), the item appeared selected. Also simplifies _onFocusIn to only track the active item for roving tabindex without auto-selecting.

Made-with: Cursor
willeastcott added a commit that referenced this pull request Mar 4, 2026
…and GridView (#518)

The roving tabindex PR (#513) added a :focus-visible CSS rule that shared the same background as selected items. When focus remained on a deselected item (e.g. after clicking a non-focusable element), the item appeared selected. Also simplifies _onFocusIn to only track the active item for roving tabindex without auto-selecting.

Made-with: Cursor
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.

2 participants