diff --git a/src/components/carousel/autoplay-controller.ts b/src/components/carousel/autoplay-controller.ts index 4cd978795..7fc6addac 100644 --- a/src/components/carousel/autoplay-controller.ts +++ b/src/components/carousel/autoplay-controller.ts @@ -8,6 +8,7 @@ export class AutoplayController implements ReactiveController { private host: ReactiveElement; private timerId = 0; private tickCallback: () => void; + private activeInteractions = 0; paused = false; stopped = true; @@ -57,12 +58,16 @@ export class AutoplayController implements ReactiveController { } pause = () => { - this.paused = true; - this.host.requestUpdate(); + if (!this.activeInteractions++) { + this.paused = true; + this.host.requestUpdate(); + } }; resume = () => { - this.paused = false; - this.host.requestUpdate(); + if (!--this.activeInteractions) { + this.paused = false; + this.host.requestUpdate(); + } }; } diff --git a/src/components/carousel/carousel.styles.ts b/src/components/carousel/carousel.styles.ts index d6b31cac2..a44883085 100644 --- a/src/components/carousel/carousel.styles.ts +++ b/src/components/carousel/carousel.styles.ts @@ -30,6 +30,7 @@ export default css` grid-area: pagination; display: flex; + flex-wrap: wrap; justify-content: center; gap: var(--sl-spacing-small); } diff --git a/src/components/carousel/carousel.test.ts b/src/components/carousel/carousel.test.ts index 1f6b36c18..1f4ca4c8f 100644 --- a/src/components/carousel/carousel.test.ts +++ b/src/components/carousel/carousel.test.ts @@ -78,6 +78,34 @@ describe('', () => { // Assert expect(el.next).not.to.have.been.called; }); + + it('should not resume if the user is still interacting', async () => { + // Arrange + const el = await fixture(html` + + Node 1 + Node 2 + Node 3 + + `); + sinon.stub(el, 'next'); + + await el.updateComplete; + + // Act + el.dispatchEvent(new Event('mouseenter')); + el.dispatchEvent(new Event('focusin')); + await el.updateComplete; + + el.dispatchEvent(new Event('mouseleave')); + await el.updateComplete; + + clock.next(); + clock.next(); + + // Assert + expect(el.next).not.to.have.been.called; + }); }); describe('when `loop` attribute is provided', () => { diff --git a/src/components/carousel/carousel.ts b/src/components/carousel/carousel.ts index ab31cff9d..417e8ce58 100644 --- a/src/components/carousel/carousel.ts +++ b/src/components/carousel/carousel.ts @@ -168,13 +168,21 @@ export default class SlCarousel extends ShoelaceElement { goToSlide(index: number, behavior: ScrollBehavior = 'smooth') { const { slidesPerPage, loop } = this; + const slides = this.getSlides(); const slidesWithClones = this.getSlides({ excludeClones: false }); - const normalizedIndex = clamp(index + (loop ? slidesPerPage : 0), 0, slidesWithClones.length - 1); - const slide = slidesWithClones[normalizedIndex]; + + // Sets the next index without taking into account clones, if any. + const newActiveSlide = (index + slides.length) % slides.length; + this.activeSlide = newActiveSlide; + + // Get the index of the next slide. For looping carousel it adds `slidesPerPage` + // to normalize the starting index in order to ignore the first nth clones. + const nextSlideIndex = clamp(index + (loop ? slidesPerPage : 0), 0, slidesWithClones.length - 1); + const nextSlide = slidesWithClones[nextSlideIndex]; this.scrollContainer.scrollTo({ - left: slide.offsetLeft, - top: slide.offsetTop, + left: nextSlide.offsetLeft, + top: nextSlide.offsetTop, behavior: prefersReducedMotion() ? 'auto' : behavior }); } @@ -307,13 +315,18 @@ export default class SlCarousel extends ShoelaceElement { this.scrollController.mouseDragging = this.mouseDragging; } - private renderPagination = () => { - const slides = this.getSlides(); - const slidesCount = slides.length; + private getPageCount() { + return Math.ceil(this.getSlides().length / this.slidesPerPage); + } - const { activeSlide, slidesPerPage } = this; - const pagesCount = Math.ceil(slidesCount / slidesPerPage); - const currentPage = Math.floor(activeSlide / slidesPerPage); + private getCurrentPage() { + return Math.floor(this.activeSlide / this.slidesPerPage); + } + + private renderPagination = () => { + const { slidesPerPage } = this; + const pagesCount = this.getPageCount(); + const currentPage = this.getCurrentPage(); return html`