Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 58 additions & 6 deletions src/components/carousel/carousel.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
enterKey,
homeKey,
spaceBar,
tabKey,
} from '../common/controllers/key-bindings.js';
import { defineComponents } from '../common/definitions/defineComponents.js';
import {
Expand Down Expand Up @@ -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;
Expand All @@ -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(
Expand Down
59 changes: 19 additions & 40 deletions src/components/carousel/carousel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof setInterval> | null;
private _hasKeyboardInteractionOnIndicators = false;
private _hasMouseStop = false;
private _hasPointerInteraction = false;
private _hasInnerFocus = false;

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

@state()
private _paused = false;

private _observerCallback({
changes: { added, attributes },
}: MutationControllerParams<IgcCarouselSlideComponent>) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Comment thread
rkaraivanov marked this conversation as resolved.
}
}

Expand Down Expand Up @@ -789,7 +768,7 @@ export default class IgcCarouselComponent extends EventEmitterMixin<

protected override render() {
return html`
<section @focusin=${this.handleFocusIn} @focusout=${this.handleFocusOut}>
<section>
${this.hideNavigation ? nothing : this.navigationTemplate()}
${this.hideIndicators || this.showIndicatorsLabel
? nothing
Expand All @@ -800,7 +779,7 @@ export default class IgcCarouselComponent extends EventEmitterMixin<
<div
${ref(this._carouselSlidesContainerRef)}
id=${this._carouselId}
aria-live=${this.interval && this.isPlaying ? 'off' : 'polite'}
aria-live=${this.interval && this._playing ? 'off' : 'polite'}
>
<slot @slotchange=${this.handleSlotChange}></slot>
</div>
Expand Down