Skip to content

fix(BottomSheet): guard standalone Escape against IME composition - #5322

Open
AKnassa wants to merge 2 commits into
facebook:mainfrom
AKnassa:rocky/issue-5302-bottomsheet-ime
Open

fix(BottomSheet): guard standalone Escape against IME composition#5322
AKnassa wants to merge 2 commits into
facebook:mainfrom
AKnassa:rocky/issue-5302-bottomsheet-ime

Conversation

@AKnassa

@AKnassa AKnassa commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

What this does

A bottom sheet no longer closes when someone typing Korean, Japanese or Chinese presses Escape to back out of a half-typed word.

Why

While an IME is composing, the browser sends an Escape keydown to cancel the pending characters, and it sends it before compositionend. The standalone sheet read that as a plain "close me" command, so a CJK user correcting a word inside a sheet lost the whole sheet, along with anything already typed into it. The documented purpose="form" and height="tall" mobile-keyboard cases both put a text field in a standalone sheet, so this is an ordinary path rather than a corner.

The base Dialog already guards this, and so does the sibling BottomSheetSwitcher. Only the standalone sheet was missing it.

What changed

  • The sheet's Escape handler now ignores the keydown that belongs to an in-progress composition, via the shared isImeKeyEvent check the rest of the library uses.
  • It claims the Escape before deciding what the key means. Left unclaimed, the browser raises its own close request, which arrives at the sheet's onCancel and closes it on the very same keypress. The guard on its own is not enough.
  • handleCancel is unchanged, and gains a comment saying why: cancel is a plain Event carrying no composition state, so a guard there could never fire.
  • Five tests, and a changeset.

Evidence

This is a bug fix, so the bar is red before and green after.

The tests are red at fba7b4009 (the branch base) and green after the fix. Reverting handleKeyDown to its previous shape turns four of the five red on their behaviour assertion (onOpenChange called once where it must not be called at all), and hoisting preventDefault above the key check turns the fifth red on its own. No test here passes vacuously, and the two halves of the fix are pinned separately.

Covered: both IME signals (isComposing, and the legacy keyCode 229); a composing Escape raised on a focused field and bubbling to the <dialog>; the release case, where the Escape after a cancelled composition still dismisses; the non-modal sheet, where Escape is the only route out; and Enter and ArrowDown mid-composition staying unclaimed, since the IME owns those keys.

Local: pnpm lint:strict clean, core typecheck clean, pnpm build green, BottomSheet suite 77/77, full suite 11289/11292. The three remaining failures are pre-existing and not from this branch: two cli tests that assume a case-sensitive filesystem, and one DateInputTouch test that times out at 5s only under load (127/127 in isolation, and packages/core/src/DateInput/ is byte-identical to main on this branch).

No screenshots: nothing rendered changes, and a real IME composition cannot be driven from browser automation. A synthetic KeyboardEvent is untrusted, so it would not exercise the close-request path either.

Rubric

Triage: bug fix · non-breaking · low blast radius → fast path · checks: §1 A1-A7/A11-A12, §7 C1-C8/C11, Q1-Q12

Component Audit Rubric v1.6, check §9 I19. §1: Escape stays operable; no ARIA, focus or live region touched. §7: no Effect, state, ref or observer added, and the shared isImeKeyEvent is composed rather than reimplemented. The component is not audited around the fix.

One note for the ledger row: the issue asks for the guard in both handleKeyDown and handleCancel. Only the first is implementable, for the reason above. The preventDefault hoist is what actually closes the cancel path.

Fixes #5302

The standalone sheet's Escape handler read a bare `event.key ===
'Escape'`, so the keydown an IME fires to cancel an in-progress
composition — which arrives before `compositionend` — dismissed the
sheet instead. A CJK user composing in a `purpose="form"` field, or in
the mobile-keyboard `height="tall"` case, lost the whole sheet on the
keystroke that was only meant to cancel a candidate.

`handleKeyDown` now early-returns on `isImeKeyEvent`, the guard `Dialog`
and the sibling `BottomSheetSwitcher` already carry. It claims the key
with `preventDefault` first, before deciding what the key means: an
unclaimed Escape lets the browser raise its own close request, which
arrives at `handleCancel` and dismisses on the same keypress.

`handleCancel` itself needs no guard, and gets a comment saying why —
`cancel` is a plain Event with no composition state to read, so an
`isImeKeyEvent` call there could never return true.

Two regression tests, mirroring the switcher's: both IME signals
(`isComposing`, and the legacy keyCode 229) leave `onOpenChange`
untouched, and the composing Escape is claimed. Both were red before
the fix; a mutation to the naive early-return-before-preventDefault
shape kills the second one on its own.

Component Audit Rubric v1.6 §9 I19.

Fixes facebook#5302
Four behaviors the first pass left unpinned, each proven to catch a real
regression rather than passing by accident:

- A composing Escape raised on a focused field inside a `purpose="form"`
  sheet — the exact path the report names — has to survive the bubble to
  the `<dialog>` and leave the sheet open.
- The guard releases: the Escape that follows a cancelled composition
  still dismisses. A guard that over-blocks would trap the user in the
  sheet, which is a worse bug than the one being fixed.
- A non-modal sheet (`hasScrim={false}`) ignores it too. There is no
  native close request behind that one, so `handleKeyDown` is the whole
  dismissal route and the only thing standing in the way.
- Enter and ArrowDown mid-composition are left unclaimed. They commit a
  candidate and walk the candidate window; claiming them would take keys
  the IME owns.

Red proofs, since the fix already landed in the previous commit. Reverting
`handleKeyDown` to its pre-fix unguarded shape turns the first three red on
their behavior assertions (`onOpenChange` called once, when it must not be
at all) and leaves the fourth green. Hoisting `preventDefault` above the
`key !== 'Escape'` check turns only the fourth red. So no test here passes
vacuously, and the two halves of the fix are pinned separately.

BottomSheet suite 77/77.

Part of facebook#5302
@vercel

vercel Bot commented Aug 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
astryx Ready Ready Preview Aug 22, 2026 3:41am

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Aug 22, 2026
@github-actions github-actions Bot added community Authored by a community contributor (not on the eng/design team) needs:code-review High-risk change (new package/component/API) — needs human code review before merge labels Aug 22, 2026
@AKnassa
AKnassa marked this pull request as ready for review August 22, 2026 03:48
@github-actions

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

🧪 Sandbox Preview

View Sandbox for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

Modified Components

BottomSheet (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 3142 -
Complexity N/A Very High (250) -

Bundle Size Summary

Package Size (ESM) Size (CJS) Gzipped
@astryxdesign/core N/A 4.8KB 1.2KB

Accessibility Audit

Status: No accessibility violations detected.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

github-actions Bot added a commit that referenced this pull request Aug 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot. community Authored by a community contributor (not on the eng/design team) needs:code-review High-risk change (new package/component/API) — needs human code review before merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[audit] BottomSheet: standalone Escape lacks an IME composition guard (I19)

1 participant