From b317f406c3ace00054ed2f6b7719b9928bea05ba Mon Sep 17 00:00:00 2001 From: Yiannis Christodoulou Date: Sun, 9 Nov 2025 22:20:15 +0200 Subject: [PATCH] fix: Scroll is locked: Need to reload page. (149) Introduced lockBodyScroll and unlockBodyScroll methods in CustomChaptersOverlay and CustomSettingsMenu to consistently handle body scroll locking and restoration on mobile devices when overlays are opened or closed. This improves iOS compatibility and code maintainability by centralizing scroll management logic. --- .../controls/CustomChaptersOverlay.js | 72 +++++++++++++------ .../components/controls/CustomSettingsMenu.js | 65 +++++++++++------ 2 files changed, 94 insertions(+), 43 deletions(-) diff --git a/frontend-tools/video-js/src/components/controls/CustomChaptersOverlay.js b/frontend-tools/video-js/src/components/controls/CustomChaptersOverlay.js index 0e2236fc..1709244d 100644 --- a/frontend-tools/video-js/src/components/controls/CustomChaptersOverlay.js +++ b/frontend-tools/video-js/src/components/controls/CustomChaptersOverlay.js @@ -20,6 +20,7 @@ class CustomChaptersOverlay extends Component { this.touchStartTime = 0; this.touchThreshold = 150; // ms for tap vs scroll detection this.isSmallScreen = window.innerWidth <= 480; + this.scrollY = 0; // Track scroll position before locking // Bind methods this.createOverlay = this.createOverlay.bind(this); @@ -31,6 +32,8 @@ class CustomChaptersOverlay extends Component { this.handleMobileInteraction = this.handleMobileInteraction.bind(this); this.setupResizeListener = this.setupResizeListener.bind(this); this.handleResize = this.handleResize.bind(this); + this.lockBodyScroll = this.lockBodyScroll.bind(this); + this.unlockBodyScroll = this.unlockBodyScroll.bind(this); // Initialize after player is ready this.player().ready(() => { @@ -65,6 +68,9 @@ class CustomChaptersOverlay extends Component { const el = this.player().el(); if (el) el.classList.remove('chapters-open'); + + // Restore body scroll on mobile when closing + this.unlockBodyScroll(); } setupResizeListener() { @@ -164,6 +170,8 @@ class CustomChaptersOverlay extends Component { this.overlay.style.display = 'none'; const el = this.player().el(); if (el) el.classList.remove('chapters-open'); + // Restore body scroll on mobile when closing + this.unlockBodyScroll(); }; chapterClose.appendChild(closeBtn); playlistTitle.appendChild(chapterClose); @@ -355,6 +363,37 @@ class CustomChaptersOverlay extends Component { } } + lockBodyScroll() { + if (!this.isMobile) return; + + // Save current scroll position + this.scrollY = window.scrollY || window.pageYOffset; + + // Lock body scroll with proper iOS handling + document.body.style.overflow = 'hidden'; + document.body.style.position = 'fixed'; + document.body.style.top = `-${this.scrollY}px`; + document.body.style.left = '0'; + document.body.style.right = '0'; + document.body.style.width = '100%'; + } + + unlockBodyScroll() { + if (!this.isMobile) return; + + // Restore body scroll + const scrollY = this.scrollY; + document.body.style.overflow = ''; + document.body.style.position = ''; + document.body.style.top = ''; + document.body.style.left = ''; + document.body.style.right = ''; + document.body.style.width = ''; + + // Restore scroll position + window.scrollTo(0, scrollY); + } + toggleOverlay() { if (!this.overlay) return; @@ -369,17 +408,11 @@ class CustomChaptersOverlay extends Component { navigator.vibrate(30); } - // Prevent body scroll on mobile when overlay is open - if (this.isMobile) { - if (isHidden) { - document.body.style.overflow = 'hidden'; - document.body.style.position = 'fixed'; - document.body.style.width = '100%'; - } else { - document.body.style.overflow = ''; - document.body.style.position = ''; - document.body.style.width = ''; - } + // Lock/unlock body scroll on mobile when overlay opens/closes + if (isHidden) { + this.lockBodyScroll(); + } else { + this.unlockBodyScroll(); } try { @@ -390,7 +423,9 @@ class CustomChaptersOverlay extends Component { m.classList.remove('vjs-lock-showing'); m.style.display = 'none'; }); - } catch (e) {} + } catch { + // Ignore errors when closing menus + } } updateCurrentChapter() { @@ -406,7 +441,6 @@ class CustomChaptersOverlay extends Component { currentTime >= chapter.startTime && (index === this.chaptersData.length - 1 || currentTime < this.chaptersData[index + 1].startTime); - const handle = item.querySelector('.playlist-drag-handle'); const dynamic = item.querySelector('.meta-dynamic'); if (isPlaying) { currentChapterIndex = index; @@ -463,11 +497,7 @@ class CustomChaptersOverlay extends Component { if (el) el.classList.remove('chapters-open'); // Restore body scroll on mobile - if (this.isMobile) { - document.body.style.overflow = ''; - document.body.style.position = ''; - document.body.style.width = ''; - } + this.unlockBodyScroll(); } } @@ -479,11 +509,7 @@ class CustomChaptersOverlay extends Component { if (el) el.classList.remove('chapters-open'); // Restore body scroll on mobile when disposing - if (this.isMobile) { - document.body.style.overflow = ''; - document.body.style.position = ''; - document.body.style.width = ''; - } + this.unlockBodyScroll(); // Clean up event listeners if (this.handleResize) { diff --git a/frontend-tools/video-js/src/components/controls/CustomSettingsMenu.js b/frontend-tools/video-js/src/components/controls/CustomSettingsMenu.js index 5c3dc163..6a66c861 100644 --- a/frontend-tools/video-js/src/components/controls/CustomSettingsMenu.js +++ b/frontend-tools/video-js/src/components/controls/CustomSettingsMenu.js @@ -25,6 +25,7 @@ class CustomSettingsMenu extends Component { this.isMobile = this.detectMobile(); this.isSmallScreen = window.innerWidth <= 480; this.touchThreshold = 150; // ms for tap vs scroll detection + this.scrollY = 0; // Track scroll position before locking // Bind methods this.createSettingsButton = this.createSettingsButton.bind(this); @@ -41,6 +42,8 @@ class CustomSettingsMenu extends Component { this.detectMobile = this.detectMobile.bind(this); this.handleMobileInteraction = this.handleMobileInteraction.bind(this); this.setupResizeListener = this.setupResizeListener.bind(this); + this.lockBodyScroll = this.lockBodyScroll.bind(this); + this.unlockBodyScroll = this.unlockBodyScroll.bind(this); // Initialize after player is ready this.player().ready(() => { @@ -656,6 +659,8 @@ class CustomSettingsMenu extends Component { if (btnEl) { btnEl.classList.remove('settings-clicked'); } + // Restore body scroll on mobile when closing + this.unlockBodyScroll(); }; closeButton.addEventListener('click', closeFunction); @@ -942,6 +947,37 @@ class CustomSettingsMenu extends Component { this.startSubtitleSync(); } + lockBodyScroll() { + if (!this.isMobile) return; + + // Save current scroll position + this.scrollY = window.scrollY || window.pageYOffset; + + // Lock body scroll with proper iOS handling + document.body.style.overflow = 'hidden'; + document.body.style.position = 'fixed'; + document.body.style.top = `-${this.scrollY}px`; + document.body.style.left = '0'; + document.body.style.right = '0'; + document.body.style.width = '100%'; + } + + unlockBodyScroll() { + if (!this.isMobile) return; + + // Restore body scroll + const scrollY = this.scrollY; + document.body.style.overflow = ''; + document.body.style.position = ''; + document.body.style.top = ''; + document.body.style.left = ''; + document.body.style.right = ''; + document.body.style.width = ''; + + // Restore scroll position + window.scrollTo(0, scrollY); + } + toggleSettings(e) { // e.stopPropagation(); const isVisible = this.settingsOverlay.classList.contains('show'); @@ -954,11 +990,7 @@ class CustomSettingsMenu extends Component { this.stopKeepingControlsVisible(); // Restore body scroll on mobile when closing - if (this.isMobile) { - document.body.style.overflow = ''; - document.body.style.position = ''; - document.body.style.width = ''; - } + this.unlockBodyScroll(); } else { this.settingsOverlay.classList.add('show'); this.settingsOverlay.style.display = 'block'; @@ -972,11 +1004,7 @@ class CustomSettingsMenu extends Component { } // Prevent body scroll on mobile when overlay is open - if (this.isMobile) { - document.body.style.overflow = 'hidden'; - document.body.style.position = 'fixed'; - document.body.style.width = '100%'; - } + this.lockBodyScroll(); } this.speedSubmenu.style.display = 'none'; // Hide submenu when main menu toggles @@ -1002,6 +1030,9 @@ class CustomSettingsMenu extends Component { this.settingsOverlay.classList.add('show'); this.settingsOverlay.style.display = 'block'; + // Lock body scroll when opening + this.lockBodyScroll(); + // Hide other submenus and show subtitles submenu this.speedSubmenu.style.display = 'none'; if (this.qualitySubmenu) this.qualitySubmenu.style.display = 'none'; @@ -1072,11 +1103,7 @@ class CustomSettingsMenu extends Component { } // Restore body scroll on mobile when closing - if (this.isMobile) { - document.body.style.overflow = ''; - document.body.style.position = ''; - document.body.style.width = ''; - } + this.unlockBodyScroll(); } } @@ -1417,6 +1444,8 @@ class CustomSettingsMenu extends Component { if (btnEl) { btnEl.classList.remove('settings-clicked'); } + // Restore body scroll on mobile when closing + this.unlockBodyScroll(); } } @@ -1493,11 +1522,7 @@ class CustomSettingsMenu extends Component { } // Restore body scroll on mobile when disposing - if (this.isMobile) { - document.body.style.overflow = ''; - document.body.style.position = ''; - document.body.style.width = ''; - } + this.unlockBodyScroll(); // Remove DOM elements if (this.settingsOverlay) {