Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

fix(sheet): make presentation sheets honour the soft keyboard on Android - #483

#483
Open
BPHvZ wants to merge 1 commit into
skiptools:mainskiptools/skip-ui:mainfrom
BPHvZ:fix/sheet-ime-insetsBPHvZ/skip-ui:fix/sheet-ime-insetsCopy head branch name to clipboard
Open

fix(sheet): make presentation sheets honour the soft keyboard on Android#483
BPHvZ wants to merge 1 commit into
skiptools:mainskiptools/skip-ui:mainfrom
BPHvZ:fix/sheet-ime-insetsBPHvZ/skip-ui:fix/sheet-ime-insetsCopy head branch name to clipboard

Conversation

@BPHvZ

@BPHvZ BPHvZ commented Jul 21, 2026

Copy link
Copy Markdown

A .medium presentation sheet containing a TextField is unusable on Android: the soft keyboard covers the field, so you cannot see what you are typing.

Cause — structural, not a missing inset. The sheet cannot react to the IME at all:

  • contentWindowInsets is zeroed on the ModalBottomSheet.
  • PresentationRoot applies imePadding() only when .bottom survives in systemBarEdges, which it does not for a non-edge-to-edge detent sheet.
  • .medium is implemented as "content pinned to the bottom half of the screen", and the keyboard covers approximately that same half.

Measured on a 2424 px screen: ~330 px of usable area above the keyboard.

There is no app-level workaround. presentationDetents(_:selection:) is a no-op on Android, and applying imePadding() to the sheet's content only squeezes it inside a fixed-height sheet — measured on-device, that pushed the field out of the accessibility tree entirely, so it could not even be selected by a UI test.

The change. Shift the content area instead of padding it:

  • Shrink the detent's top spacer by the IME height and grow the bottom spacer by the same amount, so the content area keeps its exact height and simply translates clear of the keyboard.
  • Track the adjusted topInset, because it also drives the sheet's rounded-top clip shape — otherwise the clip detaches from the visible top edge.

It also fixes a second bug in the same function. The bottom spacer computed:

max(0.dp, WindowInsets.systemBars - WindowInsets.ime)

which clamps to 0 the moment the keyboard opens, actively letting content run underneath it. Taking max(systemBars, ime) instead preserves the resting behaviour (IME closed ⇒ system bars) and clears the keyboard when open.

Before / after. Same app, same Release configuration, same device, same docked keyboard — differing only by this commit. The "before" build is upstream 1.59.0 (9c39113), this branch's exact parent commit, so the patch is the only variable.

Before — upstream 1.59.0. The sheet is crushed into the strip above the keyboard: the "Sign in" title sits on the bottom edge, the email field is almost entirely behind the keyboard, and the typed text is not visible at all.

Before: upstream 1.59.0 — the medium sheet is compressed into the strip above the soft keyboard, its title on the bottom edge and the email field almost entirely hidden

After — this branch. The field and the "Send me a link" button sit well clear of the keyboard, and the typed text is visible.

After: with this patch — the same sheet shifted clear of the keyboard, with the email field and the send button fully visible

Verification.

  • Keyboard up: the field sits ~600 px clear of the keyboard and stays in the accessibility tree (focused=true); typing lands.
  • Keyboard down: resting bounds are byte-identical to before the patch, confirmed by pixel-diffing the sheet surface — the only differences are a 5×53 px caret and the status-bar clock. Sheets do not become full-screen.
  • Exercised on four keyboard-bearing sheets in a shipping app (email sign-in, and a shared option editor behind DJ message / entry PIN / minimum tip).

⚠️ Reproduction caveat — hw.keyboard. An AVD with hw.keyboard=yes (the AVD default) hides this bug completely. Gboard then runs in physical-keyboard mode and shows a floating toolbar that contributes no bottom insets, so the field looks fine. Reproduce with hw.keyboard=no, which matches real device behaviour. Without this note the report would reasonably be closed as unreproducible.

Notes on the checklist below. iOS is left unchecked because the change is inside the Compose-only #if SKIP layer of Presentation.swift and touches no iOS code path. swift test on this branch runs 74 tests (10 skipped) with 1 failure, LayoutTests.testDrawTextDefaultFont; that failure is pre-existing and unrelated — checking out the parent commit 9c39113 and running the same filter fails identically, and nothing here touches text drawing. For Skip Fuse UI: no companion PR appears to be needed, because skip-fuse-ui consumes this code as a dependency (.package(url: "…/skip-ui.git", from: "1.59.0"), with SkipSwiftUI depending on .product(name: "SkipUI", package: "skip-ui")), so the fix propagates transitively, and this patch changes no API surface — only internal layout arithmetic. Happy to add a Showcase example if you would like the regression covered there; reproducing it needs a .medium sheet containing a TextField and an AVD with hw.keyboard=no.

Skip Pull Request Checklist:

  • REQUIRED: I have signed the Contributor Agreement
  • REQUIRED: I have tested my change locally with swift test
  • OPTIONAL: I have tested my change on an iOS simulator or device
  • OPTIONAL: I have tested my change on an Android emulator or device
  • REQUIRED: I have checked whether this change requires a corresponding update in the Skip Fuse UI repository (link related PR if applicable)
  • OPTIONAL: I have added an example of any UI changes to the Showcase sample app

  • AI was used to generate or assist with generating this PR. Please specify below how you used AI to help you, and what steps you have taken to manually verify the changes.

The diagnosis, the patch and this description were produced with AI assistance (Claude). Every claim above was checked against the source or on a device rather than accepted from the model:

  • The root cause is stated in terms of specific code in Presentation.swift (contentWindowInsets, PresentationRoot's systemBarEdges gate, the .medium detent arithmetic), read directly in the SkipUI source rather than inferred.
  • The rejected alternative (content-level imePadding()) was actually implemented, measured on-device — it pushed the field out of the accessibility tree — and then reverted. It is described as rejected because it was tried, not because it sounded wrong.
  • The before/after screenshots are two real builds of the same app differing only by this commit, driven to the same state on the same emulator with a docked keyboard.
  • "No regression when the keyboard is closed" is a pixel-diff of the resting sheet, not an impression: the surface is byte-identical apart from a caret and the clock.
  • swift test was run on the branch, and its single failure was proven pre-existing by re-running that test against unmodified upstream 1.59.0.

A `.medium` sheet containing a text field was unusable on Android: the keyboard
covered the field, so you could not see what you were typing.

Cause is structural rather than a missing inset. The sheet cannot react to the
IME at all — `contentWindowInsets` is zeroed on the `ModalBottomSheet`, and
`PresentationRoot` only applies `imePadding()` when `.bottom` survives in
`systemBarEdges`, which it does not for a non-edge-to-edge detent sheet. Since
`.medium` is implemented as "content pinned to the bottom half of the screen",
and the keyboard covers roughly that same half, the usable area collapsed to a
few hundred pixels.

Padding the content is not a fix: inside a fixed-height sheet it only squeezes
the content and clips it (measured on-device — the field was pushed out of the
accessibility tree entirely, so it could not even be selected by a UI test).

Instead shift the content area: shrink the detent's top spacer by the IME
height and grow the bottom spacer by the same amount. The content area keeps
its exact height and translates clear of the keyboard. `topInset` tracks the
adjusted value too, since it drives the sheet's rounded-top clip shape.

Also fixes the bottom spacer, which computed `max(0, systemBars - ime)`. That
clamps to zero the moment the keyboard opens — actively letting content run
underneath it. `max(systemBars, ime)` keeps the resting behaviour and clears
the keyboard when open.

Both spacers collapse to their previous values when the keyboard is closed, so
the resting layout is unchanged — verified on-device: with the keyboard down
the field bounds are byte-identical to before the patch.

Forked from upstream 1.59.0.
@cla-bot cla-bot Bot added the cla-signed label Jul 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Morty Proxy This is a proxified and sanitized view of the page, visit original site.