From 9bf4c20f11222d0fc216460ea3f9815cade47921 Mon Sep 17 00:00:00 2001 From: Frank Schwenk Date: Sat, 4 Apr 2026 11:31:31 +0200 Subject: [PATCH] fix: improve Android tablet keyboard-safe layout Use dynamic viewport sizing and keyboard-aware spacing to keep new-game inputs and navigation reachable in PWA landscape mode on Android tablets. Refs #30. Made-with: Cursor --- src/components/App.tsx | 29 +++++++++++++++++++ .../game-detail/GameDetail.module.css | 2 +- .../features/game-list/GameList.module.css | 2 +- src/lib/features/new-game/NewGame.module.css | 15 +++++++--- .../features/new-game/steps/Player1Step.tsx | 7 +++++ .../features/new-game/steps/Player2Step.tsx | 7 +++++ .../features/new-game/steps/Player3Step.tsx | 7 +++++ src/lib/ui/Layout.module.css | 2 +- src/pages/index.astro | 2 +- src/styles/index.css | 17 ++++++----- 10 files changed, 75 insertions(+), 15 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index efd1ed4..7ce128d 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -33,6 +33,35 @@ export default function App() { const validationModal = useValidationModal(); const completionModal = useCompletionModal(); + // Keep viewport height stable on Android tablets when virtual keyboard opens. + useEffect(() => { + const root = document.documentElement; + + const updateViewportVars = () => { + const viewport = window.visualViewport; + const appHeight = viewport ? viewport.height : window.innerHeight; + const keyboardOffset = viewport + ? Math.max(0, window.innerHeight - viewport.height - viewport.offsetTop) + : 0; + + root.style.setProperty('--app-height', `${Math.round(appHeight)}px`); + root.style.setProperty('--keyboard-offset', `${Math.round(keyboardOffset)}px`); + }; + + updateViewportVars(); + window.addEventListener('resize', updateViewportVars); + window.addEventListener('orientationchange', updateViewportVars); + window.visualViewport?.addEventListener('resize', updateViewportVars); + window.visualViewport?.addEventListener('scroll', updateViewportVars); + + return () => { + window.removeEventListener('resize', updateViewportVars); + window.removeEventListener('orientationchange', updateViewportVars); + window.visualViewport?.removeEventListener('resize', updateViewportVars); + window.visualViewport?.removeEventListener('scroll', updateViewportVars); + }; + }, []); + // Game lifecycle handlers const handleCreateGame = useCallback(async (gameData: any) => { try { diff --git a/src/lib/features/game-detail/GameDetail.module.css b/src/lib/features/game-detail/GameDetail.module.css index 81b8279..e42c4c8 100644 --- a/src/lib/features/game-detail/GameDetail.module.css +++ b/src/lib/features/game-detail/GameDetail.module.css @@ -4,7 +4,7 @@ top: 0; left: 0; width: 100%; - min-height: 100vh; + min-height: var(--app-height); display: none; opacity: 0; transform: translateX(100%); diff --git a/src/lib/features/game-list/GameList.module.css b/src/lib/features/game-list/GameList.module.css index 6f26861..81e9dba 100644 --- a/src/lib/features/game-list/GameList.module.css +++ b/src/lib/features/game-list/GameList.module.css @@ -9,7 +9,7 @@ .screen-content { display: flex; flex-direction: column; - min-height: 100vh; + min-height: var(--app-height); padding: var(--space-lg); overflow-y: auto; -webkit-overflow-scrolling: touch; diff --git a/src/lib/features/new-game/NewGame.module.css b/src/lib/features/new-game/NewGame.module.css index 0eb2730..58f82fe 100644 --- a/src/lib/features/new-game/NewGame.module.css +++ b/src/lib/features/new-game/NewGame.module.css @@ -4,7 +4,7 @@ top: 0; left: 0; width: 100%; - min-height: 100vh; + min-height: var(--app-height); display: none; opacity: 0; transform: translateX(100%); @@ -145,8 +145,10 @@ .form-content { flex: 1; - overflow: hidden; + overflow-y: auto; + overscroll-behavior: contain; padding: 0 var(--space-lg); + padding-bottom: var(--space-md); min-height: 0; display: flex; flex-direction: column; @@ -154,7 +156,12 @@ .form-footer { flex-shrink: 0; - padding: var(--space-lg); + position: sticky; + bottom: 0; + z-index: 2; + background: var(--color-surface); + padding: var(--space-md) var(--space-lg) calc(var(--space-lg) + var(--keyboard-offset)) var(--space-lg); + border-top: 1px solid var(--color-border); } .progress-indicator { display: flex; @@ -210,7 +217,7 @@ display: flex; justify-content: space-between; align-items: center; - margin-top: var(--space-xxl); + margin-top: var(--space-lg); width: 100%; gap: var(--space-lg); } diff --git a/src/lib/features/new-game/steps/Player1Step.tsx b/src/lib/features/new-game/steps/Player1Step.tsx index c03ad0d..9f8444a 100644 --- a/src/lib/features/new-game/steps/Player1Step.tsx +++ b/src/lib/features/new-game/steps/Player1Step.tsx @@ -111,8 +111,15 @@ export const Player1Step = ({ playerNameHistory, onNext, onCancel, initialValue } }} autoComplete="off" + autoCapitalize="words" + spellCheck={false} + enterKeyHint="next" aria-label="Name Spieler 1" aria-describedby="player1-help" + onFocus={(e) => { + const target = e.currentTarget as HTMLInputElement; + target.scrollIntoView({ block: 'center', behavior: 'smooth' }); + }} style={{ fontSize: UI_CONSTANTS.INPUT_FONT_SIZE, minHeight: UI_CONSTANTS.INPUT_MIN_HEIGHT, diff --git a/src/lib/features/new-game/steps/Player2Step.tsx b/src/lib/features/new-game/steps/Player2Step.tsx index b770c0a..f512a86 100644 --- a/src/lib/features/new-game/steps/Player2Step.tsx +++ b/src/lib/features/new-game/steps/Player2Step.tsx @@ -70,7 +70,14 @@ export const Player2Step = ({ playerNameHistory, onNext, onCancel, initialValue setPlayer2(target.value); }} autoComplete="off" + autoCapitalize="words" + spellCheck={false} + enterKeyHint="next" aria-label="Name Spieler 2" + onFocus={(e) => { + const target = e.currentTarget as HTMLInputElement; + target.scrollIntoView({ block: 'center', behavior: 'smooth' }); + }} style={{ fontSize: '1.2rem', minHeight: 48, marginTop: 12, marginBottom: 12, width: '100%', paddingRight: 44 }} ref={inputRef} /> diff --git a/src/lib/features/new-game/steps/Player3Step.tsx b/src/lib/features/new-game/steps/Player3Step.tsx index dfeedf5..453590d 100644 --- a/src/lib/features/new-game/steps/Player3Step.tsx +++ b/src/lib/features/new-game/steps/Player3Step.tsx @@ -67,7 +67,14 @@ export const Player3Step = ({ playerNameHistory, onNext, onCancel, initialValue setPlayer3(target.value); }} autoComplete="off" + autoCapitalize="words" + spellCheck={false} + enterKeyHint="done" aria-label="Name Spieler 3" + onFocus={(e) => { + const target = e.currentTarget as HTMLInputElement; + target.scrollIntoView({ block: 'center', behavior: 'smooth' }); + }} style={{ fontSize: '1.2rem', minHeight: 48, marginTop: 12, marginBottom: 12, width: '100%', paddingRight: 44 }} ref={inputRef} /> diff --git a/src/lib/ui/Layout.module.css b/src/lib/ui/Layout.module.css index 91a984d..e3cdc64 100644 --- a/src/lib/ui/Layout.module.css +++ b/src/lib/ui/Layout.module.css @@ -1,5 +1,5 @@ .layout { - height: 100vh; + height: var(--app-height); overflow: hidden; background-color: var(--color-background); color: var(--color-text); diff --git a/src/pages/index.astro b/src/pages/index.astro index 36534ef..f8e34ab 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -8,7 +8,7 @@ import App from "../components/App"; - + BSC Score - Pool Scoring App diff --git a/src/styles/index.css b/src/styles/index.css index 0266d0f..266134d 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -3,17 +3,13 @@ margin: 0; padding: 0; box-sizing: border-box; - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -webkit-tap-highlight-color: transparent; } /* Design system tokens */ :root { + --app-height: 100dvh; + --keyboard-offset: 0px; /* Colors */ --color-primary: #ff9800; --color-primary-hover: #ffa726; @@ -87,7 +83,7 @@ html, body { height: 100%; - overflow: hidden; + min-height: 100%; } body { @@ -98,6 +94,13 @@ body { line-height: 1.5; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + min-height: var(--app-height); +} + +button, .btn, .button { + -webkit-user-select: none; + user-select: none; + -webkit-touch-callout: none; } /* Improved input styling for better tablet experience */