Skip to content

CODAP-1508: toggle legend category selection on a modifier click - #2703

Open
kswenson wants to merge 3 commits into
mainfrom
CODAP-1508-legend-selection-toggle
Open

kswenson wants to merge 3 commits into
mainfrom
CODAP-1508-legend-selection-toggle

Conversation

@kswenson

@kswenson kswenson commented Sep 16, 2026

Copy link
Copy Markdown
Member

Summary

Clicking a legend key a second time now leaves the selection alone, as it always has; the modifier click is what changed. It toggles a category in and out of the selection instead of only ever adding it.

The story asked for the opposite — plain click should toggle — but that premise didn't survive checking. V2's selectCasesInCell replaces on plain click and never deselects, and v3's handleClickOnCase has an explicit branch for an already-selected case that deliberately does nothing unless a modifier is down. Legends that toggle on plain click elsewhere in the industry (Plotly, Highcharts, Chart.js) are toggling series visibility, which has no competing replace operation; legends that drive selection of other things (Power BI, Vega-Lite's legend binding) all replace on plain click and toggle on a modifier. The deciding argument is that there are three intents — replace, adjust, clear — wanting three gestures, and toggling on plain click eliminates "replace" rather than adding anything. Full reasoning is in the Jira story.

The real defect was that selectCases(caseIds, dataset) omitted its third argument, so select defaulted to true and a modifier click could only add a category, never remove one. Once several categories were selected there was no way to take one back out — which is the actual gap behind the report's "there is no way to clear a selection from the legend."

Modifier handling is now centralized

src/utilities/platform-utils.ts gains two functions, and they are the only two questions a click handler should ask:

  • hasSelectionModifier(event) — shift or the platform command key. For paths that act on a selection.
  • preservesSelection(event) — any of shift/cmd/ctrl. For paths that would clear one.

They differ deliberately. Acting on a selection should be precise, so hasSelectionModifier excludes ctrl on a Mac, where it is the secondary-click gesture. Clearing one should be conservative, so preservesSelection treats any modifier as reason not to destroy the user's work. The command key is platform-exclusive — cmd on a Mac, ctrl everywhere else — which matters given how much of our audience is on Chromebooks and Windows.

Centralizing this immediately caught something that would otherwise have spread: alt can't be a selection modifier, because option-click already zooms the plot background and rescales axes. The case table's extend check includes it today.

Three background-deselect guards were already spelling out the same expression as preservesSelection and now call it instead — no behavior change, but it makes the utility a real single source of truth rather than a helper used once.

Also

Removes a stale comment claiming a commented-out setOrExtendSelection call was a reverted toggle. It was functionally identical to the code that replaced it, in Jan 2025 as well as today. What broke the Cypress test back then was routing selection through applyModelChange, which shifted the undo counts in a spec built on long undo/redo sequences — and 34b50cc3c later moved it onto the notification path anyway.

Testing

  • platform-utils.test.ts is new: 12 tests, with isolated module loading per platform since isMac is evaluated at import.
  • Four Cypress tests in graph-legend.spec.ts, platform-aware via Cypress.platform so the same spec asserts cmd-toggles/ctrl-doesn't on macOS and ctrl-toggles/cmd-doesn't on CI's Linux.
  • All 24 tests in graph-legend.spec.ts pass locally, including the pre-existing selection test that the stale comment blamed.
  • Mutation-tested: making hasSelectionModifier permissive fails 3 unit tests; collapsing preservesSelection into the exclusive form fails the test encoding the asymmetry; reverting the legend handler to add-only fails both Cypress toggle tests.

Follow-up

A consistency sweep is filed separately as CODAP-1533: bars, box-plot ranges, map grid cells, and the map pin layer have the same add-only gap, and the case table's extend check omits ctrlKey while including altKey. Ctrl-click multi-select in the case table may well be broken for Windows and Chromebook users today — that is read from the code and not yet confirmed in a browser.

Fixes CODAP-1508

🤖 Generated with Claude Code

A legend key selects cases elsewhere rather than being selected itself, but
plain click should still replace the selection rather than toggle it, matching
V2, point selection, and Power BI/Vega-Lite. The real gap was that the modifier
click could only ever add a category, never remove one.

Adds hasSelectionModifier and preservesSelection to platform-utils so every
click handler asks one of two questions in one place, and converts the three
background-deselect guards that already spelled out preservesSelection's
expression. The two differ deliberately: acting on a selection is precise,
clearing one is conservative.

Also removes a stale comment claiming a commented-out setOrExtendSelection call
was a reverted toggle. It was functionally identical to the code that replaced
it; what broke the Cypress test was routing selection through applyModelChange,
which 34b50cc later did anyway.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@kswenson kswenson added the v3 CODAP v3 label Sep 16, 2026
@kswenson
kswenson requested a balanced review from Copilot September 16, 2026 05:17
@codecov

codecov Bot commented Sep 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.73684% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 88.03%. Comparing base (830f6e4) to head (dfa4e54).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...a-display/components/legend/categorical-legend.tsx 85.71% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2703      +/-   ##
==========================================
+ Coverage   88.02%   88.03%   +0.01%     
==========================================
  Files         822      822              
  Lines       47657    47668      +11     
  Branches    12071    12166      +95     
==========================================
+ Hits        41950    41965      +15     
+ Misses       5691     5689       -2     
+ Partials       16       14       -2     
Flag Coverage Δ
cypress 69.87% <92.85%> (+0.01%) ⬆️
jest 63.44% <57.89%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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.

🟢 Approval recommended

The focused implementation is consistent with existing selection semantics and includes comprehensive platform-aware tests.

Pull request overview

Centralizes modifier-key handling and enables categorical legend categories to be toggled with platform-appropriate modifier clicks.

Changes:

  • Adds shared selection-modifier utilities with unit coverage.
  • Toggles categorical legend selections on Shift/Cmd/Ctrl clicks.
  • Reuses centralized selection-preservation logic and adds Cypress coverage.
File summaries
File Description
v3/src/utilities/platform-utils.ts Adds modifier-key helpers.
v3/src/utilities/platform-utils.test.ts Tests platform-specific behavior.
v3/src/components/map/hooks/use-map-click-deselect.ts Centralizes map deselection checks.
v3/src/components/graph/plots/scatter-plot/use-residual-marquee.ts Centralizes residual-plot deselection checks.
v3/src/components/data-display/hooks/use-renderer-pointer-down-deselect.ts Centralizes renderer deselection checks.
v3/src/components/data-display/components/legend/categorical-legend.tsx Implements modifier-click toggling.
v3/cypress/support/helpers/graph-legend-helper.ts Adds modifier-click test helpers.
v3/cypress/e2e/graph-legend.spec.ts Covers replacement and toggling behavior.
Review details
  • Files reviewed: 8/8 changed files
  • Comments generated: 0
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@cypress

cypress Bot commented Sep 16, 2026

Copy link
Copy Markdown

codap-v3    Run #12285

Run Properties:  status check passed Passed #12285  •  git commit dfa4e54de2: null
Project codap-v3
Branch Review CODAP-1508-legend-selection-toggle
Run status status check passed Passed #12285
Run duration 07m 49s
Commit git commit dfa4e54de2: null
Committer null
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 1
Tests that did not run due to a developer annotating a test with .skip  Pending 82
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 391
View all changes introduced in this branch ↗︎

Two tests named "will select and unselect ..." had their unselect half
commented out, under a note guessing that background-click deselection was
broken. It is not: useRendererPointerDown deselects only when event.target is
the renderer's canvas, and the helper clicked the SVG plot background layered
beneath it. The `force` flag was the tell -- it was there to bypass the
actionability check that would have reported the rect as covered, which made
the helper simulate a click no user can perform.

Repointing the helper at the canvas makes both assertions pass, so the tests
now test what their names claim. Verified in the browser first: clicking the
canvas clears the selection, clicking the rect does nothing.

The map specs have four more sites commented out the same way, but the map
clears through use-map-click-deselect and Leaflet rather than the renderer
canvas, so that is a separate diagnosis. Noted on CODAP-1533.

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

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.

🟢 Approval recommended

The behavior is coherent and well tested; the remaining feedback concerns non-blocking documentation accuracy.

Review details

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

v3/src/utilities/platform-utils.ts:32

  • This says every caller uses the helper, but nominal selection handlers such as data-display-utils.ts:60 and map-point-grid.tsx:57 still inspect modifier flags directly. Please either migrate those callers or phrase this as guidance so the API documentation reflects the current code.
    v3/src/utilities/platform-utils.ts:42
  • The “any modifier at all” wording contradicts the implementation and its tests, which intentionally ignore altKey. Naming the three preserving modifiers avoids misleading future callers.
  • Files reviewed: 8/8 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Copilot flagged both. "Every caller routes through here" stated an aspiration
as fact -- several graph, map, and case table handlers still read modifier
flags directly. And "any modifier at all" contradicted both the implementation
and platform-utils.test.ts, which deliberately exclude altKey.

Comments only; no behavior change.

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

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.

🟢 Approval recommended

The implementation is consistent with existing selection semantics and has focused unit and end-to-end coverage.

Review details
  • Files reviewed: 8/8 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@kswenson
kswenson marked this pull request as ready for review September 16, 2026 06:18
@kswenson
kswenson requested a review from emcelroy September 16, 2026 06:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants