Skip to content

Commit 6ea5b45

Browse files
committed
Fix mobile scroll lock after backgrounding; smooth the top-bar hide/reveal (#977)
* Fix mobile scroll lock after app backgrounding: viewport lock + resume unstick iOS can restore the app (PWA or Safari) with the window scrolled off origin; the body then swallows touch gestures and .main-content can't scroll until a tap breaks it loose. Restore the html/body viewport lock removed in 1b4aae5 - it was dropped to preserve native pull-to-refresh, which was replaced by custom PTR the same day, so nothing needs body scroll anymore. Scoped via :has(.app-container) so the login page still scrolls. Also snap the window back to origin on pageshow/visibilitychange as a second layer for cases the CSS lock can't cover. * Drop position:fixed from viewport lock; overflow:hidden is enough position:fixed on body disturbed the dynamic-viewport behavior the sticky top-bar hide animation rides on, making the nav-bar hide feel off on scroll down. overflow:hidden alone still blocks body scroll, and the pageshow/visibilitychange handler remains the primary backstop for the background-resume case. * Smooth the mobile top-bar hide: translateY instead of top:-100% The hide animated 'top' toward -100%, which resolves against the tall scroll container (~800px), so the ~65px bar cleared the screen in the first few ms of the 0.35s transition - an abrupt snap. Slide by transform: translateY(-100%) (100% = the bar's own height) over 0.4s so the full duration moves exactly one bar-height: a gradual slide. * Slow the top-bar reveal to 0.6s, keep the hide at 0.4s Split the transition timing: the base .top-bar rule governs the reveal (slower, 0.6s) and .top-bar-hidden governs the hide (0.4s).
1 parent 7491869 commit 6ea5b45

2 files changed

Lines changed: 30 additions & 2 deletions

File tree

src/NetworkOptimizer.Web/Components/Layout/MainLayout.razor

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,16 @@
232232
setTimeout(function() { window.scrollBy(0, 1); window.scrollBy(0, -1); }, 100);
233233
});
234234
235+
// iOS can restore from background with the window scrolled off (0,0); the body
236+
// then swallows touch gestures and .main-content can't scroll until a tap resets it
237+
function unstickViewport() {
238+
if (window.scrollX || window.scrollY) window.scrollTo(0, 0);
239+
}
240+
window.addEventListener('pageshow', unstickViewport);
241+
document.addEventListener('visibilitychange', function() {
242+
if (!document.hidden) setTimeout(unstickViewport, 50);
243+
});
244+
235245
// Show nav bar when content shrinks below scrollable (e.g. tab switch to short content)
236246
var pageContent = document.querySelector('.page-content');
237247
if (window.innerWidth <= 1024 && pageContent) {

src/NetworkOptimizer.Web/wwwroot/css/app.css

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3176,6 +3176,16 @@ select.form-control {
31763176

31773177
/* Responsive Design */
31783178
@media (max-width: 1024px) {
3179+
/* Lock the viewport so only .main-content scrolls - iOS can otherwise scroll
3180+
the body (esp. on return from background), swallowing touch gestures.
3181+
overflow:hidden only (no position:fixed, which disturbs the dynamic-viewport
3182+
behavior the sticky top-bar hide rides on). Scoped to the app shell so the
3183+
login page can still scroll. */
3184+
html:has(.app-container),
3185+
body:has(.app-container) {
3186+
overflow: hidden;
3187+
}
3188+
31793189
.content-grid {
31803190
grid-template-columns: 1fr;
31813191
}
@@ -3261,12 +3271,20 @@ select.form-control {
32613271
height: auto;
32623272
padding: 0.75rem 1rem;
32633273
gap: 0.5rem;
3264-
transition: top 0.35s cubic-bezier(0.4, 0, 0.2, 1);
3274+
/* Base transition governs the reveal (returning to this state) - slower */
3275+
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
32653276
}
32663277

3278+
/* Slide up by exactly the bar's own height (translateY %) so the full
3279+
transition is spent moving one bar-height - a gradual slide, not the
3280+
instant snap that top:-100% produced (that % resolves against the tall
3281+
scroll container, so the bar cleared the screen in a few ms).
3282+
Transition here governs the hide (entering this state) - kept quicker. */
32673283
.top-bar.top-bar-hidden {
32683284
position: sticky;
3269-
top: -100%;
3285+
top: 0;
3286+
transform: translateY(-100%);
3287+
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
32703288
pointer-events: none;
32713289
}
32723290

0 commit comments

Comments
 (0)