Skip to content

fix(demo-wallet): mobile layout, safe-area, viewport height, bottom-sheet keyboard/scroll#487

Open
lesyuk wants to merge 2 commits into
mainfrom
fix/mobile-ui
Open

fix(demo-wallet): mobile layout, safe-area, viewport height, bottom-sheet keyboard/scroll#487
lesyuk wants to merge 2 commits into
mainfrom
fix/mobile-ui

Conversation

@lesyuk

@lesyuk lesyuk commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Mobile-web layout, safe-area, viewport-height and bottom-sheet keyboard/scroll fixes for the wallet demo. Every change is behind a mobile breakpoint or an opt-in prop; desktop and the extension popup are unchanged.

Motivation / Bug

On-device testing (iOS Safari + Android Chrome) surfaced several mobile-only issues:

  • Clipped bottom actions. Continue / Reset Wallet / the welcome terms line sat under the Android 3-button navigation bar. The shared CenteredScreen footer used a fixed pb-6; Android 3-button mode reports env(safe-area-inset-bottom)=0 while its ~48dp bar still overlaps the viewport, and env() didn't resolve at all without viewport-fit=cover.
  • Short viewport on iOS. The dashboard used 100vh, which excludes iOS Safari's dynamic toolbars, leaving a gap.
  • Top nav / back button vanished under sheets. Opening a bottom sheet (TON Connect request, wallet switcher, settings) over a scrolled page made the sticky top bar drop out for the sheet's lifetime and snap back late. Root cause: vaul's iOS scroll-lock sets body { position: fixed; top: -scrollY }; a position: sticky header is anchored to its scroll container, and a fixed <body> is no longer that container, so the header detaches.
  • Fluctuating top gap. The header's top pad used env(safe-area-inset-top), which in a browser tab (not a standalone PWA) tracks the address bar showing/hiding — a large, moving gap on both platforms.
  • On-screen Return key did nothing on the password screens (no <form>, so iOS Safari's Return never submitted).
  • Connect-to-dApp sheet keyboard misbehavior. vaul's repositionInputs mutates the sheet height/offset from visualViewport resize events: the sheet flew off-screen on the first iOS focus and got shoved up on Android; the dashboard could also bleed through above it.

What changed

  • Safe-area & viewportviewport-fit=cover in index.html; the CenteredScreen footer reserves max(3rem, 1.5rem + env(safe-area-inset-bottom)); the dashboard layout switched to min-h-dvh.
  • PinnedHeader (apps/demo-wallet/src/core/components/shared/pinned-header/) — new top bar: position: fixed on mobile with a ResizeObserver-measured flow spacer, md:sticky on desktop; a small fixed top pad instead of the top inset. NewLayout and the onboarding back button use it.
  • Return key (setup-password-screen, unlock-screen) — inputs wrapped in a <form> whose onSubmit mirrors the primary button (same validity guard), enterKeyHint set, plus a visually-hidden submit button (what actually makes iOS Safari's Return submit).
  • Connect-to-dApp sheet (modal.tsx, connect-dapp-modal.tsx, is-ios.ts) — a keyboardSafe opt-in: disable vaul repositionInputs on both platforms and lift the compact sheet with a manual visualViewport-based bottom offset (useKeyboardInset) so the input and Connect button rest directly above the keyboard. Input-less sheets set noBodyStyles so the scroll-lock can't break the now-fixed header; the iOS input sheet keeps the lock and restores scroll on close.

Why this approach

  • Fixed vs sticky header: a viewport-anchored (fixed) header is the only thing immune to vaul's position:fixed body lock; sticky provably detaches. Desktop keeps sticky because Radix Dialog locks scroll via overflow:hidden (not a fixed body), so the problem doesn't occur there and we avoid a fixed-vs-flow reflow on the wide layout.
  • Manual keyboard lift over vaul's repositionInputs: repositionInputs is what caused the fly-off/shove; a bottom offset is orthogonal to vaul's transform-based open/close/drag, so the two don't fight, and the resize stream supplies smooth intermediate heights.
  • Hidden submit button: the minimal, standards-based way to get iOS Safari's Return to submit without duplicating submit logic.
  • Fixed top pad over env(safe-area-inset-top): that inset is address-bar-coupled in a browser tab, which is exactly the fluctuation observed.

How it was verified

  • pnpm --filter demo-wallet typecheck (tsc, clean).
  • Manual verification on desktop (macOS, Chrome) and on the same iOS Safari / Android Chrome devices where the issues were originally reported.

Scope / safety

Every change is mobile-only (breakpoint- or visualViewport/env()-guarded) or opt-in (keyboardSafe). The desktop Dialog path and the extension popup entry are untouched. No store or business logic changed.

Follow-ups

Related: #485 (demo-wallet e2e). It should be rebased on top of these fixes before it merges.

…heet keyboard/scroll

A set of linked mobile-web (iOS Safari + Android Chrome) layout and bottom-sheet
fixes for the wallet demo. Desktop and the extension popup are unchanged.

Safe-area & viewport:
- Add viewport-fit=cover so env(safe-area-inset-*) resolves to real values on
  mobile (0 on desktop, no visual change).
- Reserve bottom clearance for the pinned actions in the onboarding/auth layout
  (CenteredScreen): max(3rem, 1.5rem + env(safe-area-inset-bottom)). The inset
  alone is 0 under Android 3-button navigation while its ~48dp bar still overlaps
  the viewport, which clipped the Continue / Reset Wallet / terms line; the base
  clears that bar while gesture/iOS/desktop keep a normal margin.
- Use min-h-dvh (was 100vh) for the dashboard layout so it fills the viewport on
  iOS Safari, whose dynamic toolbars are excluded from 100vh. dvh degrades to vh
  where unsupported, so desktop/Android are unchanged.

Pinned top bar (new PinnedHeader):
- Pin the top nav so its controls (back button on sub-screens; Connect-to-dApp,
  wallet switcher, settings on the dashboard) stay reachable while the page
  scrolls or the keyboard is open. Also pin the onboarding back button.
- On mobile the header is position:fixed (not sticky) with a measured flow
  spacer: when a vaul bottom sheet opens on iOS Safari it locks the body with
  position:fixed; top:-scrollY, which detaches a sticky header (it drops out for
  the sheet's lifetime and snaps back late). A viewport-anchored fixed header is
  immune and stays painted, dimmed under the overlay. Desktop keeps sticky (Radix
  Dialog locks scroll via overflow:hidden, so sticky is fine and avoids a reflow).
- Top padding is a small fixed value, not env(safe-area-inset-top): in a browser
  tab that inset tracks the address bar showing/hiding, producing a large
  fluctuating gap above the bar on both platforms.

On-screen Return key (setup-password / unlock):
- Wrap the password inputs in a <form> whose onSubmit mirrors the Continue/Unlock
  button (guarded by the same validity condition, so invalid input is a no-op)
  and set enterKeyHint for the right on-screen key label. A visually-hidden submit
  button is what makes iOS Safari's Return key fire submission; the visible
  buttons keep their existing handlers, no submit logic duplicated.

Connect-to-dApp sheet keyboard handling (the only sheet with a text input):
- vaul's repositionInputs mutates the sheet height/offset from visualViewport
  resize events and misfired: fly-off on the first iOS focus, shove-up on Android.
  Disable it for this sheet on both platforms and drive the keyboard lift manually
  via a visualViewport-based bottom offset (useKeyboardInset), so the compact,
  content-height sheet -- input and Connect button -- rests directly above the
  keyboard. bottom is orthogonal to vaul's transform, so there is no fight.
- Standard (input-less) sheets set noBodyStyles so vaul's iOS body scroll-lock
  doesn't break the now-fixed header; the iOS input sheet keeps the lock (so
  focusing the textarea can't scroll the document and drag the fixed sheet away)
  and restores the page scroll offset on close.

Scoped via a keyboardSafe opt-in on the input sheet; other sheets and the desktop
Dialog are unaffected.
@vercel

vercel Bot commented Jul 4, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
appkit-minter Ready Ready Preview, Comment Jul 4, 2026 4:30pm
appkit-template Ready Ready Preview, Comment Jul 4, 2026 4:30pm
kit-demo-wallet Ready Ready Preview, Comment Jul 4, 2026 4:30pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@lesyuk, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 49 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6814b5fa-0a4b-4a5a-9ac3-2638d470e019

📥 Commits

Reviewing files that changed from the base of the PR and between b2686a6 and 0bbcb39.

📒 Files selected for processing (10)
  • apps/demo-wallet/index.html
  • apps/demo-wallet/src/core/components/shared/centered-screen/centered-screen.tsx
  • apps/demo-wallet/src/core/components/shared/new-layout/new-layout.tsx
  • apps/demo-wallet/src/core/components/shared/pinned-header/index.ts
  • apps/demo-wallet/src/core/components/shared/pinned-header/pinned-header.tsx
  • apps/demo-wallet/src/core/components/ui/modal/modal.tsx
  • apps/demo-wallet/src/core/lib/is-ios.ts
  • apps/demo-wallet/src/features/auth/components/setup-password-screen/setup-password-screen.tsx
  • apps/demo-wallet/src/features/auth/components/unlock-screen/unlock-screen.tsx
  • apps/demo-wallet/src/features/ton-connect/components/connect-dapp-modal/connect-dapp-modal.tsx
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/mobile-ui

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

prettier/prettier fixes flagged by CI lint (formatting only, no behavior change):
modal.tsx keyboard-inset ternary and connect-dapp-modal Modal.Container props.
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.

1 participant