From dc1b8c4963719f57032e0195596ac3cf815e1361 Mon Sep 17 00:00:00 2001 From: Brandon Hubbard Date: Mon, 16 Mar 2026 12:49:22 -0700 Subject: [PATCH 1/5] Add support for the progress bar. This would support issue #88 - Add support for progress bar. --- inc/Plugin.php | 1 + src/blocks/carousel/progress/block.json | 20 ++++++++++++++++++++ src/blocks/carousel/progress/edit.tsx | 23 +++++++++++++++++++++++ src/blocks/carousel/progress/index.ts | 11 +++++++++++ src/blocks/carousel/progress/save.tsx | 17 +++++++++++++++++ src/blocks/carousel/progress/style.scss | 16 ++++++++++++++++ src/blocks/carousel/types.ts | 2 ++ src/blocks/carousel/view.ts | 10 ++++++++++ 8 files changed, 100 insertions(+) create mode 100644 src/blocks/carousel/progress/block.json create mode 100644 src/blocks/carousel/progress/edit.tsx create mode 100644 src/blocks/carousel/progress/index.ts create mode 100644 src/blocks/carousel/progress/save.tsx create mode 100644 src/blocks/carousel/progress/style.scss diff --git a/inc/Plugin.php b/inc/Plugin.php index 25f975a..67e4ddc 100644 --- a/inc/Plugin.php +++ b/inc/Plugin.php @@ -70,6 +70,7 @@ public function register_blocks(): void { 'carousel', 'carousel/controls', 'carousel/dots', + 'carousel/progress', 'carousel/viewport', 'carousel/slide', ]; diff --git a/src/blocks/carousel/progress/block.json b/src/blocks/carousel/progress/block.json new file mode 100644 index 0000000..7b99a38 --- /dev/null +++ b/src/blocks/carousel/progress/block.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "version": "1.0.0", + "name": "carousel-kit/carousel-progress", + "title": "Carousel Progress", + "category": "carousel-kit", + "icon": "minus", + "ancestor": [ + "carousel-kit/carousel" + ], + "description": "Progress bar for the carousel.", + "textdomain": "carousel-kit", + "attributes": {}, + "supports": { + "interactivity": true + }, + "editorScript": "file:./index.js", + "style": "file:./style-index.css" +} diff --git a/src/blocks/carousel/progress/edit.tsx b/src/blocks/carousel/progress/edit.tsx new file mode 100644 index 0000000..7293e4b --- /dev/null +++ b/src/blocks/carousel/progress/edit.tsx @@ -0,0 +1,23 @@ +import { __ } from '@wordpress/i18n'; +import { useBlockProps } from '@wordpress/block-editor'; +import { useContext } from '@wordpress/element'; +import { EditorCarouselContext } from '../editor-context'; + +export default function Edit() { + const blockProps = useBlockProps( { + className: 'carousel-kit-progress', + } ); + + const { scrollProgress } = useContext( EditorCarouselContext ) as { scrollProgress?: number }; + + return ( +
+
+
+ ); +} diff --git a/src/blocks/carousel/progress/index.ts b/src/blocks/carousel/progress/index.ts new file mode 100644 index 0000000..104f097 --- /dev/null +++ b/src/blocks/carousel/progress/index.ts @@ -0,0 +1,11 @@ +import { registerBlockType, type BlockConfiguration } from '@wordpress/blocks'; +import Edit from './edit'; +import Save from './save'; +import metadata from './block.json'; +import type { CarouselProgressAttributes } from '../types'; +import './style.scss'; + +registerBlockType( metadata as BlockConfiguration, { + edit: Edit, + save: Save, +} ); diff --git a/src/blocks/carousel/progress/save.tsx b/src/blocks/carousel/progress/save.tsx new file mode 100644 index 0000000..7fb62a3 --- /dev/null +++ b/src/blocks/carousel/progress/save.tsx @@ -0,0 +1,17 @@ +import { useBlockProps } from '@wordpress/block-editor'; + +export default function Save() { + return ( +
+
+
+ ); +} diff --git a/src/blocks/carousel/progress/style.scss b/src/blocks/carousel/progress/style.scss new file mode 100644 index 0000000..87f9061 --- /dev/null +++ b/src/blocks/carousel/progress/style.scss @@ -0,0 +1,16 @@ +.carousel-kit-progress { + width: 100%; + height: 4px; + background-color: #eee; + overflow: hidden; + position: relative; + margin-top: 10px; + + &__bar { + height: 100%; + background-color: #000; + width: 0; + transition: width 0.1s ease-out; + transform-origin: left; + } +} diff --git a/src/blocks/carousel/types.ts b/src/blocks/carousel/types.ts index c59eba4..0339acc 100644 --- a/src/blocks/carousel/types.ts +++ b/src/blocks/carousel/types.ts @@ -23,6 +23,7 @@ export type CarouselViewportAttributes = Record; export type CarouselSlideAttributes = Record; export type CarouselControlsAttributes = Record; export type CarouselDotsAttributes = Record; +export type CarouselProgressAttributes = Record; /** * Typed subset of the block editor store selectors used in this plugin. @@ -51,6 +52,7 @@ export type CarouselContext = { scrollSnaps: { index: number }[]; canScrollPrev: boolean; canScrollNext: boolean; + scrollProgress: number; ariaLabelPattern: string; ref?: HTMLElement | null; }; diff --git a/src/blocks/carousel/view.ts b/src/blocks/carousel/view.ts index 293aa22..f270609 100644 --- a/src/blocks/carousel/view.ts +++ b/src/blocks/carousel/view.ts @@ -133,6 +133,12 @@ store( 'carousel-kit/carousel', { const index = ( snap?.index || 0 ) + 1; return context.ariaLabelPattern.replace( '%d', index.toString() ); }, + getProgressBarStyle: () => { + const { scrollProgress } = getContext(); + return { + width: `${ ( scrollProgress || 0 ) * 100 }%`, + }; + }, initCarousel: () => { try { const context = getContext(); @@ -220,10 +226,14 @@ store( 'carousel-kit/carousel', { context.scrollSnaps = embla .scrollSnapList() .map( ( _, index ) => ( { index } ) ); + context.scrollProgress = embla.scrollProgress(); }; embla.on( 'select', updateState ); embla.on( 'reInit', updateState ); + embla.on( 'scroll', () => { + context.scrollProgress = embla.scrollProgress(); + } ); // Autoplay API Integration embla.on( 'autoplay:timerset', () => { From 30cdeba71a3b58c56ae7771c52e43689ad5facfc Mon Sep 17 00:00:00 2001 From: Masud Rana Date: Thu, 26 Mar 2026 13:26:43 +0600 Subject: [PATCH 2/5] feat: implement scroll progress tracking for carousel component --- src/blocks/carousel/edit.tsx | 30 ++++++++++++++++++++++++- src/blocks/carousel/editor-context.ts | 4 ++++ src/blocks/carousel/progress/edit.tsx | 10 ++++++++- src/blocks/carousel/progress/save.tsx | 5 +++++ src/blocks/carousel/progress/style.scss | 4 ++-- 5 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/blocks/carousel/edit.tsx b/src/blocks/carousel/edit.tsx index 3b0d652..fa7a5f8 100644 --- a/src/blocks/carousel/edit.tsx +++ b/src/blocks/carousel/edit.tsx @@ -20,7 +20,7 @@ import { } from '@wordpress/components'; import { plus } from '@wordpress/icons'; import { useSelect, useDispatch } from '@wordpress/data'; -import { useState, useMemo, useCallback } from '@wordpress/element'; +import { useState, useMemo, useCallback, useEffect } from '@wordpress/element'; import { createBlock, type BlockConfiguration } from '@wordpress/blocks'; import type { CarouselAttributes } from './types'; import { EditorCarouselContext } from './editor-context'; @@ -55,6 +55,7 @@ export default function Edit( { const [ emblaApi, setEmblaApi ] = useState(); const [ canScrollPrev, setCanScrollPrev ] = useState( false ); const [ canScrollNext, setCanScrollNext ] = useState( false ); + const [ scrollProgress, setScrollProgress ] = useState( 0 ); const { replaceInnerBlocks, insertBlock } = useDispatch( 'core/block-editor' ); @@ -126,19 +127,46 @@ export default function Edit( { setCanScrollPrev, canScrollNext, setCanScrollNext, + scrollProgress, + setScrollProgress, carouselOptions, } ), [ emblaApi, canScrollPrev, canScrollNext, + scrollProgress, carouselOptions, setEmblaApi, setCanScrollPrev, setCanScrollNext, + setScrollProgress, ], ); + // Subscribe to Embla scroll event to update scrollProgress + useEffect(() => { + if (!emblaApi) return; + + const updateScrollProgress = () => { + setScrollProgress(emblaApi.scrollProgress()); + }; + + emblaApi + .on('scroll', updateScrollProgress) + .on('select', updateScrollProgress) + .on('reInit', updateScrollProgress); + + updateScrollProgress(); + + return () => { + emblaApi + .off('scroll', updateScrollProgress) + .off('select', updateScrollProgress) + .off('reInit', updateScrollProgress); + }; + }, [emblaApi]); + const createNavGroup = () => createBlock( 'core/group', diff --git a/src/blocks/carousel/editor-context.ts b/src/blocks/carousel/editor-context.ts index 9937ba0..778497c 100644 --- a/src/blocks/carousel/editor-context.ts +++ b/src/blocks/carousel/editor-context.ts @@ -9,6 +9,8 @@ export type EditorCarouselContextType = { canScrollNext: boolean; setCanScrollPrev: ( value: boolean ) => void; setCanScrollNext: ( value: boolean ) => void; + scrollProgress: number; + setScrollProgress: ( value: number ) => void; carouselOptions: Omit, 'slidesToScroll'> & { slidesToScroll?: number | string; }; @@ -22,6 +24,8 @@ const defaultValue: EditorCarouselContextType = { canScrollNext: false, setCanScrollPrev: () => {}, setCanScrollNext: () => {}, + scrollProgress: 0, + setScrollProgress: () => {}, carouselOptions: {}, }; diff --git a/src/blocks/carousel/progress/edit.tsx b/src/blocks/carousel/progress/edit.tsx index 7293e4b..24a804b 100644 --- a/src/blocks/carousel/progress/edit.tsx +++ b/src/blocks/carousel/progress/edit.tsx @@ -8,12 +8,20 @@ export default function Edit() { className: 'carousel-kit-progress', } ); - const { scrollProgress } = useContext( EditorCarouselContext ) as { scrollProgress?: number }; + const { scrollProgress, slideCount } = useContext( EditorCarouselContext ) as { scrollProgress?: number, slideCount?: number }; + + // Hide if only one slide + if (slideCount === 1) return null; return (
diff --git a/src/blocks/carousel/progress/style.scss b/src/blocks/carousel/progress/style.scss index 87f9061..a4b4a48 100644 --- a/src/blocks/carousel/progress/style.scss +++ b/src/blocks/carousel/progress/style.scss @@ -1,14 +1,14 @@ .carousel-kit-progress { width: 100%; height: 4px; - background-color: #eee; + background-color: var(--carousel-kit-progress-bg-color, rgb(221, 221, 221)); overflow: hidden; position: relative; margin-top: 10px; &__bar { height: 100%; - background-color: #000; + background-color: var(--carousel-kit-progress-color, rgba(28, 28, 28, 1)); width: 0; transition: width 0.1s ease-out; transform-origin: left; From 3a3b8360b0200bdf8cb632dcafcf032257e0d935 Mon Sep 17 00:00:00 2001 From: Masud Rana Date: Tue, 31 Mar 2026 22:04:26 +0600 Subject: [PATCH 3/5] feat: enhance carousel progress tracking with selected index and slide count --- src/blocks/carousel/edit.tsx | 42 ++++++++++++++++--------- src/blocks/carousel/editor-context.ts | 4 +++ src/blocks/carousel/progress/edit.tsx | 20 +++++++----- src/blocks/carousel/progress/save.tsx | 5 +-- src/blocks/carousel/progress/style.scss | 8 +++-- src/blocks/carousel/types.ts | 1 + src/blocks/carousel/view.ts | 32 ++++++++++++------- 7 files changed, 75 insertions(+), 37 deletions(-) diff --git a/src/blocks/carousel/edit.tsx b/src/blocks/carousel/edit.tsx index fa7a5f8..1f43b4e 100644 --- a/src/blocks/carousel/edit.tsx +++ b/src/blocks/carousel/edit.tsx @@ -56,6 +56,8 @@ export default function Edit( { const [ canScrollPrev, setCanScrollPrev ] = useState( false ); const [ canScrollNext, setCanScrollNext ] = useState( false ); const [ scrollProgress, setScrollProgress ] = useState( 0 ); + const [ selectedIndex, setSelectedIndex ] = useState( 0 ); + const [ slideCount, setSlideCount ] = useState( 0 ); const { replaceInnerBlocks, insertBlock } = useDispatch( 'core/block-editor' ); @@ -129,6 +131,8 @@ export default function Edit( { setCanScrollNext, scrollProgress, setScrollProgress, + selectedIndex, + slideCount, carouselOptions, } ), [ @@ -136,6 +140,8 @@ export default function Edit( { canScrollPrev, canScrollNext, scrollProgress, + selectedIndex, + slideCount, carouselOptions, setEmblaApi, setCanScrollPrev, @@ -144,28 +150,36 @@ export default function Edit( { ], ); - // Subscribe to Embla scroll event to update scrollProgress - useEffect(() => { - if (!emblaApi) return; + // Subscribe to Embla events to update scrollProgress, selectedIndex, and slideCount + useEffect( () => { + if ( ! emblaApi ) { + return; + } const updateScrollProgress = () => { - setScrollProgress(emblaApi.scrollProgress()); + setScrollProgress( emblaApi.scrollProgress() ); + }; + + const updateState = () => { + setSelectedIndex( emblaApi.selectedScrollSnap() ); + setSlideCount( emblaApi.slideNodes().length ); + updateScrollProgress(); }; emblaApi - .on('scroll', updateScrollProgress) - .on('select', updateScrollProgress) - .on('reInit', updateScrollProgress); + .on( 'scroll', updateScrollProgress ) + .on( 'select', updateState ) + .on( 'reInit', updateState ); - updateScrollProgress(); + updateState(); return () => { emblaApi - .off('scroll', updateScrollProgress) - .off('select', updateScrollProgress) - .off('reInit', updateScrollProgress); + .off( 'scroll', updateScrollProgress ) + .off( 'select', updateState ) + .off( 'reInit', updateState ); }; - }, [emblaApi]); + }, [ emblaApi ] ); const createNavGroup = () => createBlock( @@ -183,8 +197,8 @@ export default function Edit( { ], ); - const handleSetup = ( slideCount: number ) => { - const slides = Array.from( { length: slideCount }, () => + const handleSetup = ( count: number ) => { + const slides = Array.from( { length: count }, () => createBlock( 'carousel-kit/carousel-slide', {}, [ createBlock( 'core/paragraph', {} ), ] ), diff --git a/src/blocks/carousel/editor-context.ts b/src/blocks/carousel/editor-context.ts index 778497c..b28016f 100644 --- a/src/blocks/carousel/editor-context.ts +++ b/src/blocks/carousel/editor-context.ts @@ -11,6 +11,8 @@ export type EditorCarouselContextType = { setCanScrollNext: ( value: boolean ) => void; scrollProgress: number; setScrollProgress: ( value: number ) => void; + selectedIndex: number; + slideCount: number; carouselOptions: Omit, 'slidesToScroll'> & { slidesToScroll?: number | string; }; @@ -26,6 +28,8 @@ const defaultValue: EditorCarouselContextType = { setCanScrollNext: () => {}, scrollProgress: 0, setScrollProgress: () => {}, + selectedIndex: 0, + slideCount: 0, carouselOptions: {}, }; diff --git a/src/blocks/carousel/progress/edit.tsx b/src/blocks/carousel/progress/edit.tsx index 24a804b..9fab486 100644 --- a/src/blocks/carousel/progress/edit.tsx +++ b/src/blocks/carousel/progress/edit.tsx @@ -8,10 +8,16 @@ export default function Edit() { className: 'carousel-kit-progress', } ); - const { scrollProgress, slideCount } = useContext( EditorCarouselContext ) as { scrollProgress?: number, slideCount?: number }; + const { scrollProgress, selectedIndex, slideCount, carouselOptions } = + useContext( EditorCarouselContext ); - // Hide if only one slide - if (slideCount === 1) return null; + if ( slideCount <= 1 ) { + return null; + } + + const progress = carouselOptions?.loop + ? selectedIndex / ( slideCount - 1 ) + : Math.max( 0, Math.min( 1, scrollProgress || 0 ) ); return (
@@ -19,11 +25,11 @@ export default function Edit() { className="carousel-kit-progress__bar" role="progressbar" aria-label={ __( 'Carousel progress', 'carousel-kit' ) } - aria-valuenow={ Math.round((scrollProgress || 0) * 100) } - aria-valuemin={0} - aria-valuemax={100} + aria-valuenow={ Math.round( progress * 100 ) } + aria-valuemin={ 0 } + aria-valuemax={ 100 } style={ { - width: `${ ( scrollProgress || 0 ) * 100 }%`, + transform: `translate3d(${ progress * 100 }%, 0px, 0px)`, } } />
diff --git a/src/blocks/carousel/progress/save.tsx b/src/blocks/carousel/progress/save.tsx index bc3b910..31f57c0 100644 --- a/src/blocks/carousel/progress/save.tsx +++ b/src/blocks/carousel/progress/save.tsx @@ -13,8 +13,9 @@ export default function Save() { className="carousel-kit-progress__bar" role="progressbar" aria-label={ __( 'Carousel progress', 'carousel-kit' ) } - aria-valuemin={0} - aria-valuemax={100} + aria-valuemin={ 0 } + aria-valuemax={ 100 } + data-wp-bind--aria-valuenow="callbacks.getProgressBarNow" data-wp-bind--style="callbacks.getProgressBarStyle" />
diff --git a/src/blocks/carousel/progress/style.scss b/src/blocks/carousel/progress/style.scss index a4b4a48..77c55ec 100644 --- a/src/blocks/carousel/progress/style.scss +++ b/src/blocks/carousel/progress/style.scss @@ -7,10 +7,12 @@ margin-top: 10px; &__bar { + position: absolute; + top: 0; + left: -100%; height: 100%; + width: 100%; background-color: var(--carousel-kit-progress-color, rgba(28, 28, 28, 1)); - width: 0; - transition: width 0.1s ease-out; - transform-origin: left; + transition: transform 0.4s ease-out; } } diff --git a/src/blocks/carousel/types.ts b/src/blocks/carousel/types.ts index 0339acc..4b23903 100644 --- a/src/blocks/carousel/types.ts +++ b/src/blocks/carousel/types.ts @@ -55,4 +55,5 @@ export type CarouselContext = { scrollProgress: number; ariaLabelPattern: string; ref?: HTMLElement | null; + slideCount: number; }; diff --git a/src/blocks/carousel/view.ts b/src/blocks/carousel/view.ts index f270609..ccb1695 100644 --- a/src/blocks/carousel/view.ts +++ b/src/blocks/carousel/view.ts @@ -42,10 +42,20 @@ const getEmblaFromElement = ( if ( ! viewport ) { return null; } - // EMBLA_KEY is optional, so check if it exists return emblaInstances.get( viewport ) || viewport[ EMBLA_KEY ] || null; }; +const getProgress = (): number => { + const { scrollProgress, slideCount, selectedIndex, options } = getContext(); + if ( ! slideCount || slideCount <= 1 ) { + return 0; + } + if ( options?.loop ) { + return selectedIndex / ( slideCount - 1 ); + } + return Math.max( 0, Math.min( 1, scrollProgress || 0 ) ); +}; + store( 'carousel-kit/carousel', { state: { get canScrollPrev() { @@ -82,7 +92,7 @@ store( 'carousel-kit/carousel', { const context = getContext(); const { snap } = context as CarouselContext & { snap?: { index?: number }; - }; // snap is the iterated item + }; if ( snap && typeof snap.index === 'number' ) { const element = getElementRef( getElement() ); @@ -95,7 +105,6 @@ store( 'carousel-kit/carousel', { }, callbacks: { isSlideActive: () => { - // Check for either standard slide or Query Loop post const slide = getElementRef( getElement() )?.closest?.( '.embla__slide, .wp-block-post', ); @@ -104,7 +113,6 @@ store( 'carousel-kit/carousel', { return false; } - // Filter siblings to find index among valid slides const slides = Array.from( slide.parentElement.children ).filter( ( child: Element ) => child.classList?.contains( 'embla__slide' ) || @@ -133,11 +141,15 @@ store( 'carousel-kit/carousel', { const index = ( snap?.index || 0 ) + 1; return context.ariaLabelPattern.replace( '%d', index.toString() ); }, + getProgressBarNow: () => { + return Math.round( getProgress() * 100 ); + }, getProgressBarStyle: () => { - const { scrollProgress } = getContext(); - return { - width: `${ ( scrollProgress || 0 ) * 100 }%`, - }; + const { slideCount } = getContext(); + if ( ! slideCount || slideCount <= 1 ) { + return 'display:none'; + } + return `transform:translate3d(${ getProgress() * 100 }%, 0px, 0px)`; }, initCarousel: () => { try { @@ -158,7 +170,6 @@ store( 'carousel-kit/carousel', { return; } - // Check for Query Loop container const queryLoopContainer = viewport.querySelector( '.wp-block-post-template', ); @@ -166,7 +177,6 @@ store( 'carousel-kit/carousel', { const startEmbla = () => { const rawOptions: EmblaOptionsType = context.options || {}; - // Sanitize options to prevent Embla crashes const align = [ 'start', 'center', 'end' ].includes( rawOptions.align as string, ) @@ -227,6 +237,7 @@ store( 'carousel-kit/carousel', { .scrollSnapList() .map( ( _, index ) => ( { index } ) ); context.scrollProgress = embla.scrollProgress(); + context.slideCount = embla.slideNodes().length; }; embla.on( 'select', updateState ); @@ -235,7 +246,6 @@ store( 'carousel-kit/carousel', { context.scrollProgress = embla.scrollProgress(); } ); - // Autoplay API Integration embla.on( 'autoplay:timerset', () => { context.isPlaying = true; context.timerIterationId = ( context.timerIterationId || 0 ) + 1; From cb0fecfeab753c83091d6755f83e205c27bd1882 Mon Sep 17 00:00:00 2001 From: Masud Rana Date: Tue, 31 Mar 2026 22:04:34 +0600 Subject: [PATCH 4/5] feat: add scroll progress and slide count to carousel context tests and implement progress bar logic --- src/blocks/carousel/__tests__/types.test.ts | 32 ++++++ src/blocks/carousel/__tests__/view.test.ts | 110 ++++++++++++++++++++ 2 files changed, 142 insertions(+) diff --git a/src/blocks/carousel/__tests__/types.test.ts b/src/blocks/carousel/__tests__/types.test.ts index efccbb9..aed1c05 100644 --- a/src/blocks/carousel/__tests__/types.test.ts +++ b/src/blocks/carousel/__tests__/types.test.ts @@ -251,6 +251,8 @@ describe( 'CarouselContext Type', () => { scrollSnaps: [ { index: 0 }, { index: 1 } ], canScrollPrev: false, canScrollNext: true, + scrollProgress: 0, + slideCount: 2, ariaLabelPattern: 'Go to slide %d', }; @@ -275,6 +277,8 @@ describe( 'CarouselContext Type', () => { scrollSnaps: [ { index: 0 }, { index: 1 }, { index: 2 } ], canScrollPrev: true, canScrollNext: true, + scrollProgress: 0.5, + slideCount: 3, ariaLabelPattern: 'Slide %d of 3', }; @@ -300,6 +304,8 @@ describe( 'CarouselContext Type', () => { scrollSnaps: [ { index: 0 }, { index: 1 }, { index: 2 } ], canScrollPrev: false, canScrollNext: true, + scrollProgress: 0, + slideCount: 3, ariaLabelPattern: 'Slide %d', }; @@ -318,6 +324,8 @@ describe( 'CarouselContext Type', () => { scrollSnaps: [ { index: 0 }, { index: 1 }, { index: 2 } ], canScrollPrev: true, canScrollNext: true, + scrollProgress: 0.5, + slideCount: 3, ariaLabelPattern: 'Slide %d', }; @@ -336,6 +344,8 @@ describe( 'CarouselContext Type', () => { scrollSnaps: [ { index: 0 }, { index: 1 }, { index: 2 } ], canScrollPrev: true, canScrollNext: false, + scrollProgress: 1, + slideCount: 3, ariaLabelPattern: 'Slide %d', }; @@ -354,6 +364,8 @@ describe( 'CarouselContext Type', () => { scrollSnaps: [ { index: 0 } ], canScrollPrev: false, canScrollNext: false, + scrollProgress: 0, + slideCount: 1, ariaLabelPattern: 'Slide %d', }; @@ -374,6 +386,8 @@ describe( 'CarouselContext Type', () => { scrollSnaps: [ { index: 0 } ], canScrollPrev: false, canScrollNext: false, + scrollProgress: 0, + slideCount: 1, ariaLabelPattern: 'Slide %d', }; @@ -394,6 +408,8 @@ describe( 'CarouselContext Type', () => { scrollSnaps: [], canScrollPrev: false, canScrollNext: false, + scrollProgress: 0, + slideCount: 0, ariaLabelPattern: 'Slide %d', }; @@ -414,6 +430,8 @@ describe( 'CarouselContext Type', () => { scrollSnaps: [], canScrollPrev: false, canScrollNext: false, + scrollProgress: 0, + slideCount: 0, ariaLabelPattern: 'Slide %d', ref: element, }; @@ -432,6 +450,8 @@ describe( 'CarouselContext Type', () => { scrollSnaps: [], canScrollPrev: false, canScrollNext: false, + scrollProgress: 0, + slideCount: 0, ariaLabelPattern: 'Slide %d', ref: null, }; @@ -449,6 +469,8 @@ describe( 'CarouselContext Type', () => { scrollSnaps: [], canScrollPrev: false, canScrollNext: false, + scrollProgress: 0, + slideCount: 0, ariaLabelPattern: 'Slide %d', }; @@ -470,6 +492,8 @@ describe( 'CarouselContext Type', () => { scrollSnaps: [], canScrollPrev: false, canScrollNext: false, + scrollProgress: 0, + slideCount: 0, ariaLabelPattern: 'Slide %d', }; @@ -489,6 +513,8 @@ describe( 'CarouselContext Type', () => { scrollSnaps: [], canScrollPrev: false, canScrollNext: false, + scrollProgress: 0, + slideCount: 0, ariaLabelPattern: 'Slide %d', }; @@ -515,6 +541,8 @@ describe( 'CarouselContext Type', () => { scrollSnaps: [], canScrollPrev: false, canScrollNext: false, + scrollProgress: 0, + slideCount: 0, ariaLabelPattern: pattern, }; @@ -534,6 +562,8 @@ describe( 'CarouselContext Type', () => { scrollSnaps: [], canScrollPrev: false, canScrollNext: false, + scrollProgress: 0, + slideCount: 0, ariaLabelPattern: 'Slide %d', }; @@ -558,6 +588,8 @@ describe( 'CarouselContext Type', () => { scrollSnaps, canScrollPrev: false, canScrollNext: false, + scrollProgress: 0, + slideCount: 5, ariaLabelPattern: 'Slide %d', }; diff --git a/src/blocks/carousel/__tests__/view.test.ts b/src/blocks/carousel/__tests__/view.test.ts index 0fca857..6faf5db 100644 --- a/src/blocks/carousel/__tests__/view.test.ts +++ b/src/blocks/carousel/__tests__/view.test.ts @@ -65,6 +65,8 @@ const createMockContext = ( scrollSnaps: [ { index: 0 }, { index: 1 }, { index: 2 } ], canScrollPrev: true, canScrollNext: true, + scrollProgress: 0, + slideCount: 3, ariaLabelPattern: 'Go to slide %d', ...overrides, } ); @@ -516,6 +518,114 @@ describe( 'Carousel View Module', () => { } ); } ); + describe( 'getProgressBarNow', () => { + it( 'should return 0 when slideCount is 0', () => { + const mockContext = createMockContext( { slideCount: 0 } ); + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + + const result = storeConfig.callbacks.getProgressBarNow(); + expect( result ).toBe( 0 ); + } ); + + it( 'should return 0 when slideCount is 1', () => { + const mockContext = createMockContext( { slideCount: 1 } ); + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + + const result = storeConfig.callbacks.getProgressBarNow(); + expect( result ).toBe( 0 ); + } ); + + it( 'should use index-based progress in loop mode', () => { + const mockContext = createMockContext( { + options: { loop: true }, + selectedIndex: 1, + slideCount: 3, + } ); + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + + const result = storeConfig.callbacks.getProgressBarNow(); + expect( result ).toBe( 50 ); + } ); + + it( 'should return 100 at last slide in loop mode', () => { + const mockContext = createMockContext( { + options: { loop: true }, + selectedIndex: 2, + slideCount: 3, + } ); + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + + const result = storeConfig.callbacks.getProgressBarNow(); + expect( result ).toBe( 100 ); + } ); + + it( 'should use scrollProgress in non-loop mode', () => { + const mockContext = createMockContext( { + options: { loop: false }, + scrollProgress: 0.75, + slideCount: 4, + } ); + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + + const result = storeConfig.callbacks.getProgressBarNow(); + expect( result ).toBe( 75 ); + } ); + + it( 'should clamp scrollProgress to [0, 1] in non-loop mode', () => { + const mockContext = createMockContext( { + options: { loop: false }, + scrollProgress: 1.5, + slideCount: 3, + } ); + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + + const result = storeConfig.callbacks.getProgressBarNow(); + expect( result ).toBe( 100 ); + } ); + } ); + + describe( 'getProgressBarStyle', () => { + it( 'should return display:none when slideCount is 0', () => { + const mockContext = createMockContext( { slideCount: 0 } ); + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + + const result = storeConfig.callbacks.getProgressBarStyle(); + expect( result ).toBe( 'display:none' ); + } ); + + it( 'should return display:none when slideCount is 1', () => { + const mockContext = createMockContext( { slideCount: 1 } ); + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + + const result = storeConfig.callbacks.getProgressBarStyle(); + expect( result ).toBe( 'display:none' ); + } ); + + it( 'should return transform style with index-based progress in loop mode', () => { + const mockContext = createMockContext( { + options: { loop: true }, + selectedIndex: 1, + slideCount: 3, + } ); + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + + const result = storeConfig.callbacks.getProgressBarStyle(); + expect( result ).toBe( 'transform:translate3d(50%, 0px, 0px)' ); + } ); + + it( 'should return transform style with scrollProgress in non-loop mode', () => { + const mockContext = createMockContext( { + options: { loop: false }, + scrollProgress: 0.5, + slideCount: 3, + } ); + ( getContext as jest.Mock ).mockReturnValue( mockContext ); + + const result = storeConfig.callbacks.getProgressBarStyle(); + expect( result ).toBe( 'transform:translate3d(50%, 0px, 0px)' ); + } ); + } ); + describe( 'initCarousel', () => { it( 'should be defined as a function', () => { expect( storeConfig?.callbacks?.initCarousel ).toBeDefined(); From b8bd258cd4b9a83232fa3c4c0739f7cf79c80205 Mon Sep 17 00:00:00 2001 From: Masud Rana Date: Tue, 31 Mar 2026 22:20:47 +0600 Subject: [PATCH 5/5] feat: initialize scroll progress and slide count in carousel context --- src/blocks/carousel/save.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/blocks/carousel/save.tsx b/src/blocks/carousel/save.tsx index 45ba944..f6c4cdd 100644 --- a/src/blocks/carousel/save.tsx +++ b/src/blocks/carousel/save.tsx @@ -48,6 +48,8 @@ export default function Save( { scrollSnaps: [], canScrollPrev: false, canScrollNext: false, + scrollProgress: 0, + slideCount: 0, /* translators: %d: slide number */ ariaLabelPattern: __( 'Go to slide %d', 'carousel-kit' ), };