From d5eebce1025680f78368b1ef75a43c23403a4989 Mon Sep 17 00:00:00 2001 From: RivaIvanova Date: Thu, 3 Jul 2025 17:25:56 +0300 Subject: [PATCH 1/3] fix(carousel): pause auto-rotation on pointer focus --- CHANGELOG.md | 5 +++ src/components/carousel/carousel.spec.ts | 48 ++++++++++++++++++++++++ src/components/carousel/carousel.ts | 18 +++++---- 3 files changed, 63 insertions(+), 8 deletions(-) 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..67859b705 100644 --- a/src/components/carousel/carousel.spec.ts +++ b/src/components/carousel/carousel.spec.ts @@ -745,6 +745,54 @@ 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 on next button/focusable element + nextButton.dispatchEvent(new FocusEvent('focusin', { bubbles: true })); + 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); + + nextButton.dispatchEvent(new FocusEvent('focusout', { bubbles: true })); + 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..80d9bcb42 100644 --- a/src/components/carousel/carousel.ts +++ b/src/components/carousel/carousel.ts @@ -108,7 +108,8 @@ export default class IgcCarouselComponent extends EventEmitterMixin< private _lastInterval!: ReturnType | null; private _hasKeyboardInteractionOnIndicators = false; private _hasMouseStop = false; - private _hasInnerFocus = false; + private _hasKeyboardFocus = false; + private _hasMouseFocus = false; private _context = new ContextProvider(this, { context: carouselContext, @@ -332,10 +333,10 @@ export default class IgcCarouselComponent extends EventEmitterMixin< addSafeEventListener(this, 'pointerenter', this.handlePointerEnter); addSafeEventListener(this, 'pointerleave', this.handlePointerLeave); addSafeEventListener(this, 'pointerdown', () => { - this._hasInnerFocus = false; + this._hasKeyboardFocus = false; }); addSafeEventListener(this, 'keyup', () => { - this._hasInnerFocus = true; + this._hasKeyboardFocus = true; }); addGesturesController(this, { @@ -391,7 +392,7 @@ export default class IgcCarouselComponent extends EventEmitterMixin< private handlePointerEnter(): void { this._hasMouseStop = true; - if (this._hasInnerFocus) { + if (this._hasKeyboardFocus || this._hasMouseFocus) { return; } this.handlePauseOnInteraction(); @@ -399,14 +400,15 @@ export default class IgcCarouselComponent extends EventEmitterMixin< private handlePointerLeave(): void { this._hasMouseStop = false; - if (this._hasInnerFocus) { + if (this._hasKeyboardFocus || this._hasMouseFocus) { return; } this.handlePauseOnInteraction(); } private handleFocusIn(): void { - if (this._hasInnerFocus || this._hasMouseStop) { + if (this._hasKeyboardFocus || this._hasMouseStop || this._hasMouseFocus) { + this._hasMouseFocus = !this._hasKeyboardFocus && this._hasMouseStop; return; } this.handlePauseOnInteraction(); @@ -419,8 +421,8 @@ export default class IgcCarouselComponent extends EventEmitterMixin< return; } - if (this._hasInnerFocus) { - this._hasInnerFocus = false; + if (this._hasKeyboardFocus || this._hasMouseFocus) { + this._hasKeyboardFocus = this._hasMouseFocus = false; if (!this._hasMouseStop) { this.handlePauseOnInteraction(); From 7cad5e15775ef755a360cc4db06c23a667e60b8b Mon Sep 17 00:00:00 2001 From: RivaIvanova Date: Mon, 7 Jul 2025 18:07:46 +0300 Subject: [PATCH 2/3] refactor(carousel): simplify pointer/focus event handlers --- src/components/carousel/carousel.spec.ts | 15 +++--- src/components/carousel/carousel.ts | 62 ++++++++---------------- 2 files changed, 28 insertions(+), 49 deletions(-) diff --git a/src/components/carousel/carousel.spec.ts b/src/components/carousel/carousel.spec.ts index 67859b705..7e1bd9809 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,18 +712,18 @@ 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 + // loose focus carousel.dispatchEvent(new PointerEvent('pointerdown')); await elementUpdated(carousel); @@ -765,8 +764,8 @@ describe('Carousel', () => { expect(carousel.isPaused).to.be.true; expect(carousel.current).to.equal(0); - // focus on next button/focusable element - nextButton.dispatchEvent(new FocusEvent('focusin', { bubbles: true })); + // focus a focusable element + carousel.dispatchEvent(new FocusEvent('focusin')); await elementUpdated(carousel); // hover out of the carousel @@ -781,7 +780,7 @@ describe('Carousel', () => { expect(carousel.isPaused).to.be.true; expect(carousel.current).to.equal(0); - nextButton.dispatchEvent(new FocusEvent('focusout', { bubbles: true })); + carousel.dispatchEvent(new FocusEvent('focusout')); await elementUpdated(carousel); await clock.tickAsync(200); diff --git a/src/components/carousel/carousel.ts b/src/components/carousel/carousel.ts index 80d9bcb42..279262d8a 100644 --- a/src/components/carousel/carousel.ts +++ b/src/components/carousel/carousel.ts @@ -105,11 +105,11 @@ 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 _hasKeyboardFocus = false; - private _hasMouseFocus = false; + private _hasPointerInteraction = false; + private _hasInnerFocus = false; private _context = new ContextProvider(this, { context: carouselContext, @@ -152,9 +152,6 @@ export default class IgcCarouselComponent extends EventEmitterMixin< @state() private _playing = false; - @state() - private _paused = false; - private _observerCallback({ changes: { added, attributes }, }: MutationControllerParams) { @@ -330,14 +327,13 @@ export default class IgcCarouselComponent extends EventEmitterMixin< addThemingController(this, all); - addSafeEventListener(this, 'pointerenter', this.handlePointerEnter); - addSafeEventListener(this, 'pointerleave', this.handlePointerLeave); + addSafeEventListener(this, 'pointerenter', this.handlePointerInteraction); + addSafeEventListener(this, 'pointerleave', this.handlePointerInteraction); addSafeEventListener(this, 'pointerdown', () => { - this._hasKeyboardFocus = false; - }); - addSafeEventListener(this, 'keyup', () => { - this._hasKeyboardFocus = true; + this._hasInnerFocus = false; }); + addSafeEventListener(this, 'focusin', this.handleFocusInteraction); + addSafeEventListener(this, 'focusout', this.handleFocusInteraction); addGesturesController(this, { ref: this._carouselSlidesContainerRef, @@ -390,43 +386,27 @@ export default class IgcCarouselComponent extends EventEmitterMixin< this.requestUpdate(); } - private handlePointerEnter(): void { - this._hasMouseStop = true; - if (this._hasKeyboardFocus || this._hasMouseFocus) { - return; - } - this.handlePauseOnInteraction(); - } - - private handlePointerLeave(): void { - this._hasMouseStop = false; - if (this._hasKeyboardFocus || this._hasMouseFocus) { - return; - } - this.handlePauseOnInteraction(); - } + private handlePointerInteraction(event: PointerEvent): void { + this._hasPointerInteraction = event.type === 'pointerenter'; - private handleFocusIn(): void { - if (this._hasKeyboardFocus || this._hasMouseStop || this._hasMouseFocus) { - this._hasMouseFocus = !this._hasKeyboardFocus && this._hasMouseStop; - return; + if (!this._hasInnerFocus) { + this.handlePauseOnInteraction(); } - 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._hasKeyboardFocus || this._hasMouseFocus) { - this._hasKeyboardFocus = this._hasMouseFocus = false; + this._hasInnerFocus = event.type === 'focusin'; - if (!this._hasMouseStop) { - this.handlePauseOnInteraction(); - } + if (!this._hasPointerInteraction) { + this.handlePauseOnInteraction(); } } @@ -791,7 +771,7 @@ export default class IgcCarouselComponent extends EventEmitterMixin< protected override render() { return html` -
+
${this.hideNavigation ? nothing : this.navigationTemplate()} ${this.hideIndicators || this.showIndicatorsLabel ? nothing @@ -802,7 +782,7 @@ export default class IgcCarouselComponent extends EventEmitterMixin<
From f76c60d3791ac91cd6d18ccf019921731482dd4d Mon Sep 17 00:00:00 2001 From: RivaIvanova Date: Tue, 8 Jul 2025 10:18:45 +0300 Subject: [PATCH 3/3] refactor(carousel): remove pointerdown event listener --- src/components/carousel/carousel.spec.ts | 7 ++++++- src/components/carousel/carousel.ts | 3 --- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/carousel/carousel.spec.ts b/src/components/carousel/carousel.spec.ts index 7e1bd9809..c573fb4c1 100644 --- a/src/components/carousel/carousel.spec.ts +++ b/src/components/carousel/carousel.spec.ts @@ -723,8 +723,12 @@ describe('Carousel', () => { expect(carousel.isPaused).to.be.true; expect(divContainer.ariaLive).to.equal('polite'); + // hover carousel + carousel.dispatchEvent(new PointerEvent('pointerenter')); + await elementUpdated(carousel); + // loose focus - carousel.dispatchEvent(new PointerEvent('pointerdown')); + carousel.dispatchEvent(new FocusEvent('focusout')); await elementUpdated(carousel); expect(carousel.isPlaying).to.be.false; @@ -780,6 +784,7 @@ describe('Carousel', () => { expect(carousel.isPaused).to.be.true; expect(carousel.current).to.equal(0); + // loose focus carousel.dispatchEvent(new FocusEvent('focusout')); await elementUpdated(carousel); diff --git a/src/components/carousel/carousel.ts b/src/components/carousel/carousel.ts index 279262d8a..2e1c4617c 100644 --- a/src/components/carousel/carousel.ts +++ b/src/components/carousel/carousel.ts @@ -329,9 +329,6 @@ export default class IgcCarouselComponent extends EventEmitterMixin< addSafeEventListener(this, 'pointerenter', this.handlePointerInteraction); addSafeEventListener(this, 'pointerleave', this.handlePointerInteraction); - addSafeEventListener(this, 'pointerdown', () => { - this._hasInnerFocus = false; - }); addSafeEventListener(this, 'focusin', this.handleFocusInteraction); addSafeEventListener(this, 'focusout', this.handleFocusInteraction);