mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
fix(web): prevent mobile keyboard from covering chat input (#403)
This commit is contained in:
@@ -9,6 +9,7 @@ import { useServerUrl } from '@/hooks/useServerUrl'
|
||||
import { useSSE } from '@/hooks/useSSE'
|
||||
import { useSyncingState } from '@/hooks/useSyncingState'
|
||||
import { usePushNotifications } from '@/hooks/usePushNotifications'
|
||||
import { useViewportHeight } from '@/hooks/useViewportHeight'
|
||||
import { useVisibilityReporter } from '@/hooks/useVisibilityReporter'
|
||||
import { queryKeys } from '@/lib/query-keys'
|
||||
import { AppContextProvider } from '@/lib/app-context'
|
||||
@@ -58,6 +59,9 @@ function AppInner() {
|
||||
initializeTheme()
|
||||
}, [])
|
||||
|
||||
// Track visual viewport height for mobile keyboard avoidance (see useViewportHeight.ts)
|
||||
useViewportHeight()
|
||||
|
||||
useEffect(() => {
|
||||
const preventDefault = (event: Event) => {
|
||||
event.preventDefault()
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Unit tests for the useViewportHeight hook logic.
|
||||
*
|
||||
* Because the hook depends on window.visualViewport (not available in jsdom),
|
||||
* we test the core update logic directly rather than rendering the hook.
|
||||
*/
|
||||
describe('useViewportHeight update logic', () => {
|
||||
const root = document.documentElement
|
||||
|
||||
beforeEach(() => {
|
||||
root.style.removeProperty('--app-viewport-height')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
root.style.removeProperty('--app-viewport-height')
|
||||
})
|
||||
|
||||
it('sets --app-viewport-height when visual viewport is smaller than window', () => {
|
||||
// Simulate the update logic from the hook
|
||||
const viewportHeight = 400
|
||||
const windowHeight = 800
|
||||
const diff = windowHeight - viewportHeight
|
||||
if (diff > 1) {
|
||||
root.style.setProperty('--app-viewport-height', `${viewportHeight}px`)
|
||||
} else {
|
||||
root.style.removeProperty('--app-viewport-height')
|
||||
}
|
||||
|
||||
expect(root.style.getPropertyValue('--app-viewport-height')).toBe('400px')
|
||||
})
|
||||
|
||||
it('removes --app-viewport-height when viewports match', () => {
|
||||
// First set it
|
||||
root.style.setProperty('--app-viewport-height', '400px')
|
||||
|
||||
// Then simulate keyboard close
|
||||
const viewportHeight = 800
|
||||
const windowHeight = 800
|
||||
const diff = windowHeight - viewportHeight
|
||||
if (diff > 1) {
|
||||
root.style.setProperty('--app-viewport-height', `${viewportHeight}px`)
|
||||
} else {
|
||||
root.style.removeProperty('--app-viewport-height')
|
||||
}
|
||||
|
||||
expect(root.style.getPropertyValue('--app-viewport-height')).toBe('')
|
||||
})
|
||||
|
||||
it('ignores sub-pixel differences (threshold of 1px)', () => {
|
||||
const viewportHeight = 799.5
|
||||
const windowHeight = 800
|
||||
const diff = windowHeight - viewportHeight
|
||||
if (diff > 1) {
|
||||
root.style.setProperty('--app-viewport-height', `${viewportHeight}px`)
|
||||
} else {
|
||||
root.style.removeProperty('--app-viewport-height')
|
||||
}
|
||||
|
||||
expect(root.style.getPropertyValue('--app-viewport-height')).toBe('')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
import { useEffect } from 'react'
|
||||
import { isTelegramApp } from '@/hooks/useTelegram'
|
||||
|
||||
/**
|
||||
* Sets a CSS custom property `--app-viewport-height` on <html> that tracks the
|
||||
* visual viewport height. This is a fallback for browsers that do not support
|
||||
* the `interactive-widget=resizes-content` viewport meta attribute — on those
|
||||
* browsers `100dvh` does NOT shrink when the virtual keyboard opens, so the
|
||||
* composer input is hidden behind the keyboard.
|
||||
*
|
||||
* The hook listens to `window.visualViewport.resize` and writes the viewport
|
||||
* height into the CSS variable. The CSS height chain is:
|
||||
* var(--tg-viewport-stable-height, var(--app-viewport-height, 100dvh))
|
||||
*
|
||||
* Skipped in Telegram Mini Apps (Telegram SDK provides its own height variable).
|
||||
*/
|
||||
export function useViewportHeight(): void {
|
||||
useEffect(() => {
|
||||
// Telegram Mini App has its own viewport management via --tg-viewport-stable-height
|
||||
if (isTelegramApp()) return
|
||||
|
||||
const viewport = window.visualViewport
|
||||
if (!viewport) return
|
||||
|
||||
const root = document.documentElement
|
||||
|
||||
function update() {
|
||||
if (!viewport) return
|
||||
// Only apply when the visual viewport is meaningfully smaller than
|
||||
// the window (keyboard is open). A small threshold (1px) avoids
|
||||
// false positives from sub-pixel rounding.
|
||||
const diff = window.innerHeight - viewport.height
|
||||
if (diff > 1) {
|
||||
root.style.setProperty('--app-viewport-height', `${viewport.height}px`)
|
||||
} else {
|
||||
root.style.removeProperty('--app-viewport-height')
|
||||
}
|
||||
}
|
||||
|
||||
viewport.addEventListener('resize', update)
|
||||
|
||||
return () => {
|
||||
viewport.removeEventListener('resize', update)
|
||||
root.style.removeProperty('--app-viewport-height')
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
+2
-1
@@ -97,7 +97,8 @@ html,
|
||||
body {
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
height: var(--tg-viewport-stable-height, 100dvh);
|
||||
height: var(--app-viewport-height, 100dvh);
|
||||
height: var(--tg-viewport-stable-height, var(--app-viewport-height, 100dvh));
|
||||
touch-action: pan-x pan-y;
|
||||
overscroll-behavior: none;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
|
||||
Reference in New Issue
Block a user