fix: Ensure media info is dispatched after view change (#1587)

This commit is contained in:
Dermot Duffy
2024-09-25 21:48:36 -07:00
committed by GitHub
parent 4e3b16f9b8
commit f560113031
4 changed files with 66 additions and 24 deletions
+12 -11
View File
@@ -88,6 +88,18 @@ export class CarouselController {
public selectSlide(index: number): void { public selectSlide(index: number): void {
this._carousel.scrollTo(index, this._transitionEffect === 'none'); this._carousel.scrollTo(index, this._transitionEffect === 'none');
// This event exists to allow the caller to know the difference between
// programatically force slide selections and user-driven slide selections
// (e.g. carousel drags). See the note in auto-media-loaded-info.ts on how
// this is used.
const newSlide = this.getSlide(index);
if (newSlide) {
dispatchFrigateCardEvent<CarouselSelected>(this._parent, 'carousel:force-select', {
index: index,
element: newSlide,
});
}
} }
protected _refreshCarouselContents = (): void => { protected _refreshCarouselContents = (): void => {
@@ -162,17 +174,6 @@ export class CarouselController {
}; };
carousel.on('select', () => selectSlide()); carousel.on('select', () => selectSlide());
carousel.on('settle', () => {
const carouselSelected = getCarouselSelectedObject();
if (carouselSelected) {
dispatchFrigateCardEvent<CarouselSelected>(
this._parent,
'carousel:settle',
carouselSelected,
);
}
});
return carousel; return carousel;
} }
} }
@@ -13,6 +13,30 @@ declare module 'embla-carousel/components/Plugins' {
} }
} }
/**
* On the relationship between carousel:select and carousel:force-select:
*
* There is a complex interplay here. `carousel:force-select` is an event
* dispatched by the carousel when it is forced to select a particular slide
* (i.e. the view has changed). `carousel:select` is dispatched for any
* selection -- forced or human (e.g. the user dragging the carousel).
*
* The media info should only be dispatched _after_ the view object has been
* updated (since the view will clear the loaded media info). The setting of the
* view (trigged by `carousel:select`) may require async fetches and may take a
* while -- and so if the card dispatched media on `carousel:selecte` then the
* media info may be dispatched before the view is set (which could result in
* the dispatched media immediately being cleared by the view).
*
* It is fine to have media info dispatched from the `carousel:init` event,
* since the carousel will be initialized based on a particular view object. In
* practice, the carousel will be initialized before the media is loaded, so
* there may not be anything to dispatch at that point.
*
* When media is loaded, that media loaded info will always be allowed to
* propogate upwards as long as it is selected.
*/
type AutoMediaLoadedInfoType = CreatePluginType<LoosePluginType, LooseOptionsType>; type AutoMediaLoadedInfoType = CreatePluginType<LoosePluginType, LooseOptionsType>;
function AutoMediaLoadedInfo(): AutoMediaLoadedInfoType { function AutoMediaLoadedInfo(): AutoMediaLoadedInfoType {
@@ -30,7 +54,9 @@ function AutoMediaLoadedInfo(): AutoMediaLoadedInfoType {
} }
emblaApi.on('init', slideSelectHandler); emblaApi.on('init', slideSelectHandler);
emblaApi.on('select', slideSelectHandler); emblaApi
.containerNode()
.addEventListener('frigate-card:carousel:force-select', slideSelectHandler);
} }
function destroy(): void { function destroy(): void {
@@ -40,7 +66,9 @@ function AutoMediaLoadedInfo(): AutoMediaLoadedInfoType {
} }
emblaApi.off('init', slideSelectHandler); emblaApi.off('init', slideSelectHandler);
emblaApi.off('select', slideSelectHandler); emblaApi
.containerNode()
.removeEventListener('frigate-card:carousel:force-select', slideSelectHandler);
} }
function mediaLoadedInfoHandler(ev: CustomEvent<MediaLoadedInfo>): void { function mediaLoadedInfoHandler(ev: CustomEvent<MediaLoadedInfo>): void {
+18 -7
View File
@@ -110,24 +110,35 @@ describe('CarouselController', () => {
it('should select given slide', () => { it('should select given slide', () => {
const children = createTestSlideNodes(); const children = createTestSlideNodes();
const parent = createParent({ children: children }); const parent = createParent({ children: children });
const forceSelectListener = vi.fn();
parent.addEventListener('frigate-card:carousel:force-select', forceSelectListener);
const carousel = new CarouselController(createRoot(), parent); const carousel = new CarouselController(createRoot(), parent);
carousel.selectSlide(4); carousel.selectSlide(4);
expect(getEmblaApi()?.scrollTo).toBeCalledWith(4, false); expect(getEmblaApi()?.scrollTo).toBeCalledWith(4, false);
expect(forceSelectListener).toBeCalledWith(
expect.objectContaining({
detail: { index: 4, element: children[4] },
}),
);
}); });
it('should dispatch settle event', () => { it('should not select non-existent slide', () => {
const children = createTestSlideNodes(); const children = createTestSlideNodes({ n: 10 });
const parent = createParent({ children: children }); const parent = createParent({ children: children });
new CarouselController(createRoot(), parent);
const settleHandler = vi.fn(); const forceSelectListener = vi.fn();
parent.addEventListener('frigate-card:carousel:settle', settleHandler); parent.addEventListener('frigate-card:carousel:force-select', forceSelectListener);
callEmblaHandler(getEmblaApi(), 'settle'); const carousel = new CarouselController(createRoot(), parent);
expect(settleHandler).toBeCalled(); carousel.selectSlide(11);
expect(getEmblaApi()?.scrollTo).toBeCalledWith(11, false);
expect(forceSelectListener).not.toBeCalled();
}); });
it('should dispatch select event', () => { it('should dispatch select event', () => {
@@ -6,7 +6,6 @@ import {
} from '../../../../../src/utils/media-info'; } from '../../../../../src/utils/media-info';
import { createMediaLoadedInfo, createParent } from '../../../../test-utils'; import { createMediaLoadedInfo, createParent } from '../../../../test-utils';
import { import {
callEmblaHandler,
createEmblaApiInstance, createEmblaApiInstance,
createTestEmblaOptionHandler, createTestEmblaOptionHandler,
createTestSlideNodes, createTestSlideNodes,
@@ -30,7 +29,6 @@ describe('AutoMediaLoadedInfo', () => {
plugin.destroy(); plugin.destroy();
expect(emblaApi.off).toBeCalledWith('init', expect.anything()); expect(emblaApi.off).toBeCalledWith('init', expect.anything());
expect(emblaApi.off).toBeCalledWith('select', expect.anything());
}); });
describe('should correctly propogate media load/unload depending on whether media is currently selected', () => { describe('should correctly propogate media load/unload depending on whether media is currently selected', () => {
@@ -82,11 +80,15 @@ describe('AutoMediaLoadedInfo', () => {
dispatchExistingMediaLoadedInfoAsEvent(children[5], createMediaLoadedInfo()); dispatchExistingMediaLoadedInfoAsEvent(children[5], createMediaLoadedInfo());
vi.mocked(emblaApi.selectedScrollSnap).mockReturnValue(4); vi.mocked(emblaApi.selectedScrollSnap).mockReturnValue(4);
callEmblaHandler(emblaApi, 'select'); emblaApi
.containerNode()
.dispatchEvent(new Event('frigate-card:carousel:force-select'));
expect(mediaLoadedHandler).not.toBeCalled(); expect(mediaLoadedHandler).not.toBeCalled();
vi.mocked(emblaApi.selectedScrollSnap).mockReturnValue(5); vi.mocked(emblaApi.selectedScrollSnap).mockReturnValue(5);
callEmblaHandler(emblaApi, 'select'); emblaApi
.containerNode()
.dispatchEvent(new Event('frigate-card:carousel:force-select'));
expect(mediaLoadedHandler).toBeCalled(); expect(mediaLoadedHandler).toBeCalled();
}); });
}); });