Skip to content

[DREAM-738] Add dynamic selection counter to SelectPanel - #482

Open
myabc wants to merge 1 commit into
mainfrom
feature/DREAM-738-select-panel-dynamic-counter
Open

[DREAM-738] Add dynamic selection counter to SelectPanel#482
myabc wants to merge 1 commit into
mainfrom
feature/DREAM-738-select-panel-dynamic-counter

Conversation

@myabc

@myabc myabc commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

Work package: DREAM-738

What are you trying to accomplish?

Teaches Primer::Alpha::SelectPanel to show a self-updating count of selected items in its show button, with no custom JavaScript required downstream.

<% panel.with_show_button(counter: true) { "Assignees" } %>

Consumers (e.g. the OpenProject backlogs filter) previously had to hand-write a Stimulus controller just to keep a Counter in the show button in sync with the selection. That boilerplate now lives in the component.

  • Ruby: with_show_button gains counter: (Boolean) + counter_arguments: (Hash). When counter: true, the button auto-renders a trailing Counter wired to a Catalyst target, defaulting to hide_if_zero: true. The target is forced via merge_data so a caller's data: cannot unwire it. counter: on an icon button raises ArgumentError (non-production).
  • TypeScript: SelectPanelElement writes the live count (selectedItems.length, the tracked Map) into the counter on connect, on every item activation, and after fetch. Hides at zero.
  • The legacy dynamic_label text path is untouched and may coexist with the counter.

Integration

No production code changes required to adopt. Existing SelectPanel usages are unaffected; the counter is opt-in via counter: true. Downstream consumers can delete their bespoke counter-sync Stimulus controllers. static/*.json docs regenerate via CI on push.

List the issues that this change affects.

Closes DREAM-738

Risk Assessment

  • Low risk the change is small, highly observable, and easily rolled back.
  • Medium risk changes that are isolated, reduced in scope or could impact few users. The change will not impact library availability.
  • High risk changes are those that could impact customers and SLOs, low or no test coverage, low observability, or slow to rollback.

Additive, opt-in API in the alpha namespace. Legacy dynamic_label path untouched (regression-tested). Easily reverted.

What approach did you choose and why?

The counter is rendered server-side via Primer Button's native trailing_visual_counter slot and only augmented on the client — SelectPanelElement writes the count into a Catalyst target. This keeps with the library convention (server renders, client augments) rather than introducing client-side templating.

Count is read from the tracked selectedItems Map rather than a DOM query, so it stays correct in remote/filtered panels where selected items aren't currently in the DOM.

Alternatives discarded: a <template> + {{count}} placeholder syntax (collides with lit-html/Angular interpolation downstream) and web-component <slot>s (require shadow DOM; these elements are light-DOM).

Anything you want to highlight for special attention from reviewers?

Primer Button renders a trailing counter twice — a visible aria-hidden copy and an sr-only copy (app/components/primer/beta/button.html.erb). The wiring therefore uses plural data-targets / @targets dynamicLabelCounts and updates both copies, which also keeps the screen-reader text in sync.

The count is keyed on each item's data-value; items need values to be counted (consistent with existing selection tracking).

Accessibility

  • No new axe scan violation - This change does not introduce any new axe scan violations. The sr-only counter copy is kept in sync, so the selected count is announced to screen readers.

Merge checklist

  • Added/updated tests
  • Added/updated documentation
  • Added/updated previews (Lookbook)
  • Tested in Chrome
  • Tested in Firefox
  • Tested in Safari
  • Tested in Edge

Copilot AI review requested due to automatic review settings June 20, 2026 18:53
@changeset-bot

changeset-bot Bot commented Jun 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c396b9d

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

This PR includes changesets to release 1 package
Name Type
@openproject/primer-view-components 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

@myabc
myabc force-pushed the feature/DREAM-738-select-panel-dynamic-counter branch from 1c03c8c to f3efd5a Compare June 20, 2026 18:56
@myabc myabc changed the title Add dynamic selection counter to SelectPanel [DREAM-738] Add dynamic selection counter to SelectPanel Jun 20, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds an opt-in, self-updating selection counter to Primer::Alpha::SelectPanel show buttons, implemented with built-in Catalyst wiring so downstream consumers don’t need custom JavaScript.

Changes:

  • Extend with_show_button with counter: and counter_arguments: and render a trailing Counter wired via data-targets.
  • Update SelectPanelElement to keep the counter text/visibility in sync on connect, item activation, and remote fetch.
  • Add Lookbook preview plus component/system tests and a changeset entry.

Reviewed changes

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

Show a summary per file
File Description
app/components/primer/alpha/select_panel.rb Adds counter:/counter_arguments: support and wires the trailing counter via data-targets.
app/components/primer/alpha/select_panel_element.ts Implements live counter updates driven by the tracked selected-items map.
previews/primer/alpha/select_panel_preview.rb Registers the new “With counter” Lookbook preview.
previews/primer/alpha/select_panel_preview/with_counter.html.erb Adds a new preview template demonstrating the counter behavior.
test/components/alpha/select_panel_test.rb Adds unit tests asserting counter rendering, defaults, and argument merging.
test/system/alpha/select_panel_test.rb Adds a system test validating counter visibility and updates on selection changes.
.changeset/select-panel-dynamic-counter.md Documents the user-facing change and bumps package version.

Comment thread app/components/primer/alpha/select_panel.rb Outdated
Comment thread app/components/primer/alpha/select_panel_element.ts Outdated
Comment thread test/components/alpha/select_panel_test.rb Outdated
Comment thread test/components/alpha/select_panel_test.rb Outdated
Comment thread test/components/alpha/select_panel_test.rb Outdated
@myabc
myabc force-pushed the feature/DREAM-738-select-panel-dynamic-counter branch 2 times, most recently from 7d69076 to c2a2be3 Compare June 20, 2026 19:04
@github-actions

Copy link
Copy Markdown

⚠️ Visual or ARIA snapshot differences found

Our visual and ARIA snapshot tests found UI differences. Please review the differences by viewing the files changed tab to ensure that the changes were intentional.

Review differences

@myabc
myabc force-pushed the feature/DREAM-738-select-panel-dynamic-counter branch from d6c0b6b to 54a5efe Compare July 23, 2026 11:05
Comment thread app/components/primer/alpha/select_panel_element.ts
Comment thread app/components/primer/alpha/select_panel.rb Outdated
Comment thread test/system/alpha/select_panel_test.rb
Adds an opt-in, self-updating selection counter to the SelectPanel
show button, so consumers no longer need a bespoke Stimulus controller
to keep a Counter in sync with the selection.

`with_show_button` gains `counter:` and `counter_arguments:`. When
`counter: true`, the button renders a trailing Counter wired to a
Catalyst target, defaulting to `hide_if_zero: true`. The target is
forced via `merge_data` so a caller's `data:` cannot unwire it.
Passing `counter:` to an icon button raises ArgumentError.

SelectPanelElement writes the live count into the counter's text and
title on connect, on item activation, and after fetch. The count comes
from the tracked selectedItems Map rather than a DOM query, so it stays
correct in remote panels where the selected items have been filtered
out of the DOM. Button renders a trailing counter twice, a visible
aria-hidden copy and an sr-only copy, so the wiring uses plural
data-targets and updates both.

The client writes the raw count, so Counter's formatting arguments
only apply to the initial server render.
@myabc
myabc force-pushed the feature/DREAM-738-select-panel-dynamic-counter branch from 54a5efe to c396b9d Compare August 19, 2026 21:33
@myabc
myabc requested a review from bsatarnejad August 19, 2026 21:33
#
# @param icon [String] Name of <%= link_to_octicons %> to use instead of text. If an [icon](https://primer.style/octicons/usage-guidelines/) is provided, a <%= link_to_component(Primer::Beta::IconButton) %> will be rendered. Otherwise a <%= link_to_component(Primer::Beta::Button) %> will be rendered.
# @param counter [Boolean] When true, renders a dynamic selection counter on the show button that updates as items are selected.
# @param counter_arguments [Hash] System arguments forwarded to the trailing Counter (e.g. `scheme:`). The `data-target` wiring is applied automatically and cannot be overridden. Note that the client writes the raw selected count, so `Counter`'s formatting arguments (`limit:`, `round:`) only affect the initial server render.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Small wording question: since data-targets is merged with the caller’s targets, would “cannot be removed” be a little clearer than “cannot be overridden”?

# @param counter_arguments [Hash] System arguments forwarded to the trailing Counter (e.g. `scheme:`). The `data-target` wiring is applied automatically and cannot be overridden. Note that the client writes the raw selected count, so `Counter`'s formatting arguments (`limit:`, `round:`) only affect the initial server render.
# @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Beta::Button) %>.
renders_one :show_button, lambda { |icon: nil, **system_arguments|
renders_one :show_button, lambda { |icon: nil, counter: false, counter_arguments: {}, **system_arguments|

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

is supporting counter: true for single-select panels intentional? It looks like the counter would only show 0 or 1. I wasn’t sure whether the feature was intended only for multi-select panels.

else
Primer::Beta::Button.new(**system_arguments)
button = Primer::Beta::Button.new(**system_arguments)
button.with_trailing_visual_counter(**dynamic_counter_arguments(counter_arguments)) if counter

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Could combining counter: true with dynamic_label: true make the accessible button name a little repetitive? For example, it might contain both “Item 2, Item 3” and “2”.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

3 participants