Skip to content

Allow deselecting chosen problem card on click - #455

Open
woohsin wants to merge 1 commit into
sysprog21:mainfrom
woohsin:deselect-problem-card-392
Open

woohsin wants to merge 1 commit into
sysprog21:mainfrom
woohsin:deselect-problem-card-392

Conversation

@woohsin

@woohsin woohsin commented Oct 23, 2026 •

Copy link
Copy Markdown

Summary

Support deselecting an active problem card by clicking it again in the lobby problem picker.

When the currently chosen card is clicked:

  • Releases the manual selection state by resetting manualProblem = false and keptDraw = null.
  • Invokes recommend() if history is ready to fall back to the default recommendation engine, or setProblem(null) if it is not.
  • Preserves the existing searchVisibilityException state when selecting/reselecting.

Verification

Added browser test in tests/browser/lobby.test.js to verify:

  1. Clicking a problem card applies manual selection (snap.card === cardId, prompt updated).
  2. Clicking the active card releases the manual override (manualProblem = false, keptDraw = null) and re-triggers recommend(), clearing the manual selection prompt.

Closes #392


Summary by cubic

Clicking the currently chosen problem card in the lobby picker now releases the manual selection and restores the default recommendation.

  • Resets manualProblem and keptDraw, then re-triggers recommend() when history is ready, or clears the problem and its recommendation text otherwise.
  • Preserves the searchVisibilityException state when reselecting.
  • Adds a browser test that toggles the active card off and verifies the picker falls back to the default recommendation.

Closes #392.

Written for commit d798e17. Summary will update on new commits.

View guided diff

cubic-dev-ai[bot]

This comment was marked as resolved.

Comment thread tests/browser/lobby.test.js Outdated

@ColtenOuO ColtenOuO left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please squash your commit.

@ColtenOuO ColtenOuO changed the title web: allow deselecting chosen problem card on click Allow deselecting chosen problem card on click Oct 23, 2026
@woohsin
woohsin force-pushed the deselect-problem-card-392 branch from 5d21671 to d798e17 Compare October 23, 2026 20:15
@woohsin

woohsin commented Oct 23, 2026

Copy link
Copy Markdown
Author

Squashed into a single commit. Thanks for the guidance!

Comment thread web/app.js
card.button.addEventListener("click", () => {
if (manualProblem && problem === card) {
manualProblem = false;
keptDraw = null;

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.

Releasing the pick never reapplies the level filters, so whatever the pick changed stays changed. Say "My list" draws an Easy card while only Medium is checked. The draw sets the duration to 30 and unhides the card as a searchVisibilityException. Click that card to release it: recommend() picks a Medium problem, but the duration stays at 30. And because setProblem clears the exception without hiding the card, the Easy card stays in the picker even though the filters exclude it. The random button and the level checkboxes avoid this by calling applyDifficulties() first, which reapplies the filters and resets the suggested duration (it already leaves a duration the candidate chose alone):

Suggested change
keptDraw = null;
keptDraw = null;
applyDifficulties();

Comment thread tests/browser/lobby.test.js Outdated

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

Improve git commit messages per https://cbea.ms/git-commit/ and CONTRIBUTING.md; CI applies the same rules. Hint: run make hooks once per clone, so the hooks check every message before you push.

Before asking someone to review, you MUST resolve any regressions flagged by GitHub Actions.

@woohsin
woohsin force-pushed the deselect-problem-card-392 branch from d798e17 to 7d0c5a0 Compare October 24, 2026 17:58

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

Improve git commit messages per https://cbea.ms/git-commit/ and CONTRIBUTING.md; CI applies the same rules. Hint: run make hooks once per clone, so the hooks check every message before you push.

Before asking someone to review, you MUST resolve any regressions flagged by GitHub Actions.

Comment thread tests/browser/lobby.test.js Outdated
Comment thread tests/browser/lobby.test.js Outdated
@woohsin
woohsin force-pushed the deselect-problem-card-392 branch 3 times, most recently from e15fb41 to ccf9c85 Compare October 24, 2026 20:34
@woohsin

woohsin commented Oct 24, 2026

Copy link
Copy Markdown
Author

Updated: verified both tests locally with Playwright Chromium. Fixed the test assertion by revealing the problem picker before asserting card visibility, using heldLobby() for testing the pre-history state, and asserting snapshot.duration. Could you please approve the workflow run again? Thank you!

Comment thread web/app.js
if (manualProblem && problem === card) {
manualProblem = false;
keptDraw = null;
applyDifficulties();

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.

Deselecting an out-of-filter card (for example one drawn from the practice list while the checkboxes select another level) makes applyDifficulties() hide the very button that has focus, so keyboard focus falls back to body and a keyboard user loses their place in the picker. Move focus to a visible control before hiding it, such as the details.problem-picker summary, when document.activeElement is card.button. The new out-of-filter test only clicks, so it could also activate the card by keyboard and assert where focus lands.

);

lobbyTest(
"clicking an already selected problem card releases manual selection",

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.

The accepted scope for this change asks for the case where the recommendation lands on the same card as the one just released, and no test here reaches it: the first test releases an Easy card and gets a Medium one, and the second holds the history so recommend() never runs. Seed a due review with savedAttempt(due), click that card (the note becomes Selected problem: ...), click it again, and assert the card is unchanged but the note now reads Review due after. That is what tells a released pick from a kept one when the same card comes back.

@woohsin
woohsin force-pushed the deselect-problem-card-392 branch from ccf9c85 to 05fd88b Compare October 25, 2026 12:43
Comment thread web/app.js
syncLobbyControls();
});
card.button.addEventListener("click", () => {
if (manualProblem && problem === card) {

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.

The deselect branch calls recommend() with none of the guards the other pickers use. nodes.randomProblem and nodes.practiceListPick return early on accountUpdatePending() and deletingReports, and accountUpdatePending() can be true while historyReady is still true (historyRefreshing, loginPending). Deselecting during a login or history refresh therefore draws from the previous account's reports, and during a bulk delete from reports that are about to disappear. Return early under the same conditions, or at least when accountUpdatePending() || deletingReports || starting.

Comment thread web/app.js
});
card.button.addEventListener("click", () => {
if (manualProblem && problem === card) {
if (document.activeElement === card.button) {

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.

focusPickerSummary() runs whenever the card has focus, not only when it is about to be hidden. Deselecting an in-filter card with Enter or Space, or one recommend() picks again, moves focus out of the grid to the picker summary while the card stays visible, so keyboard users lose their place. Move focus only when the card ends up hidden, for example after applyDifficulties() by checking card.button.hidden. The new test covers only the out-of-filter case, so add a keyboard test for an in-filter card.

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

Rebase onto the latest main branch, fix any regressions detected by GitHub Actions, and address the review comments.

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

Rebase onto the latest main branch, fix any regressions detected by GitHub Actions, and address the review comments.

Support toggling off a manually selected problem card by
clicking it again. Clicking the active card clears
manualProblem and keptDraw, re-applies difficulty filters,
and re-triggers recommend() if history is ready (or resets
state and note if not).

When deselecting a card that currently holds focus, move
focus to details.problem-picker summary before hiding it to
avoid dropping keyboard focus to body. Add browser tests for
keyboard focus retention and deselecting when the recommendation
lands on the same card.

Closes sysprog21#392
@woohsin
woohsin force-pushed the deselect-problem-card-392 branch from 05fd88b to 09ab63c Compare October 25, 2026 14:47
@jserv

jserv commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

A small patch against 09ab63c that covers the open review points on the deselect branch and the failing clicking an already selected problem card releases manual selection test. I only applied it to copies of the two files and ran node --check; I did not run the browser suite.

What it does:

  • The release branch now returns early on accountUpdatePending() || deletingReports, like #random-problem and #practice-list-pick, so it cannot recommend from reports that are about to change.
  • Focus moves to the picker summary only when the release actually hid the card (card.button.hidden after the redraw). A card that stays in the grid keeps focus.
  • The test no longer asserts that the redraw equals the first recommendation. It asserts that the pick is released (after.card !== easyCard), that the note is a Selected problem: recommendation, and that the new card is shown in the grid. This is a stopgap: roll is unchanged on this path, so please restore after.card === initial.card once the CI mismatch is explained.
--- a/web/app.js
+++ b/web/app.js
@@ -311,9 +311,10 @@
   });
   card.button.addEventListener("click", () => {
     if (manualProblem && problem === card) {
-      if (document.activeElement === card.button) {
-        focusPickerSummary();
-      }
+      // The same gates the other pickers use: recommending while an account
+      // refresh or a delete is in flight draws from reports about to change.
+      if (accountUpdatePending() || deletingReports) return;
+      const hadFocus = document.activeElement === card.button;
       manualProblem = false;
       keptDraw = null;
       applyDifficulties();
@@ -323,6 +324,9 @@
         setProblem(null);
         nodes.recommendation.textContent = "";
       }
+      // A button that stays in the grid keeps the focus; only one the release
+      // just hid would drop it to the body.
+      if (hadFocus && card.button.hidden) focusPickerSummary();
       return;
     }
     // setProblem clears the exception; restore it when reselecting.
--- a/tests/browser/lobby.test.js
+++ b/tests/browser/lobby.test.js
@@ -2312,9 +2312,17 @@
     await cardButton.focus();
     await page.keyboard.press("Enter");
 
+    // Asserts what must hold of any redraw: the pick is released and the card
+    // now selected is shown in the grid. Restore `after.card === initial.card`
+    // once the CI mismatch is explained, since `roll` is unchanged here.
     const after = await snapshot(page);
-    assert.equal(after.card, initial.card);
-    assert.equal(after.note, initial.note);
+    assert.notEqual(after.card, easyCard);
+    assert.match(after.note, /^Selected problem:/);
+    assert.equal(
+      await page.locator(`[data-problem="${after.card}"]`).isVisible(),
+      true,
+      "the redrawn card must be shown in the grid",
+    );
     assert.equal(
       after.duration,
       initial.duration,

Open points this does not settle:

  • The cause of the CI mismatch is not established. recommend() draws with () => roll, so with an unchanged roll, level and reports the redraw should reproduce the first card. If the card really changes with nothing else changing, something resets roll or the pool on this path and is worth finding.
  • The new guard returns silently while an account update or delete is pending, so the selected card looks clickable but does nothing. Consider disabling it or showing a busy message, as syncLobbyControls does for the other pickers.
  • Still untested: the historyReady === false branch (use holdHistory(), then assert no pressed card, an empty recommendation and Start disabled), and focus staying put when the released card remains visible.

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

Before asking someone to review, you MUST resolve any regressions flagged by GitHub Actions.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow deselecting a chosen problem card by clicking it again

3 participants