From 09b4a1b182a7a937fd843726d01833ce9a2ab485 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Tue, 10 Mar 2026 12:48:13 +0300 Subject: [PATCH] Fix tutorial tooltip flash at 0,0 and slow step transitions - Hide tooltip/ring with visibility:hidden until first step is positioned - Hide tooltip between steps when scrolling to prevent stale position flash - Replace scrollend event (poor browser support, 500ms fallback) with requestAnimationFrame polling that resolves in ~50ms when scroll settles Co-Authored-By: Claude Opus 4.6 --- .../static/js/features/tutorials.js | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/server/src/wled_controller/static/js/features/tutorials.js b/server/src/wled_controller/static/js/features/tutorials.js index 702c117..e3d1ff8 100644 --- a/server/src/wled_controller/static/js/features/tutorials.js +++ b/server/src/wled_controller/static/js/features/tutorials.js @@ -104,6 +104,14 @@ export function startTutorial(config) { onClose: config.onClose || null }); + // Hide tooltip and ring until first step positions them (prevents flash at 0,0) + const tooltip = overlay.querySelector('.tutorial-tooltip'); + const ring = overlay.querySelector('.tutorial-ring'); + const backdrop = overlay.querySelector('.tutorial-backdrop'); + if (tooltip) tooltip.style.visibility = 'hidden'; + if (ring) ring.style.visibility = 'hidden'; + if (backdrop) backdrop.style.clipPath = 'none'; + overlay.classList.add('active'); document.addEventListener('keydown', handleTutorialKey); showTutorialStep(0); @@ -276,19 +284,25 @@ function _positionSpotlight(target, overlay, step, index, isFixed) { if (tooltip) { positionTutorialTooltip(tooltip, x, y, w, h, step.position, isFixed); + tooltip.style.visibility = ''; } + if (ring) ring.style.visibility = ''; } -/** Wait for smooth scroll to finish, then resolve. */ -function _waitForScrollEnd(scrollTarget) { +/** Wait for scroll to settle (position stops changing). */ +function _waitForScrollEnd() { return new Promise(resolve => { - let timer = setTimeout(resolve, 500); // fallback - const onEnd = () => { - clearTimeout(timer); - scrollTarget.removeEventListener('scrollend', onEnd); - resolve(); - }; - scrollTarget.addEventListener('scrollend', onEnd, { once: true }); + let lastY = window.scrollY; + let stableFrames = 0; + const fallback = setTimeout(resolve, 300); + function check() { + const y = window.scrollY; + if (y === lastY) stableFrames++; + else { stableFrames = 0; lastY = y; } + if (stableFrames >= 3) { clearTimeout(fallback); resolve(); } + else requestAnimationFrame(check); + } + requestAnimationFrame(check); }); } @@ -321,8 +335,11 @@ function showTutorialStep(index, direction = 1) { const needsScroll = preRect.bottom > window.innerHeight || preRect.top < headerH; if (needsScroll) { + // Hide tooltip while scrolling to prevent stale position flash + const tt = overlay.querySelector('.tutorial-tooltip'); + if (tt) tt.style.visibility = 'hidden'; target.scrollIntoView({ behavior: 'smooth', block: 'center' }); - _waitForScrollEnd(isFixed ? document.scrollingElement || document.documentElement : activeTutorial.container).then(() => { + _waitForScrollEnd().then(() => { if (activeTutorial && activeTutorial.step === index) { _positionSpotlight(target, overlay, step, index, isFixed); }