diff --git a/CHANGELOG.md b/CHANGELOG.md index 21940af09..20ebe71c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [Unreleased] +### Fixed +- #### Carousel + - Pause automatic rotation on pointer-initiated focus [#1731](https://github.com/IgniteUI/igniteui-webcomponents/issues/1731) + ## [6.1.1] - 2025-06-25 ### Fixed - #### Dropdown diff --git a/src/components/carousel/carousel.spec.ts b/src/components/carousel/carousel.spec.ts index 9635456be..c573fb4c1 100644 --- a/src/components/carousel/carousel.spec.ts +++ b/src/components/carousel/carousel.spec.ts @@ -16,7 +16,6 @@ import { enterKey, homeKey, spaceBar, - tabKey, } from '../common/controllers/key-bindings.js'; import { defineComponents } from '../common/definitions/defineComponents.js'; import { @@ -713,19 +712,23 @@ describe('Carousel', () => { expect(carousel.isPaused).to.be.true; expect(divContainer.ariaLive).to.equal('polite'); - // focus with keyboard - simulateKeyboard(prevButton, tabKey); + // focus a focusable element + carousel.dispatchEvent(new FocusEvent('focusin')); carousel.dispatchEvent(new PointerEvent('pointerleave')); await elementUpdated(carousel); - // keyboard focus/interaction is present + // element focus/interaction is present // -> should not start rotation on pointerleave expect(carousel.isPlaying).to.be.false; expect(carousel.isPaused).to.be.true; expect(divContainer.ariaLive).to.equal('polite'); - // loose keyboard focus - carousel.dispatchEvent(new PointerEvent('pointerdown')); + // hover carousel + carousel.dispatchEvent(new PointerEvent('pointerenter')); + await elementUpdated(carousel); + + // loose focus + carousel.dispatchEvent(new FocusEvent('focusout')); await elementUpdated(carousel); expect(carousel.isPlaying).to.be.false; @@ -745,6 +748,55 @@ describe('Carousel', () => { expect(eventSpy.secondCall).calledWith('igcPlaying'); }); + it('should pause when focusing an interactive element - issue #1731', async () => { + carousel.interval = 200; + await elementUpdated(carousel); + + await clock.tickAsync(199); + + expect(carousel.isPlaying).to.be.true; + expect(carousel.isPaused).to.be.false; + expect(carousel.current).to.equal(0); + + // hover carousel + carousel.dispatchEvent(new PointerEvent('pointerenter')); + await elementUpdated(carousel); + + await clock.tickAsync(1); + + expect(carousel.isPlaying).to.be.false; + expect(carousel.isPaused).to.be.true; + expect(carousel.current).to.equal(0); + + // focus a focusable element + carousel.dispatchEvent(new FocusEvent('focusin')); + await elementUpdated(carousel); + + // hover out of the carousel + carousel.dispatchEvent(new PointerEvent('pointerleave')); + await elementUpdated(carousel); + + await clock.tickAsync(200); + + // an interactive element is focused + // -> should not start rotation on pointerleave + expect(carousel.isPlaying).to.be.false; + expect(carousel.isPaused).to.be.true; + expect(carousel.current).to.equal(0); + + // loose focus + carousel.dispatchEvent(new FocusEvent('focusout')); + await elementUpdated(carousel); + + await clock.tickAsync(200); + + // the interactive element loses focus + // -> should start rotation + expect(carousel.isPlaying).to.be.true; + expect(carousel.isPaused).to.be.false; + expect(carousel.current).to.equal(2); + }); + it('should not pause on interaction if `disablePauseOnInteraction` is true', async () => { const eventSpy = spy(carousel, 'emitEvent'); const divContainer = carousel.shadowRoot?.querySelector( diff --git a/src/components/carousel/carousel.ts b/src/components/carousel/carousel.ts index 84b2347e6..2e1c4617c 100644 --- a/src/components/carousel/carousel.ts +++ b/src/components/carousel/carousel.ts @@ -105,9 +105,10 @@ export default class IgcCarouselComponent extends EventEmitterMixin< private static readonly increment = createCounter(); private readonly _carouselId = `igc-carousel-${IgcCarouselComponent.increment()}`; + private _paused = false; private _lastInterval!: ReturnType | null; private _hasKeyboardInteractionOnIndicators = false; - private _hasMouseStop = false; + private _hasPointerInteraction = false; private _hasInnerFocus = false; private _context = new ContextProvider(this, { @@ -151,9 +152,6 @@ export default class IgcCarouselComponent extends EventEmitterMixin< @state() private _playing = false; - @state() - private _paused = false; - private _observerCallback({ changes: { added, attributes }, }: MutationControllerParams) { @@ -329,14 +327,10 @@ export default class IgcCarouselComponent extends EventEmitterMixin< addThemingController(this, all); - addSafeEventListener(this, 'pointerenter', this.handlePointerEnter); - addSafeEventListener(this, 'pointerleave', this.handlePointerLeave); - addSafeEventListener(this, 'pointerdown', () => { - this._hasInnerFocus = false; - }); - addSafeEventListener(this, 'keyup', () => { - this._hasInnerFocus = true; - }); + addSafeEventListener(this, 'pointerenter', this.handlePointerInteraction); + addSafeEventListener(this, 'pointerleave', this.handlePointerInteraction); + addSafeEventListener(this, 'focusin', this.handleFocusInteraction); + addSafeEventListener(this, 'focusout', this.handleFocusInteraction); addGesturesController(this, { ref: this._carouselSlidesContainerRef, @@ -389,42 +383,27 @@ export default class IgcCarouselComponent extends EventEmitterMixin< this.requestUpdate(); } - private handlePointerEnter(): void { - this._hasMouseStop = true; - if (this._hasInnerFocus) { - return; - } - this.handlePauseOnInteraction(); - } + private handlePointerInteraction(event: PointerEvent): void { + this._hasPointerInteraction = event.type === 'pointerenter'; - private handlePointerLeave(): void { - this._hasMouseStop = false; - if (this._hasInnerFocus) { - return; + if (!this._hasInnerFocus) { + this.handlePauseOnInteraction(); } - this.handlePauseOnInteraction(); } - private handleFocusIn(): void { - if (this._hasInnerFocus || this._hasMouseStop) { - return; - } - this.handlePauseOnInteraction(); - } - - private handleFocusOut(event: FocusEvent): void { + private handleFocusInteraction(event: FocusEvent): void { + // focusin - element that lost focus + // focusout - element that gained focus const node = event.relatedTarget as Node; - if (this.contains(node) || this.renderRoot.contains(node)) { + if (this.contains(node)) { return; } - if (this._hasInnerFocus) { - this._hasInnerFocus = false; + this._hasInnerFocus = event.type === 'focusin'; - if (!this._hasMouseStop) { - this.handlePauseOnInteraction(); - } + if (!this._hasPointerInteraction) { + this.handlePauseOnInteraction(); } } @@ -789,7 +768,7 @@ export default class IgcCarouselComponent extends EventEmitterMixin< protected override render() { return html` -
+
${this.hideNavigation ? nothing : this.navigationTemplate()} ${this.hideIndicators || this.showIndicatorsLabel ? nothing @@ -800,7 +779,7 @@ export default class IgcCarouselComponent extends EventEmitterMixin<