@@ -52,6 +52,9 @@ export class CarouselElement extends HTMLElement {
5252 private autoplayReady = false
5353 private autoplayReadyScheduled = false
5454 private autoplayReadyTimer : TimerHandle | null = null
55+ private intersectionObserver : IntersectionObserver | undefined
56+ private isFullyInViewport = true
57+ private requestedAutoplayState : AnimationPlayState = 'paused'
5558 private readonly animationInstanceId : string
5659 private readonly domReadyHandler = ( ) => {
5760 document . removeEventListener ( 'DOMContentLoaded' , this . domReadyHandler )
@@ -126,6 +129,7 @@ export class CarouselElement extends HTMLElement {
126129 this . hasAutoplaySupport = supportsAutoplay
127130 this . autoplayPlugin = supportsAutoplay ? requestedAutoplay : null
128131 this . setAutoplayState ( 'paused' )
132+ this . requestedAutoplayState = 'paused'
129133
130134 if ( this . autoplayPlugin ) {
131135 const emblaWithEvents = this . emblaApi as EmblaCarouselType & {
@@ -150,6 +154,7 @@ export class CarouselElement extends HTMLElement {
150154 } )
151155 if ( this . hasAutoplaySupport ) {
152156 this . registerAnimationLifecycle ( )
157+ this . setupViewportObserver ( )
153158 this . scheduleAutoplayReady ( )
154159 }
155160 } catch ( error ) {
@@ -163,6 +168,7 @@ export class CarouselElement extends HTMLElement {
163168 }
164169
165170 private teardown ( ) : void {
171+ this . teardownViewportObserver ( )
166172 this . removeEventListener ( 'keydown' , this . keydownHandler )
167173 if ( this . emblaApi ) {
168174 const emblaWithEvents = this . emblaApi as EmblaCarouselType & {
@@ -347,20 +353,65 @@ export class CarouselElement extends HTMLElement {
347353
348354 pause ( ) : void {
349355 try {
350- this . updateAutoplayState ( 'paused' )
356+ this . requestedAutoplayState = 'paused'
357+ this . syncAutoplayWithViewport ( )
351358 } catch ( error ) {
352359 handleScriptError ( error , { scriptName : SCRIPT_NAME , operation : 'pause' } )
353360 }
354361 }
355362
356363 resume ( ) : void {
357364 try {
358- this . updateAutoplayState ( 'playing' )
365+ this . requestedAutoplayState = 'playing'
366+ this . syncAutoplayWithViewport ( )
359367 } catch ( error ) {
360368 handleScriptError ( error , { scriptName : SCRIPT_NAME , operation : 'resume' } )
361369 }
362370 }
363371
372+ private setupViewportObserver ( ) : void {
373+ if ( typeof document === 'undefined' ) return
374+ if ( this . intersectionObserver ) return
375+
376+ try {
377+ const IntersectionObserverCtor = (
378+ globalThis as unknown as { IntersectionObserver ?: typeof IntersectionObserver }
379+ ) . IntersectionObserver
380+
381+ if ( typeof IntersectionObserverCtor !== 'function' ) return
382+
383+ this . intersectionObserver = new IntersectionObserverCtor (
384+ ( entries ) => {
385+ const entry = entries . at ( 0 )
386+ const ratio = entry ?. intersectionRatio ?? 0
387+ this . isFullyInViewport = Boolean ( entry ?. isIntersecting && ratio >= 0.999 )
388+ this . syncAutoplayWithViewport ( )
389+ } ,
390+ { threshold : [ 0 , 0.999 ] } ,
391+ )
392+
393+ const observedTarget = this . emblaRoot ?? this
394+ this . intersectionObserver . observe ( observedTarget )
395+ } catch {
396+ // Best effort only
397+ }
398+ }
399+
400+ private teardownViewportObserver ( ) : void {
401+ try {
402+ this . intersectionObserver ?. disconnect ( )
403+ this . intersectionObserver = undefined
404+ this . isFullyInViewport = true
405+ } catch {
406+ // Best effort only
407+ }
408+ }
409+
410+ private syncAutoplayWithViewport ( ) : void {
411+ const shouldPlay = this . requestedAutoplayState === 'playing' && this . isFullyInViewport
412+ this . updateAutoplayState ( shouldPlay ? 'playing' : 'paused' )
413+ }
414+
364415 private setAutoplayState ( state : 'playing' | 'paused' ) : void {
365416 this . setAttribute ( 'data-carousel-autoplay' , state )
366417 }
0 commit comments