Skip to content

Commit e68a326

Browse files
authored
fix(carousel): pause auto-rotation on pointer focus (#1774)
Pause auto-rotation when there is focus within the carousel DOM tree.
1 parent 50478de commit e68a326

File tree

3 files changed

+82
-46
lines changed

3 files changed

+82
-46
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/)
55
and this project adheres to [Semantic Versioning](http://semver.org/).
66

7+
## [Unreleased]
8+
### Fixed
9+
- #### Carousel
10+
- Pause automatic rotation on pointer-initiated focus [#1731](https://github.com/IgniteUI/igniteui-webcomponents/issues/1731)
11+
712
## [6.1.1] - 2025-06-25
813
### Fixed
914
- #### Dropdown

src/components/carousel/carousel.spec.ts

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
enterKey,
1717
homeKey,
1818
spaceBar,
19-
tabKey,
2019
} from '../common/controllers/key-bindings.js';
2120
import { defineComponents } from '../common/definitions/defineComponents.js';
2221
import {
@@ -713,19 +712,23 @@ describe('Carousel', () => {
713712
expect(carousel.isPaused).to.be.true;
714713
expect(divContainer.ariaLive).to.equal('polite');
715714

716-
// focus with keyboard
717-
simulateKeyboard(prevButton, tabKey);
715+
// focus a focusable element
716+
carousel.dispatchEvent(new FocusEvent('focusin'));
718717
carousel.dispatchEvent(new PointerEvent('pointerleave'));
719718
await elementUpdated(carousel);
720719

721-
// keyboard focus/interaction is present
720+
// element focus/interaction is present
722721
// -> should not start rotation on pointerleave
723722
expect(carousel.isPlaying).to.be.false;
724723
expect(carousel.isPaused).to.be.true;
725724
expect(divContainer.ariaLive).to.equal('polite');
726725

727-
// loose keyboard focus
728-
carousel.dispatchEvent(new PointerEvent('pointerdown'));
726+
// hover carousel
727+
carousel.dispatchEvent(new PointerEvent('pointerenter'));
728+
await elementUpdated(carousel);
729+
730+
// loose focus
731+
carousel.dispatchEvent(new FocusEvent('focusout'));
729732
await elementUpdated(carousel);
730733

731734
expect(carousel.isPlaying).to.be.false;
@@ -745,6 +748,55 @@ describe('Carousel', () => {
745748
expect(eventSpy.secondCall).calledWith('igcPlaying');
746749
});
747750

751+
it('should pause when focusing an interactive element - issue #1731', async () => {
752+
carousel.interval = 200;
753+
await elementUpdated(carousel);
754+
755+
await clock.tickAsync(199);
756+
757+
expect(carousel.isPlaying).to.be.true;
758+
expect(carousel.isPaused).to.be.false;
759+
expect(carousel.current).to.equal(0);
760+
761+
// hover carousel
762+
carousel.dispatchEvent(new PointerEvent('pointerenter'));
763+
await elementUpdated(carousel);
764+
765+
await clock.tickAsync(1);
766+
767+
expect(carousel.isPlaying).to.be.false;
768+
expect(carousel.isPaused).to.be.true;
769+
expect(carousel.current).to.equal(0);
770+
771+
// focus a focusable element
772+
carousel.dispatchEvent(new FocusEvent('focusin'));
773+
await elementUpdated(carousel);
774+
775+
// hover out of the carousel
776+
carousel.dispatchEvent(new PointerEvent('pointerleave'));
777+
await elementUpdated(carousel);
778+
779+
await clock.tickAsync(200);
780+
781+
// an interactive element is focused
782+
// -> should not start rotation on pointerleave
783+
expect(carousel.isPlaying).to.be.false;
784+
expect(carousel.isPaused).to.be.true;
785+
expect(carousel.current).to.equal(0);
786+
787+
// loose focus
788+
carousel.dispatchEvent(new FocusEvent('focusout'));
789+
await elementUpdated(carousel);
790+
791+
await clock.tickAsync(200);
792+
793+
// the interactive element loses focus
794+
// -> should start rotation
795+
expect(carousel.isPlaying).to.be.true;
796+
expect(carousel.isPaused).to.be.false;
797+
expect(carousel.current).to.equal(2);
798+
});
799+
748800
it('should not pause on interaction if `disablePauseOnInteraction` is true', async () => {
749801
const eventSpy = spy(carousel, 'emitEvent');
750802
const divContainer = carousel.shadowRoot?.querySelector(

src/components/carousel/carousel.ts

Lines changed: 19 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,10 @@ export default class IgcCarouselComponent extends EventEmitterMixin<
105105
private static readonly increment = createCounter();
106106
private readonly _carouselId = `igc-carousel-${IgcCarouselComponent.increment()}`;
107107

108+
private _paused = false;
108109
private _lastInterval!: ReturnType<typeof setInterval> | null;
109110
private _hasKeyboardInteractionOnIndicators = false;
110-
private _hasMouseStop = false;
111+
private _hasPointerInteraction = false;
111112
private _hasInnerFocus = false;
112113

113114
private _context = new ContextProvider(this, {
@@ -151,9 +152,6 @@ export default class IgcCarouselComponent extends EventEmitterMixin<
151152
@state()
152153
private _playing = false;
153154

154-
@state()
155-
private _paused = false;
156-
157155
private _observerCallback({
158156
changes: { added, attributes },
159157
}: MutationControllerParams<IgcCarouselSlideComponent>) {
@@ -329,14 +327,10 @@ export default class IgcCarouselComponent extends EventEmitterMixin<
329327

330328
addThemingController(this, all);
331329

332-
addSafeEventListener(this, 'pointerenter', this.handlePointerEnter);
333-
addSafeEventListener(this, 'pointerleave', this.handlePointerLeave);
334-
addSafeEventListener(this, 'pointerdown', () => {
335-
this._hasInnerFocus = false;
336-
});
337-
addSafeEventListener(this, 'keyup', () => {
338-
this._hasInnerFocus = true;
339-
});
330+
addSafeEventListener(this, 'pointerenter', this.handlePointerInteraction);
331+
addSafeEventListener(this, 'pointerleave', this.handlePointerInteraction);
332+
addSafeEventListener(this, 'focusin', this.handleFocusInteraction);
333+
addSafeEventListener(this, 'focusout', this.handleFocusInteraction);
340334

341335
addGesturesController(this, {
342336
ref: this._carouselSlidesContainerRef,
@@ -389,42 +383,27 @@ export default class IgcCarouselComponent extends EventEmitterMixin<
389383
this.requestUpdate();
390384
}
391385

392-
private handlePointerEnter(): void {
393-
this._hasMouseStop = true;
394-
if (this._hasInnerFocus) {
395-
return;
396-
}
397-
this.handlePauseOnInteraction();
398-
}
386+
private handlePointerInteraction(event: PointerEvent): void {
387+
this._hasPointerInteraction = event.type === 'pointerenter';
399388

400-
private handlePointerLeave(): void {
401-
this._hasMouseStop = false;
402-
if (this._hasInnerFocus) {
403-
return;
389+
if (!this._hasInnerFocus) {
390+
this.handlePauseOnInteraction();
404391
}
405-
this.handlePauseOnInteraction();
406392
}
407393

408-
private handleFocusIn(): void {
409-
if (this._hasInnerFocus || this._hasMouseStop) {
410-
return;
411-
}
412-
this.handlePauseOnInteraction();
413-
}
414-
415-
private handleFocusOut(event: FocusEvent): void {
394+
private handleFocusInteraction(event: FocusEvent): void {
395+
// focusin - element that lost focus
396+
// focusout - element that gained focus
416397
const node = event.relatedTarget as Node;
417398

418-
if (this.contains(node) || this.renderRoot.contains(node)) {
399+
if (this.contains(node)) {
419400
return;
420401
}
421402

422-
if (this._hasInnerFocus) {
423-
this._hasInnerFocus = false;
403+
this._hasInnerFocus = event.type === 'focusin';
424404

425-
if (!this._hasMouseStop) {
426-
this.handlePauseOnInteraction();
427-
}
405+
if (!this._hasPointerInteraction) {
406+
this.handlePauseOnInteraction();
428407
}
429408
}
430409

@@ -789,7 +768,7 @@ export default class IgcCarouselComponent extends EventEmitterMixin<
789768

790769
protected override render() {
791770
return html`
792-
<section @focusin=${this.handleFocusIn} @focusout=${this.handleFocusOut}>
771+
<section>
793772
${this.hideNavigation ? nothing : this.navigationTemplate()}
794773
${this.hideIndicators || this.showIndicatorsLabel
795774
? nothing
@@ -800,7 +779,7 @@ export default class IgcCarouselComponent extends EventEmitterMixin<
800779
<div
801780
${ref(this._carouselSlidesContainerRef)}
802781
id=${this._carouselId}
803-
aria-live=${this.interval && this.isPlaying ? 'off' : 'polite'}
782+
aria-live=${this.interval && this._playing ? 'off' : 'polite'}
804783
>
805784
<slot @slotchange=${this.handleSlotChange}></slot>
806785
</div>

0 commit comments

Comments
 (0)