diff --git a/packages/components/src/components/carousel/autoplay-controller.ts b/packages/components/src/components/carousel/autoplay-controller.ts index 83a4f09f8..7fc6addac 100644 --- a/packages/components/src/components/carousel/autoplay-controller.ts +++ b/packages/components/src/components/carousel/autoplay-controller.ts @@ -70,20 +70,4 @@ export class AutoplayController implements ReactiveController { this.host.requestUpdate(); } }; - - /** - * Used to manually pause autoplay without disrupting mouse/focus/touch events. - */ - controlledPause = () => { - this.paused = true; - this.host.requestUpdate(); - }; - - /** - * Used to manually resume autoplay without disrupting mouse/focus/touch events. - */ - controlledResume = () => { - this.paused = false; - this.host.requestUpdate(); - }; } diff --git a/packages/components/src/components/carousel/carousel.stories.ts b/packages/components/src/components/carousel/carousel.stories.ts index 201acc306..70503e9e6 100644 --- a/packages/components/src/components/carousel/carousel.stories.ts +++ b/packages/components/src/components/carousel/carousel.stories.ts @@ -152,12 +152,14 @@ export const Loop = { /** * Use the `autoplay` attribute to toggle autoplay. + * + * __Hint:__ Autoplay is automatically paused when the user interacts with the carousel or when the pause button is clicked. */ export const Autoplay = { render: () => html`
- +
Default slot 1
diff --git a/packages/components/src/components/carousel/carousel.test.ts b/packages/components/src/components/carousel/carousel.test.ts index 84d373d80..241ad110b 100644 --- a/packages/components/src/components/carousel/carousel.test.ts +++ b/packages/components/src/components/carousel/carousel.test.ts @@ -661,4 +661,125 @@ describe('', () => { expect(currentPage).to.equal(5); }); }); + + describe('when the user interacts with the carousel', () => { + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + clock = sinon.useFakeTimers({ + now: new Date() + }); + }); + + afterEach(() => { + clock.restore(); + }); + + it('should pause the autoplay', 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')); + await el.updateComplete; + clock.next(); + clock.next(); + + // 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; + }); + + it('should not resume if the user clicks the pause button', async () => { + // Arrange + const el = await fixture(html` + + Node 1 + Node 2 + Node 3 + + `); + sinon.stub(el, 'next'); + + await el.updateComplete; + + // Act + el.autoplayControls.click(); + await el.updateComplete; + clock.next(); + clock.next(); + + // Assert + expect(el.next).not.to.have.been.called; + }); + + it('should resume if the user clicks the resume button', async () => { + // Arrange + const el = await fixture(html` + + Node 1 + Node 2 + Node 3 + + `); + sinon.stub(el, 'next'); + + await el.updateComplete; + + // Act + el.autoplayControls.click(); + await el.updateComplete; + clock.next(); + clock.next(); + + // Assert + expect(el.next).not.to.have.been.called; + + // Act + + el.autoplayControls.click(); + await el.updateComplete; + clock.next(); + clock.next(); + + // Assert + expect(el.next).to.have.been.called; + }); + }); }); diff --git a/packages/components/src/components/carousel/carousel.ts b/packages/components/src/components/carousel/carousel.ts index 938fc0793..cdb484b23 100644 --- a/packages/components/src/components/carousel/carousel.ts +++ b/packages/components/src/components/carousel/carousel.ts @@ -52,6 +52,10 @@ import SolidElement from '../../internal/solid-element.js'; */ @customElement('sd-carousel') export default class SdCarousel extends SolidElement { + @query('[part~="autoplay-controls"]') autoplayControls: HTMLElement; + @query('[part~="navigation-button--previous"]') previousButton: HTMLButtonElement; + @query('[part~="navigation-button--next"]') nextButton: HTMLButtonElement; + /** Determines the counting system for the carousel. */ @property({ type: String, reflect: true }) variant: 'dot' | 'number' = 'number'; /** Inverts the carousel */ @@ -251,10 +255,13 @@ export default class SdCarousel extends SolidElement { @watch('pausedAutoplay') handlePausedAutoplay() { if (this.pausedAutoplay) { - this.autoplayController.controlledPause(); + this.autoplayController.stop(); } else if (this.autoplay) { - this.autoplayController.controlledResume(); + this.autoplayController.start(3000); } + + // This is necessary to allow autoplay since focus is not removed when the button is clicked. + this.autoplayControls?.blur(); } @watch('loop', { waitUntilFirstUpdate: true }) @@ -347,8 +354,6 @@ export default class SdCarousel extends SolidElement { slide.style.setProperty('scroll-snap-align', 'none'); } }); - - // this.handleScrollEnd(); } @watch('autoplay') @@ -378,6 +383,9 @@ export default class SdCarousel extends SolidElement { } else { this.goToSlide(previousIndex, behavior); } + + // This keeps the carousel from pausing autoplay due to the lingering focus + this.previousButton?.blur(); } /** @@ -387,13 +395,18 @@ export default class SdCarousel extends SolidElement { */ next(behavior: ScrollBehavior = 'smooth') { if ( - this.currentPage + 1 > SdCarousel.getPageCount(this.getSlides().length, this.slidesPerPage, this.slidesPerMove) && - this.loop + this.currentPage + 1 <= + SdCarousel.getPageCount(this.getSlides().length, this.slidesPerPage, this.slidesPerMove) ) { - this.nextTillFirst(behavior); - } else { this.goToSlide(this.activeSlide + this.slidesPerMove, behavior); + } else { + if (this.loop) { + this.nextTillFirst(behavior); + } } + + // This keeps the carousel from pausing autoplay due to the lingering focus + this.nextButton?.blur(); } nextTillFirst(behavior: ScrollBehavior = 'smooth') {