|
256 | 256 | padding: 0; |
257 | 257 | scrollbar-width: thin; |
258 | 258 | scrollbar-color: var(--sl-color-accent) var(--sl-color-gray-5); |
| 259 | + scroll-behavior: auto; /* Ensure standard scrolling, not smooth */ |
| 260 | + -webkit-overflow-scrolling: auto; /* Disable momentum scrolling on iOS/Safari */ |
| 261 | + overscroll-behavior: contain; /* Prevent scroll chaining */ |
259 | 262 | } |
260 | 263 |
|
261 | 264 | /* Remove borders from code blocks inside scrollable area since container has border */ |
|
308 | 311 | // Flag to track if manual scroll event has been sent (only send once) |
309 | 312 | let hasTrackedManualScroll = false; |
310 | 313 |
|
| 314 | + // Animation frame ID to cancel animation if needed |
| 315 | + let scrollAnimationFrame: number | null = null; |
| 316 | + let isAnimating = false; |
| 317 | + |
311 | 318 | const handleRevealButton = () => { |
312 | 319 | const button = document.querySelector('.reveal-button') as HTMLElement | null; |
313 | 320 | const overlay = document.querySelector('.code-overlay') as HTMLElement | null; |
|
316 | 323 |
|
317 | 324 | if (!button || !overlay || !scrollableContainer || !scrollableInner) return; |
318 | 325 |
|
| 326 | + // Function to cancel animation |
| 327 | + const cancelAnimation = () => { |
| 328 | + if (isAnimating) { |
| 329 | + isAnimating = false; |
| 330 | + isProgrammaticScroll = false; |
| 331 | + if (scrollAnimationFrame) { |
| 332 | + cancelAnimationFrame(scrollAnimationFrame); |
| 333 | + scrollAnimationFrame = null; |
| 334 | + } |
| 335 | + // Make sure reset button is visible when animation is interrupted |
| 336 | + const resetButton = document.querySelector('.reset-overlay-button') as HTMLElement; |
| 337 | + if (resetButton) { |
| 338 | + resetButton.style.display = 'block'; |
| 339 | + } |
| 340 | + } |
| 341 | + }; |
| 342 | + |
| 343 | + // Listen for wheel events to cancel animation on manual scroll |
| 344 | + const wheelHandler = (e: WheelEvent) => { |
| 345 | + if (isAnimating) { |
| 346 | + cancelAnimation(); |
| 347 | + } |
| 348 | + }; |
| 349 | + |
| 350 | + // Listen for touch events to cancel animation on touch devices |
| 351 | + const touchHandler = (e: TouchEvent) => { |
| 352 | + if (isAnimating) { |
| 353 | + cancelAnimation(); |
| 354 | + } |
| 355 | + }; |
| 356 | + |
319 | 357 | button.addEventListener('click', () => { |
320 | 358 | // Event tracking handled by CSS class: plausible-event-name=home:reveal-code |
321 | 359 | scrollableContainer.classList.add('scrollable-enabled'); |
322 | 360 | overlay.style.opacity = '0'; |
323 | 361 |
|
| 362 | + // Add wheel and touch event listeners to detect manual scrolling |
| 363 | + scrollableInner.addEventListener('wheel', wheelHandler, { passive: true }); |
| 364 | + scrollableInner.addEventListener('touchstart', touchHandler, { passive: true }); |
| 365 | + |
324 | 366 | // Custom 3-second scroll animation |
325 | 367 | const startScroll = scrollableInner.scrollTop; |
326 | 368 | const targetScroll = scrollableInner.scrollHeight; |
327 | 369 | const distance = targetScroll - startScroll; |
328 | 370 | const duration = 3000; // 3 seconds |
329 | 371 | const startTime = performance.now(); |
| 372 | + isAnimating = true; |
| 373 | + isProgrammaticScroll = true; // Keep this true for entire animation |
330 | 374 |
|
331 | 375 | function easeOutCubic(t: number) { |
332 | 376 | return 1 - Math.pow(1 - t, 3); |
333 | 377 | } |
334 | 378 |
|
335 | 379 | function animateScroll(currentTime: number) { |
| 380 | + if (!isAnimating) { |
| 381 | + // Animation was cancelled |
| 382 | + isProgrammaticScroll = false; |
| 383 | + scrollableInner.removeEventListener('wheel', wheelHandler); |
| 384 | + scrollableInner.removeEventListener('touchstart', touchHandler); |
| 385 | + return; |
| 386 | + } |
| 387 | + |
336 | 388 | const elapsed = currentTime - startTime; |
337 | 389 | const progress = Math.min(elapsed / duration, 1); |
338 | 390 | const easedProgress = easeOutCubic(progress); |
339 | 391 |
|
340 | 392 | scrollableInner.scrollTop = startScroll + (distance * easedProgress); |
341 | 393 |
|
342 | 394 | if (progress < 1) { |
343 | | - requestAnimationFrame(animateScroll); |
| 395 | + scrollAnimationFrame = requestAnimationFrame(animateScroll); |
| 396 | + } else { |
| 397 | + isAnimating = false; |
| 398 | + isProgrammaticScroll = false; |
| 399 | + scrollAnimationFrame = null; |
| 400 | + scrollableInner.removeEventListener('wheel', wheelHandler); |
| 401 | + scrollableInner.removeEventListener('touchstart', touchHandler); |
344 | 402 | } |
345 | 403 | } |
346 | 404 |
|
347 | | - requestAnimationFrame(animateScroll); |
| 405 | + scrollAnimationFrame = requestAnimationFrame(animateScroll); |
348 | 406 |
|
349 | 407 | setTimeout(() => overlay.classList.add('hidden'), 200); |
350 | 408 | setTimeout(() => { |
|
363 | 421 | // Show/hide button based on scroll position |
364 | 422 | const updateButtonVisibility = () => { |
365 | 423 | // Track manual scroll event (only once, and only if not programmatic) |
366 | | - if (!hasTrackedManualScroll && !isProgrammaticScroll) { |
| 424 | + if (!hasTrackedManualScroll && !isProgrammaticScroll && !isAnimating) { |
367 | 425 | hasTrackedManualScroll = true; |
368 | 426 | if (typeof window.plausible !== 'undefined') { |
369 | 427 | window.plausible('home:code-scroll'); |
|
378 | 436 | }; |
379 | 437 |
|
380 | 438 | // Listen to scroll events |
381 | | - scrollableInner.addEventListener('scroll', updateButtonVisibility); |
| 439 | + scrollableInner.addEventListener('scroll', updateButtonVisibility, { passive: true }); |
382 | 440 |
|
383 | 441 | button.addEventListener('click', () => { |
384 | 442 | // Mark as programmatic scroll to avoid tracking |
|
403 | 461 | button.addEventListener('click', () => { |
404 | 462 | const scrollTopButton = document.querySelector('.scroll-top-button') as HTMLElement | null; |
405 | 463 |
|
| 464 | + // Cancel any ongoing animation |
| 465 | + isAnimating = false; |
| 466 | + isProgrammaticScroll = false; // Reset programmatic flag |
| 467 | + if (scrollAnimationFrame) { |
| 468 | + cancelAnimationFrame(scrollAnimationFrame); |
| 469 | + scrollAnimationFrame = null; |
| 470 | + } |
| 471 | + |
406 | 472 | // Reset to initial state |
407 | 473 | overlay.classList.remove('hidden'); |
408 | 474 | overlay.style.opacity = '1'; |
|
0 commit comments