fix(mobile): start the iOS keyboard-dismissing drag clear of controls
On an iPhone, the keyboard has no dismiss key, so device.dismissKeyboard() sends it away with a short drag. That drag started at the exact centre of the screen and travelled 3% of the screen width, which is still within touch slop. If a control sat at the centre, the drag pressed it. In the user report it selected a different filter button; in the benchmark repro below it checks a checkbox. When a control took the touch, the keyboard also stayed up, because the scroll view never got the tap.
Fixes ENG-910.
- Each drag now starts at the point nearest the centre that no control covers, with 8 pt of clearance (
packages/mobile/src/keyboard.ts). - When the screen has a scroll view, the start must be inside one: a scroll view is what turns the drag into a dismissal, and a drag anywhere else does nothing.
- A control is a node with a control role (bars included), or a leaf that isn't plain content. A React Native
Pressablewith no role shows up as an unnamed leaf, while text and images don't count. - A leaf over half the screen counts as a flattened tree's merged content, not a control. Those screens drag at the centre as before, and the committed iOS recording for Flattened Registration Form depends on that.
- The start point is picked again from the latest capture before each drag (horizontal, then vertical).
- A screen with no clear point fails with the existing
UNSUPPORTED_CAPABILITYinstead of touching a control. - New benchmark test on Choice Controls (
apps/mobile-benchmark/tests/device.e2e.ts): it sets a valid order, fills the notes field, dismisses the keyboard, and places the order. A toggled control would leave Place order disabled.
Known gaps, written up in the JSDoc: a Text with its own onPress reads as plain text, and a control inside a flattened tree can't be seen. Both get the same drag main makes today.
Verified
Ran it locally: yes
- mobile benchmark on an iPhone 17 Pro simulator (iOS 26.5),
e2e run tests/device.e2e.ts --target ios-simulator --grep "choice controls" --video - main: both drags start at (201, 437), on the Mushrooms checkbox. It gets checked, the keyboard stays up, and the test fails:
× dismissing the keyboard over choice controls leaves them as they were 25.91s UNSUPPORTED_CAPABILITY: keyboard dismissal failed: the keyboard has no dismiss key and stayed up through a horizontal and a vertical drag at the centre of the screen. - this branch: one horizontal drag from (201, 517), in the gap between "Rush delivery" and its switch, dismisses the keyboard and changes no control:
✓ dismissing the keyboard over choice controls leaves them as they were 16.88s - whole iOS deterministic suite on this branch:
Tests 56 passed | 1 skipped (57), including Sequential Onboarding's dismissals - iOS agent replay,
--config e2e.agent.config.ts --grep "Flattened Registration Form":replayed all 11 recorded actions, no model call,dismissKeyboardincluded. The run had no model key, so only the judgment afterwards couldn't run. - the start point on every committed golden snapshot (iOS and Android): flattened screens and text-heavy scroll views start at the centre as before, Choice Controls steers clear of its checkbox, and the benchmark home list (all rows) is refused
- Android not run: it presses the keyboard's own dismiss key and never reaches this path
| main | this branch |
|---|---|
The videos below show main first, then this branch.
Changes made by Claude Opus 5.5 in Claude Code (T3 Code).
https://github.com/user-attachments/assets/4fcc3f62-05f9-424d-bdb8-584be75b1ef3
https://github.com/user-attachments/assets/c243f156-3493-4e5c-881c-fd9c1802d759