Skip to content

fix(dashboard): claim IME Escape before the command bar consumes it (#5481) - #7245

Merged
dwu96 merged 1 commit into
mainfrom
fix/commandbar-ime-escape-5481
Aug 31, 2026
Merged

fix(dashboard): claim IME Escape before the command bar consumes it (#5481)#7245
dwu96 merged 1 commit into
mainfrom
fix/commandbar-ime-escape-5481

Conversation

@CrysisDeu

@CrysisDeu CrysisDeu commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Summary

The command bar overlay is one of the two handleEscape: false Escape consumers (issue #5481). Its dialog onKeyDown Escape branch called e.preventDefault() unconditionally before the scope-pop-or-close decision, so an Escape pressed to cancel an in-flight IME candidate list (Chinese/Japanese/Korean input) was consumed by the overlay — popping the search scope or closing the bar — instead of letting the IME cancel its candidates. Modal, the other consumer, was already guarded on main (#7006).

The fix is the one-branch edit the guard was designed for: if (!ime.claimKey(e)) return before the preventDefault(). The overlay already had const ime = useImeGuard() in scope with bindComposition() wired on the input; ime.claimKey owns the full decline contract (a declined composing Escape keeps its default action for the IME and is stopped from leaking to outer layers; an accepted Escape proceeds normally). The scope-pop-before-close ordering is untouched, as is useDialogFocusTrap and the deliberate handleEscape: false opt-out.

Also updates the stale comment at the useDialogFocusTrap call site, which still said Escape was left to the input's own handler — the dialog panel handler has owned it since Tab could reach the scope chip.

Tests

New CommandBarOverlay.imeEscape.test.tsx (5 tests), mirroring the coverage pattern of useImeGuard.documentLatch.test.tsx (#5505 lineage):

  • Escape during live composition is declined: default kept for the IME, propagation stopped (window bubble-listener assertion), bar stays open
  • A composing Escape does not pop the search scope
  • The WebKit post-compositionend grace window still declines (fake timers hold the 50ms window open, so the test cannot flake on a loaded runner)
  • Recovery: a non-composing Escape after the window closes the bar
  • Recovery inside a scope: the scope-pop-then-close ordering is unchanged

Mutation-verified: with the claim line removed, 4 of 5 tests fail.

Gates: npx tsc -b clean, full frontend vitest suite green (26,876 tests / 1,694 files), eslint 0 errors (the file's 2 warnings pre-exist on main verbatim), backend isort/flake8/mypy clean, black + brand gates pass. The new test file trips none of the ImeEnterClaimRatchet source-scan predicates.

Accepted residual (documented, not changed)

A composition abandoned with neither compositionend nor a blur leaves the latch set, and since the claim consumes what it declines, Escape would silently stop closing the bar until the latch recovers. bindComposition()'s blur reset is the recovery, but this input holds focus for the dialog's whole life, so blur may never fire on this surface; the backdrop click and the open/close chord remain as exits. This is the same cross-tree residual useImeGuard documents — called out here rather than papered over.

Why no screenshot: behavior-only keyboard fix — the Escape key's IME claim changes event handling, not any rendered pixel; there is no visual state to photograph.

Pattern harvest

The defect shape (a key branch that consumes-and-acts with no composition signal) is already ratcheted for Enter and Tab by website/src/test/ImeEnterClaimRatchet.test.ts, but none of its eleven predicates covers an Escape branch — this bug sat in a file that already imported the guard and wired bindComposition(), invisible to every scan.

Rule candidate: an Escape twin of the ratchet's Tab rules — flag an e.key === 'Escape' branch that calls preventDefault() and then acts (close/dismiss/scope change) with no claim(Synthetic)?Key( between the key check and the consumption. The issue enumerated exactly two handleEscape: false consumers and both are now guarded, so the ratchet rule is what keeps a third copy from growing unseen.

Closes #5481

@CrysisDeu
CrysisDeu requested a review from a team August 31, 2026 09:26
@CrysisDeu
CrysisDeu requested a review from a team as a code owner August 31, 2026 09:26
@CrysisDeu
CrysisDeu requested a review from cixuuz August 31, 2026 09:26
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Aug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

UX Review (Fable 5) — ✅ PASS

UX-level review of 1856b75a9a77c5c24987769e9c3d831983b346fb — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

UX-Verdict: PASS

Escape during IME composition now cancels the candidate list instead of killing the command bar — pure recovery of expected behavior for CJK typists, no new surface.

The fix reuses the same ime.claimKey latch already guarding Enter in this component, keeps the scope-pop-before-close ordering intact, and covers the WebKit post-composition grace window; no strings, layout, or affordances change.

[UX-REVIEWED] 1856b75

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Aug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — ✅ PASS

Design-level review of 1856b75a9a77c5c24987769e9c3d831983b346fb — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Design-Verdict: PASS

One-branch fix through the established claim contract (claimSyntheticKey owns both halves), mirroring the Modal precedent — right layer, no new surface, residual honestly documented.

[DESIGN-REVIEWED] 1856b75

@github-actions

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5) — ✅ PASS

Premise-level review of 1856b75a9a77c5c24987769e9c3d831983b346fb — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

All claims verified. The two handleEscape: false consumers (Modal.tsx:68, CommandBarOverlay.tsx:322) are the complete set; Modal was already guarded via useDocumentImeLatch, and this PR guards the second with the shared ime.claimKey mechanism already instantiated in the file (line 278) and wired via bindComposition() (line 1031). No new surface, no second spelling, zero unfixed siblings of the named cause.

First-Principles-Verdict: PASS

A reported IME defect fixed with one call into the existing shared guard, at the last unguarded consumer of the named cause — nothing rides along unjustified.

What this change ships

Intent: let CJK users cancel an IME candidate list with Escape without the command bar stealing the key — a FIX.

  1. Escape mid-composition now cancels IME candidates instead of popping scope/closing bar — justified (defect IME guard the two handleEscape=false Escape consumers (Modal, CommandBarOverlay) #5481, existing useImeGuard mechanism, one line)
  2. Stale comment at useDialogFocusTrap call site corrected to name the real Escape owner — declared rider, mandated by the code-style present-tense comment rule
  3. Five tests pinning the decline contract, grace window, and recovery — declared, mutation-verified per description

Counts run: handleEscape: false has exactly 2 non-test consumers (Modal.tsx:68 guarded upstream, CommandBarOverlay.tsx:322 fixed here) — 0 unfixed siblings of the stated cause. The fix adds no new symbol, key, or flag; it calls ime.claimKey, already constructed at CommandBarOverlay.tsx:278 with composition events bound at line 1031, so this is consumption of an existing mechanism, not a duplicate. The description's "accepted residual" (a latch stuck by an abandoned composition) is a documented property of the shared guard, not new behavior shipped here.

[FIRST-PRINCIPLES-REVIEWED] 1856b75

@github-actions

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed 1856b75a9a77c5c24987769e9c3d831983b346fb — this comment is updated in place on each push.

Review details

No findings.

[OPUS-REVIEWED] 1856b75

Verdict parsed from the review's SHA-scoped output markers for commit 1856b75a9a77c5c24987769e9c3d831983b346fb.

False positive or not applicable? A repository writer can comment:
/ai-review override fable 1856b75a9a77c5c24987769e9c3d831983b346fb: <one-sentence reason>

@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of 1856b75a9a77c5c24987769e9c3d831983b346fb and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] 1856b75

False positive or not applicable? A repository writer can comment:
/ai-review override gpt 1856b75a9a77c5c24987769e9c3d831983b346fb: <one-sentence reason>

@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention readiness: passed Eligible automated validation passed for the current revision and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Aug 31, 2026
@chenmingwei23
chenmingwei23 enabled auto-merge (squash) August 31, 2026 10:05

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

Tier 1 auto-approve: fix (2 files). Criteria: no conflict, no requested changes, security path denylist clean, design-doc gate clean, SAST annotations clean (Semgrep success 0 ann, CodeQL success no PR-scoped alerts), security checklist all-NO, AI reviewers green. Category: fix - claims IME Escape via ime.claimKey before the dialog's preventDefault so a composing Escape stays with the IME; adds a focused test file. Frontend keyboard-handling only.

@dwu96
dwu96 merged commit 56c30c4 into main Aug 31, 2026
121 of 127 checks passed
@dwu96
dwu96 deleted the fix/commandbar-ime-escape-5481 branch August 31, 2026 10:06

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

Tier 1 auto-approve: fix (2 files). Criteria: no conflict, no requested changes, security path denylist clean, design-doc gate clean, SAST annotations clean, security checklist all-NO, AI reviewers green. Category: one-branch IME claim guard on the command bar overlay's Escape handler plus a new test file — clear root cause (unconditional preventDefault ran before the composition claim), no behaviour change outside the Escape branch.

@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Aug 31, 2026

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

Tier 1 auto-approve: fix (2 files). Criteria: no conflict, no requested changes, security path denylist clean, design-doc gate clean, SAST annotations clean, security checklist all-NO, AI reviewers green. Category: the command bar dialog's Escape branch now runs the shared IME claim before its unconditional preventDefault, so an Escape pressed to cancel an in-flight IME candidate list keeps its default action for the IME instead of popping the search scope or closing the bar; one production hunk in CommandBarOverlay.tsx plus a new test file, no other behaviour touched.

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.

IME guard the two handleEscape=false Escape consumers (Modal, CommandBarOverlay)

4 participants