Skip to content

Fix repeat-status silence, all-clear gate label, and wheel keyboard leak - #91

Merged
min0625 merged 1 commit into
mainfrom
fix/status-repeat-gate-level-keyboard-guard
Jul 29, 2026
Merged

min0625 merged 1 commit into
mainfrom
fix/status-repeat-gate-level-keyboard-guard

Conversation

@min0625

@min0625 min0625 commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Summary

  • showPreview now clears #status itself before the 900ms timer would, so two identical VoiceOver announcements in a row (e.g. two invalid words back to back) aren't swallowed by unchanged textContent.
  • boot() rewrites the opening gate's level line to the all-clear string when the save's currentLevel is out of range, so a player who cleared level 500 doesn't briefly see "Level 501" before landing on the all-clear screen.
  • Wheel keydown handling now checks whether any overlay/gate is open or #screen-game is hidden, instead of relying on ev.target.closest('.overlay') — that check only worked when focus happened to be inside the card, which isn't true when the clear-card overlay is dismissed (focus returns to body) or when the opening gate is showing (never focused at all). Previously keystrokes could leak into an invisible wheel and submit an unseen word.
  • .card:focus outline removed (the card is a programmatic focus target for VoiceOver, not a tabbable control) and .chip line-height pinned so the player-id pill doesn't grow taller when its label switches to the Chinese "已複製" text.
  • shareCopied copy reworded for clarity.
  • Docs (docs/word-puzzle-game-design.md, docs/word-puzzle-ui-screens.md) and tests updated to match, plus new unit tests for the player-id round trip (newUid/isUid/normalizeUid/formatUid).

Test plan

  • mise run check (biome lint + bun test, 52 tests pass)
  • Manual verification on real device per updated §17 checklist items (VoiceOver repeat-announcement, all-clear gate label, keyboard-leak scenarios)

🤖 Generated with Claude Code

…eaking under overlays

- showPreview clears #status itself before the 900ms timer would, so a
  second identical message (e.g. two invalid words in a row) still gets
  announced by VoiceOver instead of being swallowed by unchanged textContent.
- boot() rewrites the gate's level line to the all-clear string when the
  save's currentLevel is out of range, so a player who cleared level 500
  doesn't see "Level 501" before landing on the all-clear screen.
- Wheel keydown handling now checks whether any overlay/gate is open or the
  game screen is hidden, not just focus location — closing a clear-card
  overlay or the opening gate never focuses the card, so the old
  `.closest('.overlay')` check let keystrokes leak into an invisible wheel
  (submitting unseen words, mutating hidden #preview/#status).
- .card:focus outline removed (card is a programmatic focus target, not a
  tabbable control) and CJK line-height fixed on .chip so the player-id pill
  doesn't grow taller when its label switches to Chinese "已複製" text.
- shareCopied wording reworded for clarity.
- Docs and tests updated to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@min0625
min0625 merged commit b1bd4c9 into main Jul 29, 2026
1 check passed
@min0625
min0625 deleted the fix/status-repeat-gate-level-keyboard-guard branch July 29, 2026 04:00
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Fix repeated status announcements, gate labels, and wheel key leaks

🐞 Bug fix 🧪 Tests ✨ Enhancement 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Restores repeated VoiceOver announcements and corrects all-clear gate text for completed saves.
• Prevents hidden wheel submissions while gates, overlays, or non-game screens are active.
• Stabilizes dialog/chip styling, clarifies sharing copy, and adds UID round-trip coverage.
Diagram

graph TD
  I["Keyboard Event"] --> G{"Wheel Visible?"} -->|Yes| W["Wheel Controller"] --> U["Preview Status"]
  G -->|No| X["Ignore Key"]
  S["Saved Level"] --> B["Boot Check"] --> L["Opening Gate"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Centralized UI interaction state
  • ➕ Makes wheel availability explicit and independently testable.
  • ➕ Avoids querying DOM visibility for every keyboard event.
  • ➕ Provides one authority for screens, gates, and modal state.
  • ➖ Requires a broader state-management refactor across existing screen transitions.
  • ➖ Introduces synchronization risk while migrating DOM-driven behavior.
2. Lifecycle-managed keyboard listener
  • ➕ Removes the global listener whenever gameplay becomes inactive.
  • ➕ Prevents hidden input without inspecting focus or DOM visibility per event.
  • ➖ Every overlay and screen transition must correctly register or unregister the listener.
  • ➖ Nested overlays and exceptional error paths can leave listener state desynchronized.

Recommendation: Keep the PR’s DOM-visibility guard for this targeted fix. The rendered UI is the current source of truth, so checking visible gates, overlays, and the game screen directly closes the focus-dependent leak with minimal transition risk. A centralized interaction-state controller would be preferable only if screen and modal orchestration becomes more complex.

Files changed (8) +100 / -13

Enhancement (1) +2 / -1
strings.jsClarify the successful share-copy message +2/-1

Clarify the successful share-copy message

• Rewords 'shareCopied' to use player-facing language and explicitly explain that copied sharing content can be pasted to friends.

src/strings.js

Bug fix (3) +35 / -5
main.jsReset repeated status messages and correct completed-save gate labels +8/-0

Reset repeated status messages and correct completed-save gate labels

• Clears '#status' whenever a new preview begins so identical live-region messages can be announced consecutively. After loading the level count, replaces an out-of-range saved level label with the all-clear text.

src/main.js

style.cssPrevent chip resizing and suppress programmatic card outlines +10/-0

Prevent chip resizing and suppress programmatic card outlines

• Pins chip line height to prevent CJK copy feedback from changing pill and overlay dimensions. Removes the focus outline from dialog cards used only as programmatic VoiceOver focus targets.

src/style.css

wheel.jsBlock keyboard input whenever the wheel is not visible +17/-5

Block keyboard input whenever the wheel is not visible

• Introduces a keyboard guard covering focused form fields, visible gates or overlays, and hidden game screens. Replaces the focus-dependent '.closest('.overlay')' check that allowed unseen submissions.

src/wheel.js

Tests (1) +55 / -1
game.test.mjsAdd deterministic player-ID round-trip tests +55/-1

Add deterministic player-ID round-trip tests

• Adds coverage for UID generation, checksum validation, malformed values, display formatting, and pasted-value normalization. Uses a fixed fixture to avoid probabilistic checksum assertions.

tests/game.test.mjs

Documentation (3) +8 / -6
CLAUDE.mdDocument player-ID round-trip test coverage +1/-1

Document player-ID round-trip test coverage

• Expands the testing guidance to cover UID generation, validation, normalization, and formatting. Explains why deterministic fixtures are required for checksum typo tests.

CLAUDE.md

word-puzzle-game-design.mdDocument hidden-wheel guards and accessibility regressions +4/-2

Document hidden-wheel guards and accessibility regressions

• Defines when development keyboard input must be blocked and explains why focus-based overlay detection was unreliable. Adds manual checks for all-clear gate text and repeated VoiceOver announcements, and updates sharing copy expectations.

docs/word-puzzle-game-design.md

word-puzzle-ui-screens.mdAlign UI specifications with gate and sharing fixes +3/-3

Align UI specifications with gate and sharing fixes

• Documents boot-time correction of completed-save gate labels and clarifies the opening gate’s TTS role. Updates the settings/share behavior and localized 'shareCopied' wording.

docs/word-puzzle-ui-screens.md

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (2) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 80 rules
✅ Skills: frontend-design

Grey Divider


Remediation recommended

1. Added comments use Chinese 📘 Rule violation ⚙ Maintainability
Description
New comments in multiple source files contain Chinese natural-language text. The checklist requires
all source-code comments, including mixed-language comments, to be written in English.
Code

src/main.js[R277-279]

+  // 取消的那個 timer 同時是 #status 的清空者,所以這裡要自己補清一次:下一次拖曳在 900ms 內
+  // 開始(拼錯後馬上重試就是這樣)會走到這裡,不清的話 #status 就永久停在上一句,
+  // 而連撞兩次同樣的訊息時 textContent 沒變 → 輔助技術不會播報第二次,正是下面那段註解要避免的事。
Evidence
PR Compliance ID 2366288 prohibits non-English or mixed-language source comments. The cited
additions contain Chinese explanatory sentences in JavaScript, CSS, and test comments.

Rule 2366288: Source code comments must be written in English
src/main.js[277-279]
src/main.js[865-867]
src/strings.js[46-46]
src/style.css[168-170]
src/style.css[660-662]
src/wheel.js[7-18]
tests/game.test.mjs[260-264]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New source-code comments are written wholly or partly in Chinese, contrary to the English-only comment requirement.

## Issue Context
Preserve identifiers and technical meaning while translating all newly added human-readable comments into English.

## Fix Focus Areas
- src/main.js[277-279]
- src/main.js[865-867]
- src/strings.js[46-46]
- src/style.css[168-170]
- src/style.css[660-662]
- src/wheel.js[7-18]
- tests/game.test.mjs[260-264]
- tests/game.test.mjs[266-266]
- tests/game.test.mjs[270-270]
- tests/game.test.mjs[282-282]
- tests/game.test.mjs[287-289]
- tests/game.test.mjs[296-296]
- tests/game.test.mjs[301-301]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Checksum vectors remain untested 📘 Rule violation ≡ Correctness
Description
The new UID test changes a body character but only verifies that the unchanged checksum is rejected;
it never asserts the predictable checksum characters for the modified body. This leaves the checksum
test requirement incomplete and can miss regressions outside the single fixed UID.
Code

tests/game.test.mjs[R268-270]

+test('uid:合法編號驗得過,打錯一碼被校驗碼擋下', () => {
+  assert.ok(isUid(UID));
+  assert.equal(isUid(`0${UID.slice(1)}`), false); // 首碼 4 → 0
Evidence
PR Compliance ID 2241551 explicitly requires a test showing that changing one of the first ten
characters changes the two checksum characters predictably. The added test retains the original
checksum and asserts only isUid(...) === false, while uidCheck in storage.js is the
calculation whose exact output remains unasserted.

Rule 2241551: Player IDs from newUid must be 12-character Crockford Base32 with 10 random chars + 2 checksum chars
tests/game.test.mjs[268-270]
src/storage.js[24-27]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The UID tests do not assert the exact checksum produced for changed 10-character bodies.

## Issue Context
PR Compliance ID 2241551 requires predictable checksum coverage, not only rejection of a body paired with its old checksum. Add deterministic vectors that exercise the checksum calculation and assert the expected final two characters; minimally expose the checksum helper for testing if necessary.

## Fix Focus Areas
- tests/game.test.mjs[268-270]
- src/storage.js[24-27]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. All-clear label still flashes 🐞 Bug ≡ Correctness
Description
boot() renders the out-of-range level immediately and applies the all-clear correction only after
index.json finishes loading. On a slow request, the gate therefore shows the nonexistent level and
can be dismissed before the correction runs, defeating this fix's stated behavior.
Code

src/main.js[868]

+  if (save.currentLevel > levelCount) $('gate-level').textContent = strings.allClear;
Evidence
The gate click handler hides the gate immediately, while boot() first writes the saved level,
awaits the index fetch and parsing, and only then applies the new all-clear text. The gate is a
visible full-screen button during that interval.

src/main.js[844-868]
index.html[75-79]
src/style.css[581-603]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The all-clear gate initially displays the out-of-range saved level and remains dismissible while the level index is loading. Ensure the gate cannot present or be dismissed with the incorrect label.

## Issue Context
`boot()` writes `strings.levelTitle(save.currentLevel)` before awaiting `index.json`, while the existing click handler can hide the gate at any time. The added correction therefore arrives too late under network latency.

## Fix Focus Areas
- src/main.js[844-868]
- index.html[75-79]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

4. Focused dialogs lose indicator 🐞 Bug ≡ Correctness
Description
The new rule removes the outline from cards even though opening either dialog programmatically moves
focus to that card. Sighted keyboard users consequently receive no visible indication of the initial
focus location until they Tab to a child control.
Code

src/style.css[R663-664]

+.card:focus {
+  outline: none;
Evidence
Both dialog containers are explicitly focusable and are focused when shown. The added CSS
unconditionally suppresses the browser outline on that exact focus target without providing a
replacement.

src/style.css[660-665]
src/main.js[479-483]
src/main.js[687-694]
index.html[129-153]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Dialog cards receive programmatic focus but now suppress its only explicit visual indication. Preserve a focus indicator when the dialog was opened through keyboard interaction while avoiding an unwanted pointer-focus ring.

## Issue Context
Both cards use `tabindex="-1"`, and `main.js` focuses them when they open so assistive technology announces their headings. An unconditional `:focus` outline removal also affects keyboard-triggered openings; use an appropriate `:focus-visible` treatment or a custom indicator instead.

## Fix Focus Areas
- src/style.css[660-665]
- src/main.js[479-483]
- src/main.js[687-694]
- index.html[129-153]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread src/main.js
Comment on lines +277 to +279
// 取消的那個 timer 同時是 #status 的清空者,所以這裡要自己補清一次:下一次拖曳在 900ms 內
// 開始(拼錯後馬上重試就是這樣)會走到這裡,不清的話 #status 就永久停在上一句,
// 而連撞兩次同樣的訊息時 textContent 沒變 → 輔助技術不會播報第二次,正是下面那段註解要避免的事。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. Added comments use chinese 📘 Rule violation ⚙ Maintainability

New comments in multiple source files contain Chinese natural-language text. The checklist requires
all source-code comments, including mixed-language comments, to be written in English.
Agent Prompt
## Issue description
New source-code comments are written wholly or partly in Chinese, contrary to the English-only comment requirement.

## Issue Context
Preserve identifiers and technical meaning while translating all newly added human-readable comments into English.

## Fix Focus Areas
- src/main.js[277-279]
- src/main.js[865-867]
- src/strings.js[46-46]
- src/style.css[168-170]
- src/style.css[660-662]
- src/wheel.js[7-18]
- tests/game.test.mjs[260-264]
- tests/game.test.mjs[266-266]
- tests/game.test.mjs[270-270]
- tests/game.test.mjs[282-282]
- tests/game.test.mjs[287-289]
- tests/game.test.mjs[296-296]
- tests/game.test.mjs[301-301]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread tests/game.test.mjs
Comment on lines +268 to +270
test('uid:合法編號驗得過,打錯一碼被校驗碼擋下', () => {
assert.ok(isUid(UID));
assert.equal(isUid(`0${UID.slice(1)}`), false); // 首碼 4 → 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. Checksum vectors remain untested 📘 Rule violation ≡ Correctness

The new UID test changes a body character but only verifies that the unchanged checksum is rejected;
it never asserts the predictable checksum characters for the modified body. This leaves the checksum
test requirement incomplete and can miss regressions outside the single fixed UID.
Agent Prompt
## Issue description
The UID tests do not assert the exact checksum produced for changed 10-character bodies.

## Issue Context
PR Compliance ID 2241551 requires predictable checksum coverage, not only rejection of a body paired with its old checksum. Add deterministic vectors that exercise the checksum calculation and assert the expected final two characters; minimally expose the checksum helper for testing if necessary.

## Fix Focus Areas
- tests/game.test.mjs[268-270]
- src/storage.js[24-27]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/main.js
// 全破的存檔 currentLevel 是 levelCount+1(一個不存在的關),上面那行閘門文案先寫的就是它——
// 破完 500 關的人會看到「第 501 關」,點下去卻是全破畫面。要到這裡才知道超界,所以改在這裡補寫:
// 此時閘門幾乎必然還蓋著(玩家還沒點),而且不必為了正確的第一印象多等 index.json 那趟 RTT。
if (save.currentLevel > levelCount) $('gate-level').textContent = strings.allClear;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

3. All-clear label still flashes 🐞 Bug ≡ Correctness

boot() renders the out-of-range level immediately and applies the all-clear correction only after
index.json finishes loading. On a slow request, the gate therefore shows the nonexistent level and
can be dismissed before the correction runs, defeating this fix's stated behavior.
Agent Prompt
## Issue description
The all-clear gate initially displays the out-of-range saved level and remains dismissible while the level index is loading. Ensure the gate cannot present or be dismissed with the incorrect label.

## Issue Context
`boot()` writes `strings.levelTitle(save.currentLevel)` before awaiting `index.json`, while the existing click handler can hide the gate at any time. The added correction therefore arrives too late under network latency.

## Fix Focus Areas
- src/main.js[844-868]
- index.html[75-79]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/style.css
Comment on lines +663 to +664
.card:focus {
outline: none;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Informational

4. Focused dialogs lose indicator 🐞 Bug ≡ Correctness

The new rule removes the outline from cards even though opening either dialog programmatically moves
focus to that card. Sighted keyboard users consequently receive no visible indication of the initial
focus location until they Tab to a child control.
Agent Prompt
## Issue description
Dialog cards receive programmatic focus but now suppress its only explicit visual indication. Preserve a focus indicator when the dialog was opened through keyboard interaction while avoiding an unwanted pointer-focus ring.

## Issue Context
Both cards use `tabindex="-1"`, and `main.js` focuses them when they open so assistive technology announces their headings. An unconditional `:focus` outline removal also affects keyboard-triggered openings; use an appropriate `:focus-visible` treatment or a custom indicator instead.

## Fix Focus Areas
- src/style.css[660-665]
- src/main.js[479-483]
- src/main.js[687-694]
- index.html[129-153]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

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.

1 participant