You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Combobox-style components keep focus on the trigger and represent the highlighted option with aria-activedescendant. In single-select mode, Tab or Shift+Tab should select that option before moving focus. In multiselect mode, Tab should only close the popup.
The existing React onKeyDown path works during ordinary browser navigation but can be skipped when Tabster manages focus. Tabster handles keydown during window capture and may stop propagation before React receives it. TagPicker also had no real single-select mode: controlled state could contain one item, but its internal trigger, listbox, option, and dismissal semantics remained multiselect.
Selection on blur: cannot distinguish Tab from pointer or programmatic focus changes and produced false selections.
Capture listeners: React capture is still later than Tabster window capture; native listeners introduce ordering and active-option lifetime problems.
Selection inside setOpen: incorrectly couples every close operation with selection.
Casting the native Tabster event: violates the existing React event callback contract.
Adding onOptionSelect2: type-safe but introduces a versioned public callback, deprecation, dual plumbing, and future cleanup for a private integration issue.
Solution
The implementation uses two complementary paths:
Ordinary Tab navigation continues through React onKeyDown.
Tabster-managed navigation is observed through tabster:movefocus. Its original native KeyboardEvent is wrapped by a private React-compatible adapter and passed through the existing selection path. Tabster still performs focus movement.
The adapter preserves the callback behavior consumers can observe: nativeEvent, callback-scoped currentTarget, default prevention, propagation state, and persist(). It remains private and contract-tested. Combobox and TagPicker keep separate private implementations so no package-internal helper is exported.
This preserves the existing onOptionSelect API while covering both managed and ordinary focus topologies.
TagPicker
TagPicker now supports:
<TagPickerselectionMode="single">...</TagPicker>
selectionMode is optional and defaults to multiselect. It drives selection state, input/button triggers, listbox and option semantics, and tag dismissal. Single mode replaces the current option, keeps input text independent from the selected tag, and clears selection when the tag is dismissed.
selectionMode also remains optional on public state/context types to avoid breaking custom state or context objects; internal context construction defaults it to multiselect.
Validation
Combobox unit tests: 258 passing
TagPicker unit tests: 181 passing
Combobox/Dropdown Cypress: 16 passing
TagPicker Cypress: 41 passing
lint, type-check, aggregate type-check, API Extractor, and beachball validation
Keep multiselect clear behavior unchanged and centralize tag dismissal through the existing selectOption context action for styled and headless TagPicker groups.
Move Tabster movefocus handling into a private hook module consumed by useTriggerSlot, remove the package-level export, and eliminate wrapper-specific ref wiring.
The reason will be displayed to describe this comment to others. Learn more.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Fixes Tab/Shift+Tab selection behavior for combobox-style components (Dropdown/Combobox/TagPicker) when Tabster intercepts keydown events, and introduces a first-class single-select mode for TagPicker.
Changes:
Add Tabster tabster:movefocus integration to select the active option when tabbing away in single-select Combobox/Dropdown/TagPicker.
Introduce selectionMode="single" | "multiselect" for TagPicker, updating context/state semantics and stories.
Add supporting utilities (handledTabKeyEvents, createReactKeyboardEvent) and expand Cypress + unit test coverage.
multiselect:false enabled useInputTriggerSlot's Combobox-only clear-on-edit path, so typing an unmatched query or erasing it cleared the selected tag. The input slot now gets a no-op clearSelection; tag dismissal and the clear button are unaffected.
The adapter omits charCode, keyCode, and which, which are part of the existing React.KeyboardEvent contract. An onOptionSelect handler checking keyCode === 9 or which === 9 therefore receives undefined only on the Tabster path. Populate React-equivalent keydown values and add assertions for these fields to the adapter contract test.
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
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.
Problem
Combobox-style components keep focus on the trigger and represent the highlighted option with
aria-activedescendant. In single-select mode, Tab or Shift+Tab should select that option before moving focus. In multiselect mode, Tab should only close the popup.The existing React
onKeyDownpath works during ordinary browser navigation but can be skipped when Tabster manages focus. Tabster handles keydown during window capture and may stop propagation before React receives it. TagPicker also had no real single-select mode: controlled state could contain one item, but its internal trigger, listbox, option, and dismissal semantics remained multiselect.Fixes #31365.
Approaches considered
ignoreKeydown: restores React keydown by disabling Tabster focus handling, breaking focus-navigation edge cases.setOpen: incorrectly couples every close operation with selection.onOptionSelect2: type-safe but introduces a versioned public callback, deprecation, dual plumbing, and future cleanup for a private integration issue.Solution
The implementation uses two complementary paths:
onKeyDown.tabster:movefocus. Its original nativeKeyboardEventis wrapped by a private React-compatible adapter and passed through the existing selection path. Tabster still performs focus movement.The adapter preserves the callback behavior consumers can observe:
nativeEvent, callback-scopedcurrentTarget, default prevention, propagation state, andpersist(). It remains private and contract-tested. Combobox and TagPicker keep separate private implementations so no package-internal helper is exported.This preserves the existing
onOptionSelectAPI while covering both managed and ordinary focus topologies.TagPicker
TagPicker now supports:
selectionModeis optional and defaults tomultiselect. It drives selection state, input/button triggers, listbox and option semantics, and tag dismissal. Single mode replaces the current option, keeps input text independent from the selected tag, and clears selection when the tag is dismissed.selectionModealso remains optional on public state/context types to avoid breaking custom state or context objects; internal context construction defaults it tomultiselect.Validation