From e37ca3dc4cb7033adf03af4ba214167a6898da05 Mon Sep 17 00:00:00 2001 From: MartinBraquet Date: Thu, 30 Jul 2026 21:04:38 +0200 Subject: [PATCH] Handle keyboard visibility in mobile browsers via visual viewport resizing; improve cross-platform behavior in `_app.tsx`. --- web/hooks/use-hide-bottom-nav-on-keyboard.ts | 3 +- web/pages/_app.tsx | 33 +++++++++++++++++--- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/web/hooks/use-hide-bottom-nav-on-keyboard.ts b/web/hooks/use-hide-bottom-nav-on-keyboard.ts index 2c898829..8a7f78f2 100644 --- a/web/hooks/use-hide-bottom-nav-on-keyboard.ts +++ b/web/hooks/use-hide-bottom-nav-on-keyboard.ts @@ -9,7 +9,8 @@ import {useEffect} from 'react' * for navigation nobody is using mid-message. * * The keyboard state itself is tracked in `_app.tsx`, which toggles `keyboard-open` on `` from - * the Capacitor keyboard events; the rules live in `globals.css` so showing/hiding costs no re-render. + * the Capacitor keyboard events in the app, and from the visual viewport shrinking in a mobile + * browser; the rules live in `globals.css` so showing/hiding costs no re-render. * Zeroing `--bnv` there also reclaims the bar's height for the page below it. */ export const useHideBottomNavOnKeyboard = () => { diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx index 0fed0b0d..60cfe6ab 100644 --- a/web/pages/_app.tsx +++ b/web/pages/_app.tsx @@ -140,15 +140,40 @@ function MyApp(props: AppProps) { useEffect(() => { debug('isAndroidWebView app:', isAndroidApp()) - if (!Capacitor.isNativePlatform()) return const onShow = () => document.body.classList.add('keyboard-open') const onHide = () => document.body.classList.remove('keyboard-open') - Keyboard.addListener('keyboardWillShow', onShow) - Keyboard.addListener('keyboardWillHide', onHide) + if (Capacitor.isNativePlatform()) { + Keyboard.addListener('keyboardWillShow', onShow) + Keyboard.addListener('keyboardWillHide', onHide) + + return () => { + Keyboard.removeAllListeners() + } + } + + // Mobile browsers have no keyboard events, so infer the keyboard from the visual viewport: it + // shrinks by the keyboard's height while the layout viewport (window.innerHeight) stays put. + // Without this, `keyboard-open` never fires outside the app and the page keeps reserving the + // bottom nav's height, leaving a gap between the composer and the keyboard. + const vv = window.visualViewport + if (!vv) return + // Comfortably above the ~60px the collapsing URL bar accounts for, well below any keyboard. + const KEYBOARD_MIN_PX = 150 + const update = () => { + const hidden = window.innerHeight - (vv.height + vv.offsetTop) + if (hidden > KEYBOARD_MIN_PX) onShow() + else onHide() + } + + update() + vv.addEventListener('resize', update) + vv.addEventListener('scroll', update) return () => { - Keyboard.removeAllListeners() + vv.removeEventListener('resize', update) + vv.removeEventListener('scroll', update) + onHide() } }, [])