fix(offline): hotseat form — natural scroll + focus-into-view (keyboard)
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m6s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m38s

The nested scroll region regressed the keyboard behaviour (a double
scroll: the whole screen scrolled with a huge blank area under the Start
button, and a focused bottom row hid behind the keyboard). Drop it: the
hotseat form now flows naturally and scrolls with the screen's own scroll
(.page.scrollform: no forced full height, no pinned button), and a name
input scrolls itself into view (centred above the keyboard) on focus.
This commit is contained in:
Ilia Denisov
2026-07-07 12:54:57 +02:00
parent 3adc4e32c9
commit cafe67e9c8
+20 -19
View File
@@ -232,6 +232,14 @@
deleteRow = null;
}
// Centre a focused name input above the soft keyboard: the keyboard shrinks the screen (--vvh),
// so wait a beat for it to open + the layout to settle, then scroll the field into view (the
// default focus-scroll leaves a bottom row hidden behind the keyboard).
function bringIntoView(e: FocusEvent): void {
const el = e.currentTarget as HTMLElement;
setTimeout(() => el.scrollIntoView({ block: 'center' }), 300);
}
function setHostPlays(yes: boolean): void {
askHostPlays = false;
if (!yes) return;
@@ -278,7 +286,10 @@
</script>
<Screen title={t('new.title')} back="/">
<div class="page">
<!-- The hotseat form flows naturally and scrolls with the screen's own scroll (scrollform: no
forced full height, no pinned button) so a focused name input's soft keyboard just scrolls the
page rather than fighting a nested scroll region. -->
<div class="page" class:scrollform={mode === 'friends' && offlineMode.active}>
<!-- The auto/friends selector. Online it is hidden for guests (no friends to invite). Offline
it is always shown: "quick" is the local vs_ai flow, "with friends" is the local
pass-and-play (hotseat) flow. -->
@@ -332,11 +343,7 @@
>{t('new.start')}</button>
{:else if offlineMode.active}
<!-- Offline pass-and-play (hotseat): a device-local 2-4 human game. The host sets a master
PIN first (it gates the roster); each seat may add its own PIN. The scrollable region +
the pinned Start button mirror the friends form, so a name input's soft keyboard shrinks
the region (no full-height spacer that would paint a blank block behind the keyboard). -->
<div class="fg">
<div class="hs-scroll">
PIN first (it gates the roster); each seat may add its own PIN. -->
<div class="hostpin">
<span class="ftitle">{t('hotseat.hostPin')}</span>
<button class="plink" onclick={() => (pad = hostPin ? { kind: 'host-change' } : { kind: 'host-set' })}>
@@ -375,6 +382,7 @@
disabled={!hostPin}
placeholder={t('hotseat.playerName')}
maxlength="40"
onfocus={bringIntoView}
oninput={() => (row.committed = commitName(row.name, row.committed))}
onblur={() => (row.name = row.committed)}
/>
@@ -401,13 +409,11 @@
+ {t('hotseat.addPlayer')}
</button>
{/if}
</div>
<button
class="invite"
disabled={!hostPin || !inviteVariant || !rosterReady(rows) || starting}
onclick={startHotseat}
>{t('new.start')}</button>
</div>
{:else if friends.length === 0}
<p class="subtitle">{t('new.noFriends')}</p>
{:else}
@@ -486,6 +492,12 @@
height: 100%;
box-sizing: border-box;
}
/* The hotseat form: natural height (grows past the viewport → the screen's own scroll takes over)
with no full-height pinning, so a focused name input just scrolls the page above the keyboard. */
.page.scrollform {
height: auto;
min-height: 100%;
}
.subtitle {
color: var(--text-muted);
margin: 0;
@@ -680,17 +692,6 @@
line-height: 1.4;
}
/* --- offline hotseat roster --- */
/* The scrollable region (host PIN + variants + roster), pinned above the Start button — mirrors
the friends form so a focused name input's soft keyboard shrinks this region (via --vvh on the
screen) instead of leaving a full-height blank spacer painted behind the keyboard. */
.hs-scroll {
flex: 1;
min-height: 0;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 14px;
}
.hostpin {
display: flex;
align-items: center;