Skip to content

android: Hide the keyboard on Keyboard.dismiss() after the field that showed it is gone - #714

Open
jt-wang wants to merge 5 commits into
longbridge:mainfrom
jt-wang:android-keyboard-dismiss
Open

jt-wang wants to merge 5 commits into
longbridge:mainfrom
jt-wang:android-keyboard-dismiss

Conversation

@jt-wang

@jt-wang jt-wang commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Description

On Android, Keyboard.dismiss() leaves the keyboard up once the field that showed it has been removed. That happens whenever a screen changes after typing: a sign-in or search screen submits, the app moves to the next screen and calls Keyboard.dismiss(), and the next screen opens under the keyboard. The docs say Keyboard.dismiss() "hides the software keyboard" (website/modules/nsh.md).

The cause is in Mounter.command, in the blur command on the root view (id 1): it hides the keyboard only when host.findFocus() returns a view inside the host. After the focused EditText is removed, nothing in the host has focus, so the call does nothing, while Android keeps the keyboard shown for the window.

The fix hides it with the focused view's window token, or the host's when nothing in the host is focused. In that second case it leaves the keyboard up when the window's focused view is an editor outside the host that the input method serves (an embedding app's EditText, another NshView's input), since they share the window token; iOS likewise ends editing only in the root view and its modals, not in the window (nsh-platform-ios/src/mount.rs, read, not run). It still moves focus back to the host, and only when a child had it. The change is 14 lines in Mounter.java, plus a regression test (examples/gallery/tests/keyboard-dismiss.test.tsx and its fixture). No API change, no new dependency.

How to Test

Before and after on an Android 15 emulator

The fixture has an Input and a "Next" button. "Next" removes the Input, as a screen change does, and calls Keyboard.dismiss() 100 ms later; a line shows keyboardFrame(). The test types into the field, presses "Next" and waits for "keyboard: down":

cd examples/gallery
nsh test tests/keyboard-dismiss.test.tsx --target android   # an Android device or emulator
nsh test tests/keyboard-dismiss.test.tsx                    # headless
On 19216f7 (main at 201d780 merged in) --target android (Android 15 arm64 emulator) headless
Without the fix fails: `text:keyboard: down` showing never happened in 5000 ms; on screen: ["Next screen", "Next", "keyboard: up"] passes
With the fix passes passes

The embedding case, a native EditText above the NshView in the same Activity that has the keyboard when the app calls Keyboard.dismiss(), was checked on the same emulator with a local-only change to DevActivity (not in this PR: the test host's window holds only the NshView, and a fixture cannot add a native view outside it). Without the fix in the second commit the keyboard goes down (logcat: a client HIDE_SOFT_INPUT at the dismiss); with it the keyboard stays up.

CI runs no --target android job, so the Android row is a command for the reviewer to run. The runtime was built with cargo xtask android --release --dev --abi arm64-v8a.

Not tested: iOS, which takes a separate path; two NshViews in one Activity; a physical device; armeabi-v7a and x86_64.

Checklist

  • I have read the CONTRIBUTING document and followed the guidelines.
  • Reviewed the changes in this PR and confirmed AI generated code (if any) is accurate.
  • Passed cargo test --workspace, cargo +nightly fmt --all and clippy (including the Catalyst and Android runs for platform code). (cargo test --workspace on dde7f77 with this commit and the four other Android fixes of this series: 3260 passed, 0 failed, 43 ignored. cargo xtask clippy-android passes on main at 201d780, and this PR changes no Rust. The Java is compiled by cargo xtask android, and there is no Rust for fmt to check)
  • Changes to what apps use keep what released versions shipped and come with a case in tests/compat/ (cargo xtask compat). (No API change.)
  • Ran the related examples/gallery demos on iOS and Android (if the change is visible or platform-specific). (The test above, on Android.)

@huacnlee huacnlee left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Static review; not reproduced on an Android device.

Reviewed by ChatGPT on behalf of @huacnlee.

host.requestFocus();
}
InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(f != null ? f.getWindowToken() : host.getWindowToken(), 0);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P2] Preserve keyboard ownership when falling back to the host token

If a sibling native EditText or another NshView owns focus in the same Activity, host.findFocus() is null but both share the window token, so this fallback dismisses that other editor's keyboard. Check for a focused editor outside this surface before using the fallback, and cover that embedding case alongside removal of this surface's input.

Reviewed by ChatGPT on behalf of @huacnlee.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Right: hideSoftInputFromWindow hides the keyboard of whichever view the input method serves in that window, so a native EditText next to the NshView lost its keyboard too. Fixed in febe40e: when nothing in the host has focus, the keyboard is hidden only if the window's focused view isn't an editor outside the host that the input method serves (onCheckIsTextEditor() and imm.isActive()). This matches iOS, which ends editing in the root view and its modals, not in the window.

Tested on an Android 15 arm64 emulator with a local change to DevActivity that puts a focused native EditText above the NshView, plus a fixture that calls Keyboard.dismiss() once that keyboard is up. Before the fix, the keyboard goes down. With the fix, it stays up. keyboard-dismiss.test.tsx still passes on Android and headless. I didn't commit that case as a gallery test because the test host's window holds only the NshView, and a fixture can't add a native view outside it. If you want it covered, I can add a test-only hook for that.

… nothing in the host has focus

The fallback that hides the keyboard of a removed field used the host's window token, which an
embedding app's EditText or another NshView in the same Activity shares, so it hid their keyboard
too. It now leaves the keyboard alone when the window's focused view is an editor outside the host
that the input method serves.
@jt-wang
jt-wang requested a review from huacnlee October 30, 2026 16:26

This branch has not been deployed

No deployments
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.

2 participants