Skip to content

feat(core): added SelectionController#6487

Closed
nikkimk wants to merge 17 commits into
mainfrom
nikkimk/simple-selection-controller
Closed

feat(core): added SelectionController#6487
nikkimk wants to merge 17 commits into
mainfrom
nikkimk/simple-selection-controller

Conversation

@nikkimk

@nikkimk nikkimk commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Description

  • Proof-of-concept for a simple controller to handle single-selection for components that do not handle their own click selection.
  • Documentation with demos and code snippets for implementing the controller.
  • Refactor of 2nd-gen swc-tabs to leverage selection controller.

Motivation and context

This controller could handle selection for radio groups, and segmented controls.

Related issue(s)

Screenshots (if appropriate)


Author's checklist

  • I have read the CONTRIBUTING and PULL_REQUESTS documents.
  • I have reviewed at the Accessibility Practices for this feature, see: Aria Practices
  • I have added automated tests to cover my changes.
  • I have included a well-written changeset if my change needs to be published.
  • I have included updated documentation if my change required it.

Reviewer's checklist

  • Includes a Github Issue with appropriate flag or Jira ticket number without a link
  • Includes thoughtfully written changeset if changes suggested include patch, minor, or major features
  • Automated tests cover all use cases and follow best practices for writing
  • Validated on all supported browsers
  • All VRTs are approved before the author can update Golden Hash

Manual review test cases

  • Selection controller

    1. Go to Selection controller
    2. Verify that the examples work.
    3. Verify that the documentation is sufficient and the examples work as they should.
    4. Verify that automated tests pass.
    5. Verify the examples pass manual keyboard and screenreader testing.
  • Tabs

    1. Go to Tabs documentation
    2. Verify that the examples work.
    3. Verify that the public API has not changed.
    4. Verify no visual regressions.
    5. Verify that automated tests pass.
    6. Verify the examples pass manual keyboard and screenreader testing.
  • Is the creation and implementation of this controller useful for tabs, accordions, radio groups, swatch groups with selectable swatches, button groups that function as radio groups, menus, and pickers? Or is it easier just to let each custom element handle its own selection management?

Device review

  • Did it pass in Desktop?
  • Did it pass in (emulated) Mobile?
  • Did it pass in (emulated) iPad?

@changeset-bot

changeset-bot Bot commented Jul 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b2f34c7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@adobe/spectrum-wc Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@nikkimk nikkimk self-assigned this Jul 8, 2026
@coveralls

coveralls commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Coverage Report for CI Build 29260633738

Warning

No base build found for commit 6d2a275 on main.
Coverage changes can't be calculated without a base build.
If a base build is processing, this comment will update automatically when it completes.

Coverage: 96.254%

Details

  • Patch coverage: No coverable lines changed in this PR.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

Requires a base build to compare against. How to fix this →


Coverage Stats

Coverage Status
Relevant Lines: 39169
Covered Lines: 37903
Line Coverage: 96.77%
Relevant Branches: 6457
Covered Branches: 6014
Branch Coverage: 93.14%
Branches in Coverage %: Yes
Coverage Strength: 461.22 hits per line

💛 - Coveralls

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

📚 Branch Preview Links

🔍 Gen1 Visual Regression Test Results

When a visual regression test fails (or has previously failed while working on this branch), its results can be found in the following URLs:

Deployed to Azure Blob Storage: pr-6487

If the changes are expected, update the current_golden_images_cache hash in the circleci config to accept the new images. Instructions are included in that file.
If the changes are unexpected, you can investigate the cause of the differences and update the code accordingly.

nikkimk and others added 3 commits July 9, 2026 15:28
…ate branch

Split off to nikkimk/tabs-focusgroup-refactor: this branch keeps only
SelectionController and its Tabs click/Enter-Space/selected-property
integration. Tabs' arrow-key/Home/End navigation reverts to the
hand-rolled implementation, and FocusgroupNavigationController's
`reason` field addition (needed for that refactor, not for this one)
reverts along with it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@nikkimk nikkimk marked this pull request as ready for review July 10, 2026 15:06
@nikkimk nikkimk requested a review from a team as a code owner July 10, 2026 15:06
@nikkimk nikkimk added Status:Ready for review PR ready for review or re-review. High priority PR review PR is a high priority and should be reviewed ASAP labels Jul 10, 2026

@Rajdeepc Rajdeepc 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.

Nice work! Before merging, I would love to align on scope coz this is a fairly general abstraction built for one consumer (Tabs), since accordion/menu/picker are explicitly deferred to LiveSelectionController.
Also we are not validating against a second real use case yet.
My recommendation on this PR would be to ship a smaller v1 scoped to what Tabs needs (single mode + keydownActivation + confirmSelectionChange), and add multiple/single-toggle/selectAll/isSelectable back in once a second consumer actually needs them.

* @see https://www.w3.org/WAI/ARIA/apg/patterns/listbox/
* @see https://www.w3.org/WAI/ARIA/apg/patterns/radio/
*/
export class SelectionController implements ReactiveController {

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.

Given Tabs is the only consumer today, what do you think about trimming this to a smaller first version and adding multiple, single-toggle, selectAll/clearAll, and isSelectable later, once there's a real second consumer to design against?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good call on multiple/selectAll. I split those out to a separate branch (nikkimk/selection-controller-multiple-mode), so this PR is now single-mode + single-toggle only, no selectAll/clearAll-for-multiple. I fixed PR description to match the components that this controller is for now that we've separated out the controller into two separate controllers.

On single-toggle and isSelectable specifically: I'd like to keep those in this PR. single-toggle is a real, distinct interaction (optional single choice vs. mandatory), not a speculative extra, and we will need it soon as we do more radio selectable items. isSelectable is a zero-cost override (unused by Tabs, no code path exercised unless a consumer supplies it).

Comment on lines +768 to +772
private getScopedRawItems(): HTMLElement[] {
return this.options
.getItems()
.filter((el) => this.isNodeWithinHostScope(el));
}

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.

This still walks the DOM (isNodeWithinHostScope) for every item on every click/keydown and that's the O(n) cost the comment above says we're avoiding for large lists (the picker/virtualized case mentioned in the isSelectable doc).

* hand-rolled in this class (see `focusByDelta` / `updateRovingTabindex`);
* this controller only owns click and Enter/Space activation.
*/
private readonly _selection = new SelectionController(this, {

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.

This gives us a second state container (_selection's internal set) alongside this.selected, synced in both directions (_syncSelectionController down, onSelectionChange up), plus the selectTarget escape hatch for markup-seeded tabs. Compared to the updateCheckedState this replaces, I'm not sure Tabs itself gets simpler. Is this payoff for future customers apart from Tabs? Is that what you are thinking here?

private readonly _selection = new SelectionController(this, {
getItems: () => this._tabs as HTMLElement[],
selectItem: (item) => {
(item as TabLike).selected = true;

@Rajdeepc Rajdeepc Jul 13, 2026

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.

Please help me understand here. SelectionControllers model assumes items never change their own selected state independently but here TabLike.selected is still a public property and it can be overriden externally by tab.selected and this will desync the controller's cache.
Would you be okay to write a test for this?

@@ -0,0 +1,9 @@
---
'@adobe/spectrum-wc': patch

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.

I would point this to minor. :)

nikkimk and others added 2 commits July 13, 2026 10:35
…branch

Split off to nikkimk/selection-controller-multiple-mode: this branch
keeps only `single` and `single-toggle` modes. `multiple` mode touched
the controller's core transition logic (not just a bolt-on demo), so
removing it also removes the mode-switch normalization in setOptions()
(it only ever mattered for the multiple-to-single-item transition,
which can no longer happen) and selectAll(), which was multiple-only.

- src/selection-controller.ts: SelectionMode narrows to 'single' |
  'single-toggle'; removed selectAll() and the multiple-mode branches
  in setSelectedItem()/toggleItem()/applyClickOrKey(); removed the now
  provably-dead setOptions() mode-switch collapse (selectedItems can
  never exceed one item without multiple mode).
- stories/demo-hosts.ts: removed the DemoSelectionFilterTags (multiple
  mode) demo; DemoSelectionModeSwitcher now switches only between
  single and single-toggle; DemoSelectionEligibility moved from
  multiple to single-toggle mode (unrelated to what it tests -
  eligibility exclusion - but needed a mode that still exists).
- stories.ts / test.ts: removed the MultipleMode story/test; rewrote
  ModeSwitchingTest to demonstrate the single vs single-toggle
  behavioral difference instead of the removed multi-select collapse.
- selection-controller.mdx: removed the Multiple mode section and all
  now-inaccurate mentions of multiple/selectAll in the API reference.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Bump changeset from patch to minor.
- Fix the O(n) host-scope walk on the click/keydown hot path:
  handleClickCapture/handleKeyDownCapture now search the consumer's raw,
  unfiltered item list for the composedPath hit, then defer the
  isNodeWithinHostScope check to that single resolved item, instead of
  scope-filtering every item on every click/keydown regardless of
  whether it hits a participant. Matches the O(1)-on-the-hit intent the
  neighboring isSelectableItem comment already documented.
- Add a Tabs test (ExternalTabSelectedMutationTest) documenting that a
  tab's own `selected` property is still publicly settable, and that
  setting it directly bypasses `_selection` and desyncs from
  `tabs.selected` until the next resync (any `_syncSelectionController`
  trigger forces every scoped item back in line via a full mutator
  re-scan, not just the item that changed).

`multiple` mode (the other half of the scope-reduction ask) was
already split into nikkimk/selection-controller-multiple-mode before
this PR was opened.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@nikkimk

nikkimk commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Since we have the live selection controller and a use case for it, I'm going to pause work on this one until we have a use case that doesn't require live selection.

@nikkimk nikkimk closed this Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

High priority PR review PR is a high priority and should be reviewed ASAP Status:Ready for review PR ready for review or re-review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants