diff --git a/src/modules/esl-carousel/core/esl-carousel.events.ts b/src/modules/esl-carousel/core/esl-carousel.events.ts index 158645e07..07dfa3e62 100644 --- a/src/modules/esl-carousel/core/esl-carousel.events.ts +++ b/src/modules/esl-carousel/core/esl-carousel.events.ts @@ -3,10 +3,10 @@ import type {ESLCarouselDirection, ESLCarouselStaticState} from './nav/esl-carou /** {@link ESLCarouselSlideEvent} init object */ export interface ESLCarouselSlideEventInit { - /** Current slide index */ - current: number; - /** Related slide index (target on pre-event, current on post-event) */ - related: number; + /** A list of indexes of slides that were active before the change */ + indexesBefore: number[]; + /** A list of indexes of slides that are active after the change */ + indexesAfter: number[]; /** Direction of slide animation */ direction: ESLCarouselDirection; /** Auxiliary request attribute that represents object that initiates slide change */ @@ -21,8 +21,8 @@ export class ESLCarouselSlideEvent extends Event implements ESLCarouselSlideEven public static readonly AFTER = 'esl:slide-change'; public override readonly target: ESLCarousel; - public readonly current: number; - public readonly related: number; + public readonly indexesBefore: number[]; + public readonly indexesAfter: number[]; public readonly direction: ESLCarouselDirection; public readonly activator?: any; @@ -38,14 +38,24 @@ export class ESLCarouselSlideEvent extends Event implements ESLCarouselSlideEven Object.assign(this, init); } - /** @returns current slide element */ - public get $currentSlide(): HTMLElement | null { - return this.target.slideAt(this.current); + /** @returns current slide index */ + public get current(): number { + return this.indexesAfter[0]; } - /** @returns related slide element */ - public get $relatedSlide(): HTMLElement | null { - return this.target.slideAt(this.related); + /** @returns related slide index */ + public get related(): number { + return this.indexesBefore[0]; + } + + /** @returns list of slides that are active before the change */ + public get $slidesBefore(): HTMLElement[] { + return this.indexesBefore.map((index) => this.target.slideAt(index)); + } + + /** @returns list of slides that are active after the change */ + public get $slidesAfter(): HTMLElement[] { + return this.indexesAfter.map((index) => this.target.slideAt(index)); } public static create(type: 'BEFORE' | 'AFTER', init: ESLCarouselSlideEventInit): ESLCarouselSlideEvent { diff --git a/src/modules/esl-carousel/core/esl-carousel.renderer.ts b/src/modules/esl-carousel/core/esl-carousel.renderer.ts index c38de614c..16b9b4775 100644 --- a/src/modules/esl-carousel/core/esl-carousel.renderer.ts +++ b/src/modules/esl-carousel/core/esl-carousel.renderer.ts @@ -2,7 +2,7 @@ import {memoize} from '../../esl-utils/decorators'; import {isEqual} from '../../esl-utils/misc/object'; import {SyntheticEventTarget} from '../../esl-utils/dom'; import {ESLCarouselSlideEvent} from './esl-carousel.events'; -import {calcDirection, normalize} from './nav/esl-carousel.nav.utils'; +import {calcDirection, normalize, sequence} from './nav/esl-carousel.nav.utils'; import type {ESLCarousel, ESLCarouselActionParams} from './esl-carousel'; import type {ESLCarouselConfig, ESLCarouselDirection} from './nav/esl-carousel.nav.types'; @@ -99,8 +99,8 @@ export abstract class ESLCarouselRenderer implements ESLCarouselConfig { if (!this.$carousel.dispatchEvent(ESLCarouselSlideEvent.create('BEFORE', { direction, activator, - current: activeIndex, - related: index + indexesBefore: activeIndexes, + indexesAfter: sequence(index, this.count, this.size) }))) return; this.setPreActive(index); @@ -131,11 +131,15 @@ export abstract class ESLCarouselRenderer implements ESLCarouselConfig { /** Sets active slides from passed index **/ public setActive(current: number, event?: Partial): void { const related = this.$carousel.activeIndex; + const indexesBefore = this.$carousel.activeIndexes; const count = Math.min(this.count, this.size); + const indexesAfter = []; for (let i = 0; i < this.size; i++) { const position = normalize(i + current, this.size); const $slide = this.$slides[position]; + if (i < count) indexesAfter.push(position); + $slide.toggleAttribute('active', i < count); $slide.toggleAttribute('pre-active', false); $slide.toggleAttribute('next', i === count && (this.loop || position !== 0)); @@ -144,7 +148,7 @@ export abstract class ESLCarouselRenderer implements ESLCarouselConfig { if (event && typeof event === 'object') { const direction = event.direction || calcDirection(related, current, this.size); - const details = {...event, direction, current, related}; + const details = {...event, direction, indexesBefore, indexesAfter}; this.$carousel.dispatchEvent(ESLCarouselSlideEvent.create('AFTER', details)); } } diff --git a/src/modules/esl-carousel/core/nav/esl-carousel.nav.utils.ts b/src/modules/esl-carousel/core/nav/esl-carousel.nav.utils.ts index bc1863e8a..226404ef5 100644 --- a/src/modules/esl-carousel/core/nav/esl-carousel.nav.utils.ts +++ b/src/modules/esl-carousel/core/nav/esl-carousel.nav.utils.ts @@ -11,11 +11,20 @@ export function normalize(index: number, size: number): number { return (size + (index % size)) % size; } -/** @returns normalize slide index according to the carousel mode */ +/** @returns normalize first slide index according to the carousel mode */ export function normalizeIndex(index: number, {size, count, loop}: ESLCarouselStaticState): number { return loop ? normalize(index, size) : Math.max(0, Math.min(size - count, index)); } +/** @returns normalized sequence of slides starting from the current index */ +export function sequence(current: number, count: number, size: number): number[] { + const result = []; + for (let i = 0; i < count; i++) { + result.push(normalize(current + i, size)); + } + return result; +} + /** Gets count of slides between active and passed considering given direction. */ export function getDistance(from: number, direction: ESLCarouselDirection, {activeIndex, size}: ESLCarouselState): number { if (direction === 'prev') return normalize(activeIndex - from, size); diff --git a/src/modules/esl-carousel/test/common/esl-carousel.dummy.ts b/src/modules/esl-carousel/test/common/esl-carousel.dummy.ts new file mode 100644 index 000000000..c9909246b --- /dev/null +++ b/src/modules/esl-carousel/test/common/esl-carousel.dummy.ts @@ -0,0 +1,23 @@ +import {ESLCarousel} from '../../core/esl-carousel'; +import {ESLCarouselDummyRenderer} from './esl-carousel.dummy.renderer'; + +export function createDummyCarousel(size: number): {$carousel: ESLCarousel, $slides: HTMLElement[]} { + ESLCarousel.register(); + ESLCarouselDummyRenderer.register(); + + const $carousel = ESLCarousel.create(); + const $slides = Array.from({length: size}, () => { + const $el = document.createElement('div'); + $el.setAttribute('esl-carousel-slide', ''); + return $el; + }); + + beforeAll(async () => { + document.body.appendChild($carousel); + await ESLCarousel.registered; + $slides.forEach(($slide) => $carousel.addSlide($slide)); + }); + afterAll(() => document.body.removeChild($carousel)); + + return {$carousel, $slides}; +} diff --git a/src/modules/esl-carousel/test/core/esl-carousel.renderer.test.ts b/src/modules/esl-carousel/test/core/esl-carousel.renderer.test.ts index 4b509b3ec..655090158 100644 --- a/src/modules/esl-carousel/test/core/esl-carousel.renderer.test.ts +++ b/src/modules/esl-carousel/test/core/esl-carousel.renderer.test.ts @@ -1,28 +1,15 @@ import {ESLCarousel} from '../../core/esl-carousel'; +import {createDummyCarousel} from '../common/esl-carousel.dummy'; import {ESLCarouselDummyRenderer} from '../common/esl-carousel.dummy.renderer'; + jest.mock('../../../esl-utils/dom/ready', () => ({ onDocumentReady: (cb: any) => cb() })); describe('ESLCarouselRenderer: base class tests', () => { - ESLCarousel.register(); - ESLCarouselDummyRenderer.register(); - describe('Slide markers defined correctly (`setActive` method of the base class)', () => { - const $carousel = ESLCarousel.create(); - const $slides = Array.from({length: 5}, () => { - const $el = document.createElement('div'); - $el.setAttribute('esl-carousel-slide', ''); - return $el; - }); - - beforeAll(async () => { - document.body.appendChild($carousel); - await ESLCarousel.registered; - $slides.forEach(($slide) => $carousel.addSlide($slide)); - }); - afterAll(() => document.body.removeChild($carousel)); + const {$carousel} = createDummyCarousel(5); test('Carousel size defined correctly', () => { expect($carousel.size).toBe(5); diff --git a/src/modules/esl-carousel/test/core/esl-carousel.slide.events.test.ts b/src/modules/esl-carousel/test/core/esl-carousel.slide.events.test.ts new file mode 100644 index 000000000..1d00fc8f1 --- /dev/null +++ b/src/modules/esl-carousel/test/core/esl-carousel.slide.events.test.ts @@ -0,0 +1,95 @@ +import {createDummyCarousel} from '../common/esl-carousel.dummy'; +import {ESLCarouselSlideEvent} from '../../core/esl-carousel.events'; + +jest.mock('../../../esl-utils/dom/ready', () => ({ + onDocumentReady: (cb: any) => cb() +})); + +describe('ESLCarouselRenderer: Slide change events created correctly', () => { + const beforeEventTrap = jest.fn(); + const afterEventTrap = jest.fn(); + document.addEventListener(ESLCarouselSlideEvent.BEFORE, beforeEventTrap); + document.addEventListener(ESLCarouselSlideEvent.AFTER, afterEventTrap); + + const {$carousel} = createDummyCarousel(5); + + describe('3 slides visible, no loop', () => { + beforeEach(() => { + beforeEventTrap.mockReset(); + afterEventTrap.mockReset(); + }); + + beforeAll(async () => { + $carousel.count = '3'; + $carousel.loop = 'false'; + await Promise.resolve(); + }); + + test('ESLCarouselSlideEvent: Initial slide triggered correct events', async () => { + const request = $carousel.renderer.navigate(0, 'next', {activator: 'user'}); + expect(beforeEventTrap).toHaveBeenCalledTimes(1); + expect(afterEventTrap).toHaveBeenCalledTimes(0); + expect(beforeEventTrap).toHaveBeenLastCalledWith(expect.objectContaining({ + type: ESLCarouselSlideEvent.BEFORE, + indexesAfter: [0, 1, 2], + direction: 'next', + activator: 'user' + })); + + await request; + expect(afterEventTrap).toHaveBeenCalledTimes(1); + expect(afterEventTrap).toHaveBeenLastCalledWith(expect.objectContaining({ + type: ESLCarouselSlideEvent.AFTER, + indexesAfter: [0, 1, 2], + direction: 'next', + activator: 'user' + })); + }); + + test('ESLCarouselSlideEvent: correct events triggered in the middle state', async () => { + const request = $carousel.renderer.navigate(1, 'next', {activator: 'user'}); + expect(beforeEventTrap).toHaveBeenCalledTimes(1); + expect(afterEventTrap).toHaveBeenCalledTimes(0); + expect(beforeEventTrap).toHaveBeenLastCalledWith(expect.objectContaining({ + type: ESLCarouselSlideEvent.BEFORE, + indexesBefore: [0, 1, 2], + indexesAfter: [1, 2, 3], + direction: 'next', + activator: 'user' + })); + + await request; + expect(afterEventTrap).toHaveBeenCalledTimes(1); + expect(afterEventTrap).toHaveBeenLastCalledWith(expect.objectContaining({ + type: ESLCarouselSlideEvent.AFTER, + indexesBefore: [0, 1, 2], + indexesAfter: [1, 2, 3], + direction: 'next', + activator: 'user' + })); + }); + + test('ESLCarouselSlideEvent: Last slide triggered correct events', async () => { + const request = $carousel.renderer.navigate(2, 'next', {activator: 'user'}); + expect(beforeEventTrap).toHaveBeenCalledTimes(1); + expect(afterEventTrap).toHaveBeenCalledTimes(0); + expect(beforeEventTrap).toHaveBeenLastCalledWith(expect.objectContaining({ + type: ESLCarouselSlideEvent.BEFORE, + indexesBefore: [1, 2, 3], + indexesAfter: [2, 3, 4], + direction: 'next', + activator: 'user' + })); + + await request; + expect(afterEventTrap).toHaveBeenCalledTimes(1); + expect(afterEventTrap).toHaveBeenLastCalledWith(expect.objectContaining({ + type: ESLCarouselSlideEvent.AFTER, + indexesBefore: [1, 2, 3], + indexesAfter: [2, 3, 4], + direction: 'next', + activator: 'user' + })); + }); + }); +});