From 2e4fdf3d9172eb36cdead14aea730d24172037c9 Mon Sep 17 00:00:00 2001 From: Milind More Date: Fri, 10 Jul 2026 19:19:15 +0530 Subject: [PATCH 1/2] fix: disable auto-scroll functionality when using fade transition to prevent conflicts --- src/blocks/carousel/__tests__/edit.test.tsx | 4 +- src/blocks/carousel/__tests__/view.test.ts | 52 ++++++++ src/blocks/carousel/edit.tsx | 141 +++++++++++--------- src/blocks/carousel/save.tsx | 2 +- src/blocks/carousel/view.ts | 2 +- tests/js/setup.ts | 16 +++ 6 files changed, 148 insertions(+), 69 deletions(-) diff --git a/src/blocks/carousel/__tests__/edit.test.tsx b/src/blocks/carousel/__tests__/edit.test.tsx index 716238b..946fb86 100644 --- a/src/blocks/carousel/__tests__/edit.test.tsx +++ b/src/blocks/carousel/__tests__/edit.test.tsx @@ -257,6 +257,7 @@ describe( 'Carousel Edit setup flow', () => { expect( selectLabels ).not.toContain( 'Contain Scroll' ); expect( toggleLabels ).not.toContain( 'Free Drag' ); expect( toggleLabels ).not.toContain( 'Scroll Auto' ); + expect( toggleLabels ).not.toContain( 'Enable Auto Scroll' ); expect( ( RangeControl as unknown as jest.Mock ).mock.calls.some( ( [ props ] ) => props.label === 'Slides to Scroll', ) ).toBe( false ); @@ -282,6 +283,7 @@ describe( 'Carousel Edit setup flow', () => { expect( selectLabels ).toContain( 'Contain Scroll' ); expect( toggleLabels ).toContain( 'Free Drag' ); expect( toggleLabels ).toContain( 'Scroll Auto' ); + expect( toggleLabels ).toContain( 'Enable Auto Scroll' ); } ); it( 'calls setAttributes with the selected transition', () => { @@ -300,6 +302,6 @@ describe( 'Carousel Edit setup flow', () => { transitionCall[ 0 ].onChange( 'fade' ); - expect( setAttributes ).toHaveBeenCalledWith( { transition: 'fade' } ); + expect( setAttributes ).toHaveBeenCalledWith( { transition: 'fade', autoScroll: false } ); } ); } ); diff --git a/src/blocks/carousel/__tests__/view.test.ts b/src/blocks/carousel/__tests__/view.test.ts index 0daab79..a740040 100644 --- a/src/blocks/carousel/__tests__/view.test.ts +++ b/src/blocks/carousel/__tests__/view.test.ts @@ -16,6 +16,7 @@ import { store, getContext, getElement } from '@wordpress/interactivity'; import EmblaCarousel, { type EmblaCarouselType } from 'embla-carousel'; import Fade from 'embla-carousel-fade'; +import AutoScroll from 'embla-carousel-auto-scroll'; // Symbol key used by the view.ts for Embla instances const EMBLA_KEY = Symbol.for( 'rt-carousel.carousel' ); @@ -913,6 +914,57 @@ describe( 'Carousel View Module', () => { originalIntersectionObserver; } } ); + + it( 'does not include the AutoScroll plugin when transition is fade', () => { + const mockContext = createMockContext( { + transition: 'fade', + autoScroll: { + speed: 2, + direction: 'forward', + startDelay: 1000, + stopOnInteraction: true, + stopOnMouseEnter: false, + }, + } ); + const { wrapper, viewport } = createMockCarouselDOM(); + const originalIntersectionObserver = window.IntersectionObserver; + + viewport.getBoundingClientRect = jest.fn( () => ( { + width: 100, + height: 0, + top: 0, + right: 0, + bottom: 0, + left: 0, + x: 0, + y: 0, + toJSON: () => ( {} ), + } ) ); + + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + ( getElement as jest.Mock ).mockReturnValue( { ref: wrapper } ); + ( EmblaCarousel as unknown as jest.Mock ).mockReturnValue( + createMockEmblaInstance( { + scrollProgress: jest.fn( () => 0 ), + slideNodes: jest.fn( () => [] ), + } ), + ); + delete ( window as Window & { IntersectionObserver?: typeof IntersectionObserver } ).IntersectionObserver; + + try { + storeConfig.callbacks.initCarousel(); + + const lastCall = ( EmblaCarousel as unknown as jest.Mock ).mock.calls.at( -1 ); + expect( lastCall?.[ 2 ] ).toContainEqual( ( Fade as unknown as jest.Mock ).mock.results.at( -1 )?.value ); + const autoScrollMockResult = ( AutoScroll as unknown as jest.Mock ).mock.results.at( -1 )?.value; + if ( autoScrollMockResult ) { + expect( lastCall?.[ 2 ] ).not.toContainEqual( autoScrollMockResult ); + } + } finally { + ( window as Window & { IntersectionObserver?: typeof IntersectionObserver } ).IntersectionObserver = + originalIntersectionObserver; + } + } ); } ); } ); } ); diff --git a/src/blocks/carousel/edit.tsx b/src/blocks/carousel/edit.tsx index 0f2ca83..46cc028 100644 --- a/src/blocks/carousel/edit.tsx +++ b/src/blocks/carousel/edit.tsx @@ -326,9 +326,16 @@ export default function Edit( { { label: __( 'Slide', 'rt-carousel' ), value: 'slide' }, { label: __( 'Fade', 'rt-carousel' ), value: 'fade' }, ] } - onChange={ ( value ) => - setAttributes( { transition: value as CarouselAttributes[ 'transition' ] } ) - } + onChange={ ( value ) => { + const nextTransition = value as CarouselAttributes[ 'transition' ]; + const nextAttributes: Partial< CarouselAttributes > = { + transition: nextTransition, + }; + if ( nextTransition === 'fade' ) { + nextAttributes.autoScroll = false; + } + setAttributes( nextAttributes ); + } } help={ __( 'Choose how slides transition: sliding horizontally or cross-fading.', 'rt-carousel', @@ -496,71 +503,73 @@ export default function Edit( { ) } - - setAttributes( { - autoScroll: value, - autoplay: value ? false : autoplay, - loop: ( value && autoScrollDirection === 'backward' ) ? true : loop, - } ) } - /> - { autoScroll && ( <> - - setAttributes( { autoScrollSpeed: value ?? 2 } ) - } - min={ 1 } - max={ 10 } - /> - - setAttributes( { - autoScrollDirection: value as CarouselAttributes['autoScrollDirection'], - loop: value === 'backward' ? true : loop, - } ) - } - /> - - setAttributes( { autoScrollStartDelay: value ?? 1000 } ) - } - min={ 0 } - max={ 10000 } - step={ 100 } - /> - - setAttributes( { autoScrollStopOnInteraction: value } ) - } - help={ __( 'Stop auto scroll when user interacts with carousel.', 'rt-carousel' ) } - /> + { transition !== 'fade' && ( + - setAttributes( { autoScrollStopOnMouseEnter: value } ) - } - help={ __( 'Stop auto scroll when mouse hovers over carousel.', 'rt-carousel' ) } + label={ __( 'Enable Auto Scroll', 'rt-carousel' ) } + checked={ autoScroll } + onChange={ ( value ) => setAttributes( { + autoScroll: value, + autoplay: value ? false : autoplay, + loop: ( value && autoScrollDirection === 'backward' ) ? true : loop, + } ) } /> - ) } - + { autoScroll && ( <> + + setAttributes( { autoScrollSpeed: value ?? 2 } ) + } + min={ 1 } + max={ 10 } + /> + + setAttributes( { + autoScrollDirection: value as CarouselAttributes['autoScrollDirection'], + loop: value === 'backward' ? true : loop, + } ) + } + /> + + setAttributes( { autoScrollStartDelay: value ?? 1000 } ) + } + min={ 0 } + max={ 10000 } + step={ 100 } + /> + + setAttributes( { autoScrollStopOnInteraction: value } ) + } + help={ __( 'Stop auto scroll when user interacts with carousel.', 'rt-carousel' ) } + /> + + setAttributes( { autoScrollStopOnMouseEnter: value } ) + } + help={ __( 'Stop auto scroll when mouse hovers over carousel.', 'rt-carousel' ) } + /> + ) } + + ) } ( { } ) ), } ) ); +/** + * Mock embla-carousel-auto-scroll module. + */ +jest.mock( 'embla-carousel-auto-scroll', () => ( { + __esModule: true, + default: jest.fn( () => ( { + name: 'autoScroll', + options: {}, + init: jest.fn(), + destroy: jest.fn(), + play: jest.fn(), + stop: jest.fn(), + isPlaying: jest.fn( () => false ), + } ) ), +} ) ); + /** * Mock WordPress block editor components. */ From 3704dcded8f67d8207b88e3b8014eae6aa7ea89e Mon Sep 17 00:00:00 2001 From: Milind More Date: Thu, 16 Jul 2026 13:53:54 +0530 Subject: [PATCH 2/2] feat: allow auto-scroll for all transitions, update UI states, and add related tests. --- src/blocks/carousel/__tests__/edit.test.tsx | 43 +++++- src/blocks/carousel/__tests__/view.test.ts | 53 ++++++- src/blocks/carousel/controls/edit.tsx | 5 +- src/blocks/carousel/edit.tsx | 156 ++++++++++---------- src/blocks/carousel/save.tsx | 2 +- src/blocks/carousel/view.ts | 6 +- 6 files changed, 176 insertions(+), 89 deletions(-) diff --git a/src/blocks/carousel/__tests__/edit.test.tsx b/src/blocks/carousel/__tests__/edit.test.tsx index 946fb86..c73926f 100644 --- a/src/blocks/carousel/__tests__/edit.test.tsx +++ b/src/blocks/carousel/__tests__/edit.test.tsx @@ -257,7 +257,7 @@ describe( 'Carousel Edit setup flow', () => { expect( selectLabels ).not.toContain( 'Contain Scroll' ); expect( toggleLabels ).not.toContain( 'Free Drag' ); expect( toggleLabels ).not.toContain( 'Scroll Auto' ); - expect( toggleLabels ).not.toContain( 'Enable Auto Scroll' ); + expect( toggleLabels ).toContain( 'Enable Auto Scroll' ); expect( ( RangeControl as unknown as jest.Mock ).mock.calls.some( ( [ props ] ) => props.label === 'Slides to Scroll', ) ).toBe( false ); @@ -302,6 +302,45 @@ describe( 'Carousel Edit setup flow', () => { transitionCall[ 0 ].onChange( 'fade' ); - expect( setAttributes ).toHaveBeenCalledWith( { transition: 'fade', autoScroll: false } ); + expect( setAttributes ).toHaveBeenCalledWith( { transition: 'fade' } ); + } ); + + it( 'disables the transition dropdown with notice when autoScroll is enabled', () => { + render( + , + ); + + const transitionCall = ( SelectControl as unknown as jest.Mock ).mock.calls.find( + ( [ props ] ) => props.label === 'Transition', + ); + + expect( transitionCall[ 0 ].disabled ).toBe( true ); + expect( transitionCall[ 0 ].help ).toBe( 'Auto Scroll does not support transitions.' ); + } ); + + it( 'switches transition to slide when autoScroll is enabled', () => { + const setAttributes = jest.fn(); + render( + , + ); + + const autoScrollToggle = ( ToggleControl as unknown as jest.Mock ).mock.calls.find( + ( [ props ] ) => props.label === 'Enable Auto Scroll', + ); + + autoScrollToggle[ 0 ].onChange( true ); + + expect( setAttributes ).toHaveBeenCalledWith( expect.objectContaining( { + autoScroll: true, + transition: 'slide', + } ) ); } ); } ); diff --git a/src/blocks/carousel/__tests__/view.test.ts b/src/blocks/carousel/__tests__/view.test.ts index a740040..b886e29 100644 --- a/src/blocks/carousel/__tests__/view.test.ts +++ b/src/blocks/carousel/__tests__/view.test.ts @@ -175,6 +175,29 @@ describe( 'Carousel View Module', () => { expect( result ).toBe( false ); } ); + + it( 'should return true when autoScroll is enabled', () => { + const mockContext = createMockContext( { + canScrollPrev: false, + autoScroll: { + speed: 2, + direction: 'forward', + startDelay: 1000, + stopOnInteraction: true, + stopOnMouseEnter: false, + stopOnFocusIn: true, + }, + } ); + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + + const result = + Object.getOwnPropertyDescriptor( + storeConfig.state, + 'canScrollPrev', + )?.get?.() ?? false; + + expect( result ).toBe( true ); + } ); } ); describe( 'canScrollNext', () => { @@ -203,6 +226,29 @@ describe( 'Carousel View Module', () => { expect( result ).toBe( false ); } ); + + it( 'should return true when autoScroll is enabled', () => { + const mockContext = createMockContext( { + canScrollNext: false, + autoScroll: { + speed: 2, + direction: 'forward', + startDelay: 1000, + stopOnInteraction: true, + stopOnMouseEnter: false, + stopOnFocusIn: true, + }, + } ); + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + + const result = + Object.getOwnPropertyDescriptor( + storeConfig.state, + 'canScrollNext', + )?.get?.() ?? false; + + expect( result ).toBe( true ); + } ); } ); } ); @@ -915,7 +961,7 @@ describe( 'Carousel View Module', () => { } } ); - it( 'does not include the AutoScroll plugin when transition is fade', () => { + it( 'includes the AutoScroll plugin when transition is fade', () => { const mockContext = createMockContext( { transition: 'fade', autoScroll: { @@ -924,6 +970,7 @@ describe( 'Carousel View Module', () => { startDelay: 1000, stopOnInteraction: true, stopOnMouseEnter: false, + stopOnFocusIn: true, }, } ); const { wrapper, viewport } = createMockCarouselDOM(); @@ -957,9 +1004,7 @@ describe( 'Carousel View Module', () => { const lastCall = ( EmblaCarousel as unknown as jest.Mock ).mock.calls.at( -1 ); expect( lastCall?.[ 2 ] ).toContainEqual( ( Fade as unknown as jest.Mock ).mock.results.at( -1 )?.value ); const autoScrollMockResult = ( AutoScroll as unknown as jest.Mock ).mock.results.at( -1 )?.value; - if ( autoScrollMockResult ) { - expect( lastCall?.[ 2 ] ).not.toContainEqual( autoScrollMockResult ); - } + expect( lastCall?.[ 2 ] ).toContainEqual( autoScrollMockResult ); } finally { ( window as Window & { IntersectionObserver?: typeof IntersectionObserver } ).IntersectionObserver = originalIntersectionObserver; diff --git a/src/blocks/carousel/controls/edit.tsx b/src/blocks/carousel/controls/edit.tsx index da15df7..c02d159 100644 --- a/src/blocks/carousel/controls/edit.tsx +++ b/src/blocks/carousel/controls/edit.tsx @@ -13,6 +13,7 @@ export default function Edit() { emblaApi: contextApi, canScrollPrev, canScrollNext, + carouselOptions, } = useContext( EditorCarouselContext ); const ref = useRef( null ); @@ -58,7 +59,7 @@ export default function Edit() { handleScroll( 'prev' ); } } type="button" - disabled={ ! canScrollPrev } + disabled={ ! carouselOptions?.autoScroll && ! canScrollPrev } aria-label={ __( 'Previous Slide', 'rt-carousel' ) } > @@ -70,7 +71,7 @@ export default function Edit() { handleScroll( 'next' ); } } type="button" - disabled={ ! canScrollNext } + disabled={ ! carouselOptions?.autoScroll && ! canScrollNext } aria-label={ __( 'Next Slide', 'rt-carousel' ) } > diff --git a/src/blocks/carousel/edit.tsx b/src/blocks/carousel/edit.tsx index 46cc028..c4a31c3 100644 --- a/src/blocks/carousel/edit.tsx +++ b/src/blocks/carousel/edit.tsx @@ -322,24 +322,22 @@ export default function Edit( { { - const nextTransition = value as CarouselAttributes[ 'transition' ]; - const nextAttributes: Partial< CarouselAttributes > = { - transition: nextTransition, - }; - if ( nextTransition === 'fade' ) { - nextAttributes.autoScroll = false; - } - setAttributes( nextAttributes ); - } } - help={ __( - 'Choose how slides transition: sliding horizontally or cross-fading.', - 'rt-carousel', - ) } + onChange={ ( value ) => + setAttributes( { transition: value as CarouselAttributes[ 'transition' ] } ) + } + help={ + autoScroll + ? __( 'Auto Scroll does not support transitions.', 'rt-carousel' ) + : __( + 'Choose how slides transition: sliding horizontally or cross-fading.', + 'rt-carousel', + ) + } /> ) } - { transition !== 'fade' && ( - - setAttributes( { + + { + const nextAttributes: Partial< CarouselAttributes > = { autoScroll: value, autoplay: value ? false : autoplay, loop: ( value && autoScrollDirection === 'backward' ) ? true : loop, - } ) } + }; + if ( value ) { + nextAttributes.transition = 'slide'; + } + setAttributes( nextAttributes ); + } } + /> + { autoScroll && ( <> + + setAttributes( { autoScrollSpeed: value ?? 2 } ) + } + min={ 1 } + max={ 10 } /> - { autoScroll && ( <> - - setAttributes( { autoScrollSpeed: value ?? 2 } ) - } - min={ 1 } - max={ 10 } - /> - - setAttributes( { - autoScrollDirection: value as CarouselAttributes['autoScrollDirection'], - loop: value === 'backward' ? true : loop, - } ) - } - /> - - setAttributes( { autoScrollStartDelay: value ?? 1000 } ) - } - min={ 0 } - max={ 10000 } - step={ 100 } - /> - - setAttributes( { autoScrollStopOnInteraction: value } ) - } - help={ __( 'Stop auto scroll when user interacts with carousel.', 'rt-carousel' ) } - /> - - setAttributes( { autoScrollStopOnMouseEnter: value } ) - } - help={ __( 'Stop auto scroll when mouse hovers over carousel.', 'rt-carousel' ) } - /> - ) } - - ) } + + setAttributes( { + autoScrollDirection: value as CarouselAttributes['autoScrollDirection'], + loop: value === 'backward' ? true : loop, + } ) + } + /> + + setAttributes( { autoScrollStartDelay: value ?? 1000 } ) + } + min={ 0 } + max={ 10000 } + step={ 100 } + /> + + setAttributes( { autoScrollStopOnInteraction: value } ) + } + help={ __( 'Stop auto scroll when user interacts with carousel.', 'rt-carousel' ) } + /> + + setAttributes( { autoScrollStopOnMouseEnter: value } ) + } + help={ __( 'Stop auto scroll when mouse hovers over carousel.', 'rt-carousel' ) } + /> + ) } + (); - return context.canScrollPrev; + return context.autoScroll ? true : context.canScrollPrev; }, get canScrollNext() { const context = getContext(); - return context.canScrollNext; + return context.autoScroll ? true : context.canScrollNext; }, }, actions: { @@ -320,7 +320,7 @@ store( 'rt-carousel/carousel', { plugins.push( Autoplay( context.autoplay as AutoplayOptionsType ) ); } - if ( context.autoScroll && context.transition !== 'fade' ) { + if ( context.autoScroll ) { plugins.push( AutoScroll( context.autoScroll as AutoScrollOptionsType ) ); }