diff --git a/package-lock.json b/package-lock.json index 1245dc7..c10eb3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@wordpress/icons": "11.5.0", "@wordpress/interactivity": "6.37.0", "embla-carousel": "8.6.0", + "embla-carousel-auto-scroll": "8.6.0", "embla-carousel-autoplay": "8.6.0", "react": "^18.3.1", "react-dom": "^18.3.1" @@ -4529,9 +4530,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4553,9 +4551,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4577,9 +4572,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4601,9 +4593,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4625,9 +4614,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4649,9 +4635,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7275,9 +7258,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7292,9 +7272,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7309,9 +7286,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7326,9 +7300,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7343,9 +7314,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7360,9 +7328,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7377,9 +7342,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7394,9 +7356,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -12686,6 +12645,15 @@ "version": "8.6.0", "license": "MIT" }, + "node_modules/embla-carousel-auto-scroll": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-auto-scroll/-/embla-carousel-auto-scroll-8.6.0.tgz", + "integrity": "sha512-WT9fWhNXFpbQ6kP+aS07oF5IHYLZ1Dx4DkwgCY8Hv2ZyYd2KMCPfMV1q/cA3wFGuLO7GMgKiySLX90/pQkcOdQ==", + "license": "MIT", + "peerDependencies": { + "embla-carousel": "8.6.0" + } + }, "node_modules/embla-carousel-autoplay": { "version": "8.6.0", "license": "MIT", @@ -23219,7 +23187,7 @@ }, "node_modules/typescript": { "version": "5.9.3", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index 61f3de3..bace22f 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@wordpress/icons": "11.5.0", "@wordpress/interactivity": "6.37.0", "embla-carousel": "8.6.0", + "embla-carousel-auto-scroll": "8.6.0", "embla-carousel-autoplay": "8.6.0", "react": "^18.3.1", "react-dom": "^18.3.1" diff --git a/src/blocks/carousel/__tests__/edit.test.tsx b/src/blocks/carousel/__tests__/edit.test.tsx index 62297fe..d7e67e0 100644 --- a/src/blocks/carousel/__tests__/edit.test.tsx +++ b/src/blocks/carousel/__tests__/edit.test.tsx @@ -157,6 +157,12 @@ const createAttributes = (): CarouselAttributes => ( { ariaLabel: 'Carousel', slidesToScroll: '1', slideGap: 0, + autoScroll: false, + autoScrollSpeed: 2, + autoScrollDirection: 'forward' as const, + autoScrollStartDelay: 1000, + autoScrollStopOnInteraction: true, + autoScrollStopOnMouseEnter: false, } ); describe( 'Carousel Edit setup flow', () => { @@ -217,4 +223,16 @@ describe( 'Carousel Edit setup flow', () => { Object.defineProperty( globalThis, 'document', originalDocumentDescriptor ); } } ); + + it( 'should have correct default autoScroll attributes', () => { + const attributes = createAttributes(); + expect( attributes.autoScroll ).toBe( false ); + expect( attributes.autoScrollSpeed ).toBe( 2 ); + expect( attributes.autoScrollDirection ).toBe( 'forward' ); + expect( attributes.autoScrollStartDelay ).toBe( 1000 ); + expect( attributes.autoScrollStopOnInteraction ).toBe( true ); + expect( attributes.autoScrollStopOnMouseEnter ).toBe( false ); + } ); + + } ); diff --git a/src/blocks/carousel/__tests__/view.test.ts b/src/blocks/carousel/__tests__/view.test.ts index 599fca3..e06e25f 100644 --- a/src/blocks/carousel/__tests__/view.test.ts +++ b/src/blocks/carousel/__tests__/view.test.ts @@ -68,6 +68,7 @@ const createMockContext = ( scrollProgress: 0, slideCount: 3, ariaLabelPattern: 'Go to slide %d', + autoScroll: false, ...overrides, } ); @@ -1010,4 +1011,34 @@ describe( 'Edge Cases and Error Handling', () => { expect( mockContext.scrollSnaps ).toHaveLength( 100 ); expect( mockContext.selectedIndex ).toBe( 50 ); } ); + + it( 'should handle autoScroll configuration', () => { + const mockContextWithAutoScroll = createMockContext( { + autoScroll: { + speed: 3, + direction: 'forward', + startDelay: 500, + stopOnInteraction: false, + stopOnMouseEnter: true, + stopOnFocusIn: false, + }, + } ); + + expect( mockContextWithAutoScroll.autoScroll ).toEqual( { + speed: 3, + direction: 'forward', + startDelay: 500, + stopOnInteraction: false, + stopOnMouseEnter: true, + stopOnFocusIn: false, + } ); + } ); + + it( 'should handle autoScroll disabled', () => { + const mockContext = createMockContext( { + autoScroll: false, + } ); + + expect( mockContext.autoScroll ).toBe( false ); + } ); } ); diff --git a/src/blocks/carousel/block.json b/src/blocks/carousel/block.json index e0df7ca..c5287b5 100644 --- a/src/blocks/carousel/block.json +++ b/src/blocks/carousel/block.json @@ -85,6 +85,31 @@ "slidesToScroll": { "type": "string", "default": "1" + }, + "autoScroll": { + "type": "boolean", + "default": false + }, + "autoScrollSpeed": { + "type": "number", + "default": 2 + }, + "autoScrollDirection": { + "type": "string", + "default": "ltr", + "enum": ["forward", "backward"] + }, + "autoScrollStartDelay": { + "type": "number", + "default": 1000 + }, + "autoScrollStopOnInteraction": { + "type": "boolean", + "default": true + }, + "autoScrollStopOnMouseEnter": { + "type": "boolean", + "default": false } }, "editorScript": "file:./index.js", diff --git a/src/blocks/carousel/edit.tsx b/src/blocks/carousel/edit.tsx index f4f4be8..c6a4e4e 100644 --- a/src/blocks/carousel/edit.tsx +++ b/src/blocks/carousel/edit.tsx @@ -54,6 +54,12 @@ export default function Edit( { autoplayStopOnMouseEnter, ariaLabel, slidesToScroll = '1', + autoScroll, + autoScrollSpeed, + autoScrollDirection, + autoScrollStartDelay, + autoScrollStopOnInteraction, + autoScrollStopOnMouseEnter } = attributes; const [ emblaApi, setEmblaApi ] = useState(); @@ -314,11 +320,11 @@ export default function Edit( { setAttributes( { loop: value } ) } - help={ __( - 'Enables infinite scrolling of slides.', - 'rt-carousel', - ) } + help={ autoScrollDirection === 'backward' + ? __( 'Loop is required for backward auto scroll.', 'rt-carousel' ) + : __( 'Enables infinite scrolling of slides.', 'rt-carousel' ) } /> setAttributes( { autoplay: value } ) } + onChange={ ( value ) => { + setAttributes( { + autoplay: value, + autoScroll: value ? false : autoScroll, + } ) + }} /> { autoplay && ( <> @@ -460,6 +471,70 @@ export default function Edit( { ) } + + setAttributes( { + autoScroll: value, + autoplay: value ? false : autoplay, + } ) } + /> + { 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' ) } + /> + ) } + ; @@ -65,4 +71,12 @@ export type CarouselContext = { ref?: HTMLElement | null; slideCount: number; initialized?: boolean; + autoScroll: boolean | { + speed: number; + direction: 'forward' | 'backward'; + startDelay: number; + stopOnInteraction: boolean; + stopOnMouseEnter: boolean; + stopOnFocusIn: boolean; + }; }; diff --git a/src/blocks/carousel/view.ts b/src/blocks/carousel/view.ts index c2ae8e4..5c68545 100644 --- a/src/blocks/carousel/view.ts +++ b/src/blocks/carousel/view.ts @@ -4,6 +4,8 @@ import EmblaCarousel, { type EmblaCarouselType, } from 'embla-carousel'; import Autoplay, { type AutoplayOptionsType } from 'embla-carousel-autoplay'; +import AutoScroll, { type AutoScrollOptionsType } from 'embla-carousel-auto-scroll'; + import type { CarouselContext } from './types'; import { DYNAMIC_LIST_CONTAINER_SELECTOR, @@ -310,6 +312,10 @@ store( 'rt-carousel/carousel', { plugins.push( Autoplay( context.autoplay as AutoplayOptionsType ) ); } + if (context.autoScroll) { + plugins.push( AutoScroll( context.autoScroll as AutoScrollOptionsType) ); + } + const embla = EmblaCarousel( viewport, options, plugins ); emblaInstances.set( viewport, embla );