diff --git a/src/blocks/carousel/__tests__/edit.test.tsx b/src/blocks/carousel/__tests__/edit.test.tsx index 716238b..c73926f 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 ).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', () => { @@ -302,4 +304,43 @@ describe( 'Carousel Edit setup flow', () => { 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 0daab79..b886e29 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' ); @@ -174,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', () => { @@ -202,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 ); + } ); } ); } ); @@ -913,6 +960,56 @@ describe( 'Carousel View Module', () => { originalIntersectionObserver; } } ); + + it( 'includes the AutoScroll plugin when transition is fade', () => { + const mockContext = createMockContext( { + transition: 'fade', + autoScroll: { + speed: 2, + direction: 'forward', + startDelay: 1000, + stopOnInteraction: true, + stopOnMouseEnter: false, + stopOnFocusIn: true, + }, + } ); + 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; + 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 0f2ca83..c4a31c3 100644 --- a/src/blocks/carousel/edit.tsx +++ b/src/blocks/carousel/edit.tsx @@ -322,6 +322,7 @@ export default function Edit( { setAttributes( { transition: value as CarouselAttributes[ 'transition' ] } ) } - help={ __( - 'Choose how slides transition: sliding horizontally or cross-fading.', - 'rt-carousel', - ) } + help={ + autoScroll + ? __( 'Auto Scroll does not support transitions.', 'rt-carousel' ) + : __( + 'Choose how slides transition: sliding horizontally or cross-fading.', + 'rt-carousel', + ) + } /> setAttributes( { - autoScroll: value, - autoplay: value ? false : autoplay, - loop: ( value && autoScrollDirection === 'backward' ) ? true : loop, - } ) } + onChange={ ( value ) => { + const nextAttributes: Partial< CarouselAttributes > = { + autoScroll: value, + autoplay: value ? false : autoplay, + loop: ( value && autoScrollDirection === 'backward' ) ? true : loop, + }; + if ( value ) { + nextAttributes.transition = 'slide'; + } + setAttributes( nextAttributes ); + } } /> { autoScroll && ( <> (); - return context.canScrollPrev; + return context.autoScroll ? true : context.canScrollPrev; }, get canScrollNext() { const context = getContext(); - return context.canScrollNext; + return context.autoScroll ? true : context.canScrollNext; }, }, actions: { diff --git a/tests/js/setup.ts b/tests/js/setup.ts index e2b7da9..21ba522 100644 --- a/tests/js/setup.ts +++ b/tests/js/setup.ts @@ -61,6 +61,22 @@ jest.mock( 'embla-carousel-fade', () => ( { } ) ), } ) ); +/** + * 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. */