fix: re-anchor stray iOS layout-viewport scroll that broke nav taps
This commit is contained in:
@@ -2303,6 +2303,40 @@ function setupPortraitPwaWatcher() {
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Layout-viewport anchor guard (iOS Safari / standalone PWA)
|
||||
//
|
||||
// The app is a fixed-viewport layout: body is overflow:hidden and only inner
|
||||
// panes scroll, so the document itself must always sit at scroll position 0.
|
||||
// iOS WebKit can still scroll the *layout viewport* behind our back — exiting
|
||||
// native video fullscreen (webkitEnterFullscreen is the only fullscreen path
|
||||
// on iPhone, and iOS enters it by itself when the phone rotates while a video
|
||||
// plays), the on-screen keyboard revealing a focused input, or any
|
||||
// scrollIntoView() walking up into <html>. Once that happens, fixed elements
|
||||
// (bottom nav, mini-bar) are still *drawn* in place but their hit-testing
|
||||
// regions are offset by the stray scroll amount, so taps on the nav buttons
|
||||
// silently do nothing — and because the body isn't user-scrollable there is
|
||||
// no gesture that can undo it. That is the "nav buttons stop working after
|
||||
// playing a video" iPhone bug. Snap the document back to 0 whenever it ends
|
||||
// up scrolled; skipped while an input is focused so we never fight the
|
||||
// keyboard auto-scroll, then re-anchored once focus leaves the field.
|
||||
function setupViewportAnchorGuard() {
|
||||
const editing = () => {
|
||||
const el = document.activeElement;
|
||||
return !!el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.isContentEditable);
|
||||
};
|
||||
const reanchor = () => {
|
||||
if (editing()) return;
|
||||
const doc = document.scrollingElement || document.documentElement;
|
||||
if (window.scrollY || doc.scrollTop) window.scrollTo(0, 0);
|
||||
};
|
||||
window.addEventListener('scroll', reanchor);
|
||||
document.addEventListener('focusout', () => setTimeout(reanchor, 50));
|
||||
// Exiting native video fullscreen is the most reliable reproducer of the
|
||||
// stray-scroll state; the scroll event alone doesn't always fire for it.
|
||||
els.video.addEventListener('webkitendfullscreen', () => setTimeout(reanchor, 50));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Events
|
||||
// ============================================================================
|
||||
@@ -2649,8 +2683,12 @@ document.querySelectorAll('.chip').forEach((c) => {
|
||||
$('miniPlayBtn').addEventListener('click', (e) => { e.stopPropagation(); Player.toggle(); });
|
||||
$('miniBar').addEventListener('click', () => {
|
||||
hideMiniBar();
|
||||
// Scroll the player into view if needed
|
||||
els.playerPane.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
// Scroll the player into view if needed. In portrait PWA, scroll only the
|
||||
// list-pane (the designated scroll container) — scrollIntoView() also
|
||||
// scrolls overflow:hidden ancestors up to <html> on iOS, leaving the
|
||||
// document offset and fixed-element hit testing broken.
|
||||
if (isPortraitPWA()) scrollPlayerIntoViewPortrait();
|
||||
else els.playerPane.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
});
|
||||
$('miniCloseBtn').addEventListener('click', (e) => { e.stopPropagation(); hideMiniBar(); });
|
||||
|
||||
@@ -3104,6 +3142,7 @@ async function boot() {
|
||||
wireUI();
|
||||
wireShortcutHelp();
|
||||
setupPortraitPwaWatcher();
|
||||
setupViewportAnchorGuard();
|
||||
try {
|
||||
const loaded = await API.loadData();
|
||||
if (loaded && typeof loaded === 'object') {
|
||||
|
||||
Reference in New Issue
Block a user