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
This commit is contained in:
Frank Schwenk
2026-04-04 11:31:31 +02:00
parent f8d895ab21
commit 9bf4c20f11
10 changed files with 75 additions and 15 deletions
+29
View File
@@ -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 {
@@ -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%);
@@ -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;
+11 -4
View File
@@ -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);
}
@@ -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,
@@ -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}
/>
@@ -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}
/>
+1 -1
View File
@@ -1,5 +1,5 @@
.layout {
height: 100vh;
height: var(--app-height);
overflow: hidden;
background-color: var(--color-background);
color: var(--color-text);
+1 -1
View File
@@ -8,7 +8,7 @@ import App from "../components/App";
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content">
<title>BSC Score - Pool Scoring App</title>
<meta name="description" content="Professional pool/billiards scoring application for tournaments and casual games">
+10 -7
View File
@@ -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 */