fix(ui): iOS soft-keyboard shell alignment + hotseat relock on return
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 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m43s
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 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m43s
iOS Safari/WKWebView does not shrink the layout viewport for the soft keyboard (Android Chrome does): the visual viewport shrinks AND offsets down toward the focused field, and the values do not cleanly revert. The pinned shell tracked only the height (--vvh), not the offset, so its top-anchored content misaligned on iOS — empty space below the form, worst on a repeat focus. Fix (one place; covers NewGame, Chat, Feedback): - app.svelte.ts syncViewport: also mirror visualViewport.offsetTop into --vv-top, and scroll the focused field into view on the keyboard-open transition (iOS does not reliably scroll a pinned document to it). - app.css: the pinned app-shell body follows top=--vv-top + height=--vvh (was inset:0), so it stays on the visible area. - e2e/viewport.spec.ts emulates the visual-viewport resize+offset (a fake window.visualViewport) to verify the shell follows, on Chromium+WebKit. Hotseat: returning to the lobby now re-locks the current seat (its PIN is re-prompted) — LocalSource.relock, cleared in Game.svelte onDestroy. Root-caused a crash it exposed: a $props() value (id) reads back undefined during Svelte teardown, so isLocalGameId(id) threw and aborted onDestroy (breaking navigation) — read the id from the loaded view instead.
This commit is contained in:
@@ -46,6 +46,9 @@ export const localSource = {
|
||||
unlockSeat: (id, pin) => load().then((s) => s.unlockSeat(id, pin)),
|
||||
verifyHostPin: (id, pin) => load().then((s) => s.verifyHostPin(id, pin)),
|
||||
hostAction: (id, pin, action, targetSeat) => load().then((s) => s.hostAction(id, pin, action, targetSeat)),
|
||||
// relock is fire-and-forget: the engine is already loaded (the game was open), and re-locking a
|
||||
// cached game before the screen is left needs no result.
|
||||
relock: (id) => void load().then((s) => s.relock(id)),
|
||||
// events must return the unsubscribe synchronously (the screen subscribes in onMount), so it wires
|
||||
// the real subscription once the engine loads and, until then, cancels a pending subscribe.
|
||||
events: (id, onEvent) => {
|
||||
@@ -60,7 +63,7 @@ export const localSource = {
|
||||
};
|
||||
},
|
||||
} satisfies GameLoopSource &
|
||||
Pick<LocalSource, 'events' | 'create' | 'list' | 'delete' | 'unlockSeat' | 'verifyHostPin' | 'hostAction'>;
|
||||
Pick<LocalSource, 'events' | 'create' | 'list' | 'delete' | 'unlockSeat' | 'verifyHostPin' | 'hostAction' | 'relock'>;
|
||||
|
||||
/**
|
||||
* gameSource returns the source that runs the game with the given id: the local engine for a local
|
||||
|
||||
Reference in New Issue
Block a user