mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-11 21:39:01 +00:00
fix(mobile): hide bottom nav when on-screen keyboard is up (BUG-2070) (#859)
PLAN-1694 Phase 1 specced "hide the bar when the on-screen keyboard is up" but it shipped unimplemented — the fixed BottomNav stayed pinned above the keyboard, eating vertical space during text entry. Detect the keyboard from its own geometry: track visualViewport.height against the tallest height seen (the keyboard-closed baseline) and flag keyboardVisible when it shrinks >150px. This works on iOS Safari and Android Chrome, unlike `innerHeight - visualViewport.height`, which stays ~0 on browsers that shrink innerHeight in lockstep with the visual viewport. Baseline grows as the URL bar collapses on scroll and re-captures on orientationchange so rotation/chrome don't false-trigger; gated on isTouch. BottomNav gates the <nav> on !keyboardVisible (docked sheets stay mounted so QuickCapture survives raising the keyboard itself), and drops the has-bottom-nav content-reflow padding with it to avoid a dead gap. Claude-Session: https://claude.ai/code/session_019knGmnHcx5rrgWXQ8V8DZS
This commit is contained in:
@@ -71,11 +71,12 @@
|
||||
captureOpen = next;
|
||||
}
|
||||
|
||||
// Drive the .main-content reflow only while shown (mobile). app.css owns
|
||||
// the media-gated padding rule.
|
||||
// Drive the .main-content reflow only while the bar is actually shown —
|
||||
// mobile AND no keyboard up. app.css owns the media-gated padding rule;
|
||||
// dropping the class while the keyboard hides the bar avoids a dead gap.
|
||||
$effect(() => {
|
||||
if (typeof document === 'undefined') return;
|
||||
document.body.classList.toggle('has-bottom-nav', uiStore.isMobile);
|
||||
document.body.classList.toggle('has-bottom-nav', uiStore.isMobile && !uiStore.keyboardVisible);
|
||||
});
|
||||
onDestroy(() => {
|
||||
if (typeof document !== 'undefined') document.body.classList.remove('has-bottom-nav');
|
||||
@@ -83,6 +84,13 @@
|
||||
</script>
|
||||
|
||||
{#if uiStore.isMobile && wsPrefix}
|
||||
<!--
|
||||
Hide the bar while the on-screen keyboard is up so it doesn't sit
|
||||
stranded above the keyboard (PLAN-1694 Phase 1). The docked sheets
|
||||
below stay mounted regardless — QuickCaptureSheet's title input
|
||||
raises the keyboard itself, and it must survive that.
|
||||
-->
|
||||
{#if !uiStore.keyboardVisible}
|
||||
<nav class="bottom-nav" aria-label="Primary">
|
||||
<button
|
||||
class="bn-item"
|
||||
@@ -147,6 +155,7 @@
|
||||
<span class="bn-label">You</span>
|
||||
</button>
|
||||
</nav>
|
||||
{/if}
|
||||
|
||||
<WorkspaceSheet open={workspaceOpen} onclose={() => (workspaceOpen = false)} />
|
||||
<YouSheet open={youOpen} onclose={() => (youOpen = false)} />
|
||||
|
||||
@@ -5,6 +5,14 @@ let topbarOpen = $state(browser ? localStorage.getItem('pad-topbar') !== 'closed
|
||||
let searchOpen = $state(false);
|
||||
let isMobile = $state(browser ? window.innerWidth <= 768 : false);
|
||||
let isTouch = $state(browser ? 'ontouchstart' in window : false);
|
||||
// True while the on-screen keyboard is up. Detected from the geometry of the
|
||||
// keyboard itself — visualViewport.height shrinking below the tallest height
|
||||
// we've seen (the keyboard-closed baseline). This works on iOS Safari AND
|
||||
// Android Chrome; a naive `innerHeight - visualViewport.height` does NOT,
|
||||
// because those browsers shrink window.innerHeight in lockstep with the visual
|
||||
// viewport, leaving the delta ~0. Consumers (e.g. BottomNav) hide fixed bottom
|
||||
// chrome so it doesn't sit stranded above the keyboard. PLAN-1694.
|
||||
let keyboardVisible = $state(false);
|
||||
let detailPanelOpen = $state(browser ? localStorage.getItem('pad-detail-panel') !== 'closed' && window.innerWidth > 768 : false);
|
||||
let createWorkspaceOpen = $state(false);
|
||||
let quickAddRequested = $state(false);
|
||||
@@ -30,6 +38,30 @@ if (browser) {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Track on-screen keyboard visibility from the visual viewport. Only touch
|
||||
// devices raise a soft keyboard, so gate on isTouch — a narrow desktop
|
||||
// window never has one and must not hide the nav.
|
||||
if (isTouch && window.visualViewport) {
|
||||
const vv = window.visualViewport;
|
||||
// The tallest viewport height we've seen == keyboard closed. It grows as
|
||||
// the URL bar collapses on scroll and resets on rotation, so the keyboard
|
||||
// shrink is always measured against the true full-height reference.
|
||||
let baseline = vv.height;
|
||||
const KEYBOARD_MIN_PX = 150; // smaller shrinks are browser chrome, not a keyboard
|
||||
const measure = () => {
|
||||
if (vv.height > baseline) baseline = vv.height;
|
||||
keyboardVisible = baseline - vv.height > KEYBOARD_MIN_PX;
|
||||
};
|
||||
vv.addEventListener('resize', measure);
|
||||
vv.addEventListener('scroll', measure);
|
||||
// Re-capture the baseline after an orientation change so a shorter
|
||||
// landscape viewport isn't mistaken for an open keyboard.
|
||||
window.addEventListener('orientationchange', () => {
|
||||
baseline = 0;
|
||||
setTimeout(measure, 300);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const uiStore = {
|
||||
@@ -38,6 +70,7 @@ export const uiStore = {
|
||||
get searchOpen() { return searchOpen; },
|
||||
get isMobile() { return isMobile; },
|
||||
get isTouch() { return isTouch; },
|
||||
get keyboardVisible() { return keyboardVisible; },
|
||||
get detailPanelOpen() { return detailPanelOpen; },
|
||||
get createWorkspaceOpen() { return createWorkspaceOpen; },
|
||||
|
||||
|
||||
Reference in New Issue
Block a user