Skip to content

Commit 6a2b1e8

Browse files
committed
fix: harden embla initialization filters
1 parent 1a0dd9c commit 6a2b1e8

3 files changed

Lines changed: 94 additions & 4 deletions

File tree

docs/API.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ addFilter(
2626
);
2727
```
2828

29-
Both filters receive the carousel context object as the third argument. `rtcamp.carouselKit.emblaPlugins` also receives the filtered options on `options`.
29+
Both filter callbacks receive the filtered value as their first argument and the filter context object as their second argument. `rtcamp.carouselKit.emblaPlugins` also receives the filtered options on the `options` property of this object.
3030

3131
rtCarousel also exposes an action after Embla has initialized so integrations can call Embla methods or subscribe to Embla events:
3232

src/blocks/carousel/__tests__/view.test.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -863,6 +863,92 @@ describe( 'Carousel View Module', () => {
863863
delete ( window as HooksWindow ).wp;
864864
}
865865
} );
866+
867+
it( 'should keep original Embla options when the options filter returns undefined', () => {
868+
const mockContext = createMockContext( {
869+
options: { duration: 25 },
870+
} );
871+
const { wrapper, viewport } = createMockCarouselDOM();
872+
const mockEmbla = createMockEmblaInstance();
873+
const originalIntersectionObserver = window.IntersectionObserver;
874+
const applyFilters = jest.fn( ( hookName, value ) => {
875+
if ( hookName === 'rtcamp.carouselKit.emblaOptions' ) {
876+
return undefined;
877+
}
878+
return value;
879+
} );
880+
881+
mockVisibleViewport( viewport );
882+
883+
( window as HooksWindow ).wp = {
884+
hooks: {
885+
applyFilters,
886+
},
887+
};
888+
( getContext as jest.Mock ).mockReturnValue( mockContext );
889+
( getElement as jest.Mock ).mockReturnValue( { ref: wrapper } );
890+
( EmblaCarousel as unknown as jest.Mock ).mockReturnValue( mockEmbla );
891+
delete ( window as Window & { IntersectionObserver?: typeof IntersectionObserver } ).IntersectionObserver;
892+
893+
try {
894+
storeConfig.callbacks.initCarousel();
895+
896+
expect( EmblaCarousel ).toHaveBeenCalledWith(
897+
viewport,
898+
expect.objectContaining( { duration: 25 } ),
899+
[],
900+
);
901+
} finally {
902+
( window as Window & { IntersectionObserver?: typeof IntersectionObserver } ).IntersectionObserver =
903+
originalIntersectionObserver;
904+
delete ( window as HooksWindow ).wp;
905+
}
906+
} );
907+
908+
it( 'should keep original Embla plugins when the plugins filter returns a non-array value', () => {
909+
const mockContext = createMockContext( {
910+
autoplay: {
911+
delay: 3000,
912+
stopOnInteraction: true,
913+
stopOnMouseEnter: false,
914+
},
915+
} );
916+
const { wrapper, viewport } = createMockCarouselDOM();
917+
const mockEmbla = createMockEmblaInstance();
918+
const originalIntersectionObserver = window.IntersectionObserver;
919+
const applyFilters = jest.fn( ( hookName, value ) => {
920+
if ( hookName === 'rtcamp.carouselKit.emblaPlugins' ) {
921+
return 'not-an-array';
922+
}
923+
return value;
924+
} );
925+
926+
mockVisibleViewport( viewport );
927+
928+
( window as HooksWindow ).wp = {
929+
hooks: {
930+
applyFilters,
931+
},
932+
};
933+
( getContext as jest.Mock ).mockReturnValue( mockContext );
934+
( getElement as jest.Mock ).mockReturnValue( { ref: wrapper } );
935+
( EmblaCarousel as unknown as jest.Mock ).mockReturnValue( mockEmbla );
936+
delete ( window as Window & { IntersectionObserver?: typeof IntersectionObserver } ).IntersectionObserver;
937+
938+
try {
939+
storeConfig.callbacks.initCarousel();
940+
941+
expect( EmblaCarousel ).toHaveBeenCalledWith(
942+
viewport,
943+
expect.any( Object ),
944+
[ expect.objectContaining( { name: 'autoplay' } ) ],
945+
);
946+
} finally {
947+
( window as Window & { IntersectionObserver?: typeof IntersectionObserver } ).IntersectionObserver =
948+
originalIntersectionObserver;
949+
delete ( window as HooksWindow ).wp;
950+
}
951+
} );
866952
} );
867953
} );
868954
} );

src/blocks/carousel/view.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ const applyEmblaFilter = <T>(
5656
return value;
5757
}
5858

59-
return applyFilters( hookName, value, filterContext ) as T;
59+
const result = applyFilters( hookName, value, filterContext );
60+
return result !== null && result !== undefined ? ( result as T ) : value;
6061
};
6162

6263
const doEmblaAction = (
@@ -339,8 +340,11 @@ store( 'rt-carousel/carousel', {
339340
plugins,
340341
{ ...filterContext, options: filteredOptions },
341342
);
343+
const safePlugins = Array.isArray( filteredPlugins )
344+
? filteredPlugins
345+
: plugins;
342346

343-
const embla = EmblaCarousel( viewport, filteredOptions, filteredPlugins );
347+
const embla = EmblaCarousel( viewport, filteredOptions, safePlugins );
344348

345349
emblaInstances.set( viewport, embla );
346350
viewport[ EMBLA_KEY ] = embla;
@@ -384,7 +388,7 @@ store( 'rt-carousel/carousel', {
384388
{
385389
...filterContext,
386390
options: filteredOptions,
387-
plugins: filteredPlugins,
391+
plugins: safePlugins,
388392
},
389393
);
390394

0 commit comments

Comments
 (0)