From 1eda24e952591b7e37926331e02ef4636204cd7d Mon Sep 17 00:00:00 2001 From: RivaIvanova Date: Wed, 9 Jul 2025 18:56:04 +0300 Subject: [PATCH] fix(carousel): emit igcSlideChanged on slide change --- CHANGELOG.md | 1 + src/components/carousel/carousel.spec.ts | 131 +++++++++++++++++++++-- src/components/carousel/carousel.ts | 26 +++-- 3 files changed, 141 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20ebe71c3..5d07635bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - #### Carousel - Pause automatic rotation on pointer-initiated focus [#1731](https://github.com/IgniteUI/igniteui-webcomponents/issues/1731) + - Ensure `igcSlideChanged` event is emitted when a slide is changed [#1772](https://github.com/IgniteUI/igniteui-webcomponents/issues/1772) ## [6.1.1] - 2025-06-25 ### Fixed diff --git a/src/components/carousel/carousel.spec.ts b/src/components/carousel/carousel.spec.ts index c573fb4c1..f4d6f1b5d 100644 --- a/src/components/carousel/carousel.spec.ts +++ b/src/components/carousel/carousel.spec.ts @@ -7,7 +7,7 @@ import { waitUntil, } from '@open-wc/testing'; -import { type SinonFakeTimers, spy, useFakeTimers } from 'sinon'; +import { type SinonFakeTimers, spy, stub, useFakeTimers } from 'sinon'; import IgcButtonComponent from '../button/button.js'; import { arrowLeft, @@ -88,12 +88,6 @@ describe('Carousel', () => { IgcCarouselIndicatorComponent.tagName ) ); - - clock = useFakeTimers({ toFake: ['setInterval'] }); - }); - - afterEach(() => { - clock.restore(); }); describe('Initialization', () => { @@ -556,6 +550,26 @@ describe('Carousel', () => { detail: 0, }); }); + + it('should properly call `igcSlideChanged` event', async () => { + const eventSpy = spy(carousel, 'emitEvent'); + + stub(carousel, 'select') + .onFirstCall() + .resolves(true) + .onSecondCall() + .resolves(false); + + // select second indicator + simulateClick(defaultIndicators[1]); + await slideChangeComplete(slides[0], slides[1]); + + // select second indicator again + simulateClick(defaultIndicators[1]); + await slideChangeComplete(slides[0], slides[1]); + + expect(eventSpy.callCount).to.equal(1); + }); }); describe('Keyboard', () => { @@ -643,6 +657,12 @@ describe('Carousel', () => { }); describe('Automatic rotation', () => { + beforeEach(async () => { + clock = useFakeTimers({ toFake: ['setInterval'] }); + }); + + afterEach(() => clock.restore()); + it('should automatically change slides', async () => { expect(carousel.current).to.equal(0); @@ -655,6 +675,21 @@ describe('Carousel', () => { expect(carousel.current).to.equal(1); }); + it('should properly call `igcSlideChanged` event', async () => { + const eventSpy = spy(carousel, 'emitEvent'); + + carousel.disableLoop = true; + carousel.interval = 100; + await elementUpdated(carousel); + + expect(carousel.current).to.equal(0); + + await clock.tickAsync(300); + + expect(carousel.current).to.equal(2); + expect(eventSpy.callCount).to.equal(2); + }); + it('should pause/play on pointerenter/pointerleave', async () => { const eventSpy = spy(carousel, 'emitEvent'); const divContainer = carousel.shadowRoot?.querySelector( @@ -1021,6 +1056,88 @@ describe('Carousel', () => { expect(carousel.current).to.equal(0); }); + + it('should properly call `igcSlideChanged` event', async () => { + carousel = await fixture( + html` + + 1 + + + 2 + + ` + ); + + carouselSlidesContainer = carousel.shadowRoot?.querySelector( + 'div[aria-live="polite"]' + ) as Element; + + const eventSpy = spy(carousel, 'emitEvent'); + + const prevStub = stub(carousel, 'prev'); + const nextStub = stub(carousel, 'next'); + + prevStub.resolves(false); + nextStub.onFirstCall().resolves(true).onSecondCall().resolves(false); + + carousel.disableLoop = true; + await elementUpdated(carousel); + + expect(carousel.current).to.equal(0); + + // swipe right - disabled + simulatePointerDown(carouselSlidesContainer); + simulatePointerMove(carouselSlidesContainer, {}, { x: 100 }, 10); + simulateLostPointerCapture(carouselSlidesContainer); + await slideChangeComplete(slides[0], slides[2]); + + // swipe left + simulatePointerDown(carouselSlidesContainer); + simulatePointerMove(carouselSlidesContainer, {}, { x: -100 }, 10); + simulateLostPointerCapture(carouselSlidesContainer); + await slideChangeComplete(slides[0], slides[1]); + + // swipe left - disabled + simulatePointerDown(carouselSlidesContainer); + simulatePointerMove(carouselSlidesContainer, {}, { x: -100 }, 10); + simulateLostPointerCapture(carouselSlidesContainer); + await slideChangeComplete(slides[0], slides[1]); + + expect(eventSpy.callCount).to.equal(1); + + eventSpy.resetHistory(); + prevStub.resetHistory(); + nextStub.resetHistory(); + + prevStub.resolves(false); + nextStub.onFirstCall().resolves(true).onSecondCall().resolves(false); + + carousel.vertical = true; + await elementUpdated(carousel); + + expect(eventSpy.callCount).to.equal(0); + + // swipe down - disabled + simulatePointerDown(carouselSlidesContainer); + simulatePointerMove(carouselSlidesContainer, {}, { y: 100 }, 10); + simulateLostPointerCapture(carouselSlidesContainer); + await slideChangeComplete(slides[2], slides[0]); + + // swipe up + simulatePointerDown(carouselSlidesContainer); + simulatePointerMove(carouselSlidesContainer, {}, { y: -100 }, 10); + simulateLostPointerCapture(carouselSlidesContainer); + await slideChangeComplete(slides[2], slides[1]); + + // swipe up - disabled + simulatePointerDown(carouselSlidesContainer); + simulatePointerMove(carouselSlidesContainer, {}, { y: -100 }, 10); + simulateLostPointerCapture(carouselSlidesContainer); + await slideChangeComplete(slides[1], slides[0]); + + expect(eventSpy.callCount).to.equal(1); + }); }); }); }); diff --git a/src/components/carousel/carousel.ts b/src/components/carousel/carousel.ts index 2e1c4617c..de65096bd 100644 --- a/src/components/carousel/carousel.ts +++ b/src/components/carousel/carousel.ts @@ -451,13 +451,14 @@ export default class IgcCarouselComponent extends EventEmitterMixin< private handleHorizontalSwipe({ data: { direction } }: SwipeEvent) { if (!this.vertical) { - this.handleInteraction(async () => { + const callback = () => { if (isLTR(this)) { - direction === 'left' ? await this.next() : await this.prev(); - } else { - direction === 'left' ? await this.prev() : await this.next(); + return direction === 'left' ? this.next : this.prev; } - }); + return direction === 'left' ? this.prev : this.next; + }; + + this.handleInteraction(callback()); } } @@ -485,14 +486,15 @@ export default class IgcCarouselComponent extends EventEmitterMixin< } private async handleInteraction( - callback: () => Promise + callback: () => Promise ): Promise { if (this.interval) { this.resetInterval(); } - await callback.call(this); - this.emitEvent('igcSlideChanged', { detail: this.current }); + if (await callback.call(this)) { + this.emitEvent('igcSlideChanged', { detail: this.current }); + } if (this.interval) { this.restartInterval(); @@ -538,8 +540,12 @@ export default class IgcCarouselComponent extends EventEmitterMixin< if (asNumber(this.interval) > 0) { this._lastInterval = setInterval(() => { - if (this.isPlaying && this.total) { - this.next(); + if ( + this.isPlaying && + this.total && + !(this.disableLoop && this.nextIndex === 0) + ) { + this.select(this.slides[this.nextIndex], 'next'); this.emitEvent('igcSlideChanged', { detail: this.current }); } else { this.pause();