Skip to content

Commit aa7d07d

Browse files
committed
fix: harden embla initialization filters
1 parent e1c4271 commit aa7d07d

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
@@ -914,6 +914,92 @@ describe( 'Carousel View Module', () => {
914914
delete ( window as HooksWindow ).wp;
915915
}
916916
} );
917+
918+
it( 'should keep original Embla options when the options filter returns undefined', () => {
919+
const mockContext = createMockContext( {
920+
options: { duration: 25 },
921+
} );
922+
const { wrapper, viewport } = createMockCarouselDOM();
923+
const mockEmbla = createMockEmblaInstance();
924+
const originalIntersectionObserver = window.IntersectionObserver;
925+
const applyFilters = jest.fn( ( hookName, value ) => {
926+
if ( hookName === 'rtcamp.carouselKit.emblaOptions' ) {
927+
return undefined;
928+
}
929+
return value;
930+
} );
931+
932+
mockVisibleViewport( viewport );
933+
934+
( window as HooksWindow ).wp = {
935+
hooks: {
936+
applyFilters,
937+
},
938+
};
939+
( getContext as jest.Mock ).mockReturnValue( mockContext );
940+
( getElement as jest.Mock ).mockReturnValue( { ref: wrapper } );
941+
( EmblaCarousel as unknown as jest.Mock ).mockReturnValue( mockEmbla );
942+
delete ( window as Window & { IntersectionObserver?: typeof IntersectionObserver } ).IntersectionObserver;
943+
944+
try {
945+
storeConfig.callbacks.initCarousel();
946+
947+
expect( EmblaCarousel ).toHaveBeenCalledWith(
948+
viewport,
949+
expect.objectContaining( { duration: 25 } ),
950+
[],
951+
);
952+
} finally {
953+
( window as Window & { IntersectionObserver?: typeof IntersectionObserver } ).IntersectionObserver =
954+
originalIntersectionObserver;
955+
delete ( window as HooksWindow ).wp;
956+
}
957+
} );
958+
959+
it( 'should keep original Embla plugins when the plugins filter returns a non-array value', () => {
960+
const mockContext = createMockContext( {
961+
autoplay: {
962+
delay: 3000,
963+
stopOnInteraction: true,
964+
stopOnMouseEnter: false,
965+
},
966+
} );
967+
const { wrapper, viewport } = createMockCarouselDOM();
968+
const mockEmbla = createMockEmblaInstance();
969+
const originalIntersectionObserver = window.IntersectionObserver;
970+
const applyFilters = jest.fn( ( hookName, value ) => {
971+
if ( hookName === 'rtcamp.carouselKit.emblaPlugins' ) {
972+
return 'not-an-array';
973+
}
974+
return value;
975+
} );
976+
977+
mockVisibleViewport( viewport );
978+
979+
( window as HooksWindow ).wp = {
980+
hooks: {
981+
applyFilters,
982+
},
983+
};
984+
( getContext as jest.Mock ).mockReturnValue( mockContext );
985+
( getElement as jest.Mock ).mockReturnValue( { ref: wrapper } );
986+
( EmblaCarousel as unknown as jest.Mock ).mockReturnValue( mockEmbla );
987+
delete ( window as Window & { IntersectionObserver?: typeof IntersectionObserver } ).IntersectionObserver;
988+
989+
try {
990+
storeConfig.callbacks.initCarousel();
991+
992+
expect( EmblaCarousel ).toHaveBeenCalledWith(
993+
viewport,
994+
expect.any( Object ),
995+
[ expect.objectContaining( { name: 'autoplay' } ) ],
996+
);
997+
} finally {
998+
( window as Window & { IntersectionObserver?: typeof IntersectionObserver } ).IntersectionObserver =
999+
originalIntersectionObserver;
1000+
delete ( window as HooksWindow ).wp;
1001+
}
1002+
} );
9171003
} );
9181004
} );
9191005
} );

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 = (
@@ -380,8 +381,11 @@ store( 'rt-carousel/carousel', {
380381
plugins,
381382
{ ...filterContext, options: filteredOptions },
382383
);
384+
const safePlugins = Array.isArray( filteredPlugins )
385+
? filteredPlugins
386+
: plugins;
383387

384-
const embla = EmblaCarousel( viewport, filteredOptions, filteredPlugins );
388+
const embla = EmblaCarousel( viewport, filteredOptions, safePlugins );
385389

386390
emblaInstances.set( viewport, embla );
387391
viewport[ EMBLA_KEY ] = embla;
@@ -425,7 +429,7 @@ store( 'rt-carousel/carousel', {
425429
{
426430
...filterContext,
427431
options: filteredOptions,
428-
plugins: filteredPlugins,
432+
plugins: safePlugins,
429433
},
430434
);
431435

0 commit comments

Comments
 (0)