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 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 12:48:13 +03:00
parent d8e73cb2b5
commit 09b4a1b182

View File

@@ -104,6 +104,14 @@ export function startTutorial(config) {
onClose: config.onClose || null 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'); overlay.classList.add('active');
document.addEventListener('keydown', handleTutorialKey); document.addEventListener('keydown', handleTutorialKey);
showTutorialStep(0); showTutorialStep(0);
@@ -276,19 +284,25 @@ function _positionSpotlight(target, overlay, step, index, isFixed) {
if (tooltip) { if (tooltip) {
positionTutorialTooltip(tooltip, x, y, w, h, step.position, isFixed); 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. */ /** Wait for scroll to settle (position stops changing). */
function _waitForScrollEnd(scrollTarget) { function _waitForScrollEnd() {
return new Promise(resolve => { return new Promise(resolve => {
let timer = setTimeout(resolve, 500); // fallback let lastY = window.scrollY;
const onEnd = () => { let stableFrames = 0;
clearTimeout(timer); const fallback = setTimeout(resolve, 300);
scrollTarget.removeEventListener('scrollend', onEnd); function check() {
resolve(); const y = window.scrollY;
}; if (y === lastY) stableFrames++;
scrollTarget.addEventListener('scrollend', onEnd, { once: true }); 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; const needsScroll = preRect.bottom > window.innerHeight || preRect.top < headerH;
if (needsScroll) { 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' }); target.scrollIntoView({ behavior: 'smooth', block: 'center' });
_waitForScrollEnd(isFixed ? document.scrollingElement || document.documentElement : activeTutorial.container).then(() => { _waitForScrollEnd().then(() => {
if (activeTutorial && activeTutorial.step === index) { if (activeTutorial && activeTutorial.step === index) {
_positionSpotlight(target, overlay, step, index, isFixed); _positionSpotlight(target, overlay, step, index, isFixed);
} }