• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IgniteUI / igniteui-webcomponents / 16115217920

07 Jul 2025 11:05AM UTC coverage: 98.28% (+0.009%) from 98.271%
16115217920

Pull #1774

github

web-flow
Merge 0b8f0847d into 50478dee2
Pull Request #1774: fix(carousel): pause auto-rotation on pointer focus

4953 of 5198 branches covered (95.29%)

Branch coverage included in aggregate %.

18 of 19 new or added lines in 1 file covered. (94.74%)

9 existing lines in 1 file now uncovered.

31778 of 32176 relevant lines covered (98.76%)

1728.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.35
/src/components/carousel/carousel.ts
1
import { ContextProvider } from '@lit/context';
10✔
2
import { html, LitElement, nothing } from 'lit';
10✔
3
import {
10✔
4
  property,
10✔
5
  queryAll,
10✔
6
  queryAssignedElements,
10✔
7
  state,
10✔
8
} from 'lit/decorators.js';
10✔
9

10✔
10
import { createRef, ref } from 'lit/directives/ref.js';
10✔
11
import { styleMap } from 'lit/directives/style-map.js';
10✔
12
import { addThemingController } from '../../theming/theming-controller.js';
10✔
13
import IgcButtonComponent from '../button/button.js';
10✔
14
import { carouselContext } from '../common/context.js';
10✔
15
import {
10✔
16
  addGesturesController,
10✔
17
  type SwipeEvent,
10✔
18
} from '../common/controllers/gestures.js';
10✔
19
import { addInternalsController } from '../common/controllers/internals.js';
10✔
20
import {
10✔
21
  addKeybindings,
10✔
22
  arrowLeft,
10✔
23
  arrowRight,
10✔
24
  endKey,
10✔
25
  homeKey,
10✔
26
} from '../common/controllers/key-bindings.js';
10✔
27
import {
10✔
28
  createMutationController,
10✔
29
  type MutationControllerParams,
10✔
30
} from '../common/controllers/mutation-observer.js';
10✔
31
import { watch } from '../common/decorators/watch.js';
10✔
32
import { registerComponent } from '../common/definitions/register.js';
10✔
33
import type { Constructor } from '../common/mixins/constructor.js';
10✔
34
import { EventEmitterMixin } from '../common/mixins/event-emitter.js';
10✔
35
import { partMap } from '../common/part-map.js';
10✔
36
import {
10✔
37
  addSafeEventListener,
10✔
38
  asNumber,
10✔
39
  createCounter,
10✔
40
  findElementFromEventPath,
10✔
41
  first,
10✔
42
  formatString,
10✔
43
  isLTR,
10✔
44
  last,
10✔
45
  wrap,
10✔
46
} from '../common/util.js';
10✔
47
import IgcIconComponent from '../icon/icon.js';
10✔
48
import type {
10✔
49
  CarouselIndicatorsOrientation,
10✔
50
  HorizontalTransitionAnimation,
10✔
51
} from '../types.js';
10✔
52
import IgcCarouselIndicatorComponent from './carousel-indicator.js';
10✔
53
import IgcCarouselIndicatorContainerComponent from './carousel-indicator-container.js';
10✔
54
import IgcCarouselSlideComponent from './carousel-slide.js';
10✔
55
import { styles } from './themes/carousel.base.css.js';
10✔
56
import { all } from './themes/container.js';
10✔
57
import { styles as shared } from './themes/shared/carousel.common.css.js';
10✔
58

10✔
59
export interface IgcCarouselComponentEventMap {
10✔
60
  igcSlideChanged: CustomEvent<number>;
10✔
61
  igcPlaying: CustomEvent<void>;
10✔
62
  igcPaused: CustomEvent<void>;
10✔
63
}
10✔
64

10✔
65
/**
10✔
66
 * The `igc-carousel` presents a set of `igc-carousel-slide`s by sequentially displaying a subset of one or more slides.
10✔
67
 *
10✔
68
 * @element igc-carousel
10✔
69
 *
10✔
70
 * @slot Default slot for the carousel. Any projected `igc-carousel-slide` components should be projected here.
10✔
71
 * @slot previous-button - Renders content inside the previous button.
10✔
72
 * @slot next-button - Renders content inside the next button.
10✔
73
 *
10✔
74
 * @fires igcSlideChanged - Emitted when the current active slide is changed either by user interaction or by the interval callback.
10✔
75
 * @fires igcPlaying - Emitted when the carousel enters playing state by a user interaction.
10✔
76
 * @fires igcPaused - Emitted when the carousel enters paused state by a user interaction.
10✔
77
 *
10✔
78
 * @csspart navigation - The wrapper container of each carousel navigation button.
10✔
79
 * @csspart previous - The wrapper container of the carousel previous navigation button.
10✔
80
 * @csspart next - The wrapper container of the carousel next navigation button.
10✔
81
 * @csspart dot - The carousel dot indicator container.
10✔
82
 * @csspart active - The carousel active dot indicator container.
10✔
83
 * @csspart label - The label container of the carousel indicators.
10✔
84
 * @csspart start - The wrapping container of all carousel indicators when indicators-orientation is set to start.
10✔
85
 */
10✔
86
export default class IgcCarouselComponent extends EventEmitterMixin<
10✔
87
  IgcCarouselComponentEventMap,
10✔
88
  Constructor<LitElement>
10✔
89
>(LitElement) {
10✔
90
  public static styles = [styles, shared];
10✔
91
  public static readonly tagName = 'igc-carousel';
10✔
92

10✔
93
  /* blazorSuppress */
10✔
94
  public static register(): void {
10✔
95
    registerComponent(
1✔
96
      IgcCarouselComponent,
1✔
97
      IgcCarouselIndicatorComponent,
1✔
98
      IgcCarouselIndicatorContainerComponent,
1✔
99
      IgcCarouselSlideComponent,
1✔
100
      IgcIconComponent,
1✔
101
      IgcButtonComponent
1✔
102
    );
1✔
103
  }
1✔
104

10✔
105
  private static readonly increment = createCounter();
10✔
106
  private readonly _carouselId = `igc-carousel-${IgcCarouselComponent.increment()}`;
10✔
107

10✔
108
  private _lastInterval!: ReturnType<typeof setInterval> | null;
10✔
109
  private _hasKeyboardInteractionOnIndicators = false;
10✔
110
  private _hasMouseStop = false;
10✔
111
  private _hasKeyboardFocus = false;
10✔
112
  private _hasMouseFocus = false;
10✔
113

10✔
114
  private _context = new ContextProvider(this, {
10✔
115
    context: carouselContext,
10✔
116
    initialValue: this,
10✔
117
  });
10✔
118

10✔
119
  private readonly _carouselSlidesContainerRef = createRef<HTMLDivElement>();
10✔
120
  private readonly _indicatorsContainerRef = createRef<HTMLDivElement>();
10✔
121
  private readonly _prevButtonRef = createRef<IgcButtonComponent>();
10✔
122
  private readonly _nextButtonRef = createRef<IgcButtonComponent>();
10✔
123

10✔
124
  private get hasProjectedIndicators(): boolean {
10✔
125
    return this._projectedIndicators.length > 0;
146✔
126
  }
146✔
127

10✔
128
  private get showIndicatorsLabel(): boolean {
10✔
129
    return this.total > this.maximumIndicatorsCount;
278✔
130
  }
278✔
131

10✔
132
  private get nextIndex(): number {
10✔
133
    return wrap(0, this.total - 1, this.current + 1);
14✔
134
  }
14✔
135

10✔
136
  private get prevIndex(): number {
10✔
137
    return wrap(0, this.total - 1, this.current - 1);
11✔
138
  }
11✔
139

10✔
140
  @queryAll(IgcCarouselIndicatorComponent.tagName)
10✔
141
  private _defaultIndicators!: NodeListOf<IgcCarouselIndicatorComponent>;
10✔
142

10✔
143
  @queryAssignedElements({
10✔
144
    selector: IgcCarouselIndicatorComponent.tagName,
10✔
145
    slot: 'indicator',
10✔
146
  })
10✔
147
  private _projectedIndicators!: Array<IgcCarouselIndicatorComponent>;
10✔
148

10✔
149
  @state()
10✔
150
  private _activeSlide!: IgcCarouselSlideComponent;
10✔
151

10✔
152
  @state()
10✔
153
  private _playing = false;
10✔
154

10✔
155
  @state()
10✔
156
  private _paused = false;
10✔
157

10✔
158
  private _observerCallback({
10✔
159
    changes: { added, attributes },
77✔
160
  }: MutationControllerParams<IgcCarouselSlideComponent>) {
77✔
161
    const activeSlides = this.slides.filter((slide) => slide.active);
77✔
162

77✔
163
    if (activeSlides.length <= 1) {
77✔
164
      return;
74✔
165
    }
74✔
166
    const idx = this.slides.indexOf(
3✔
167
      added.length ? last(added).node : last(attributes).node
77✔
168
    );
77✔
169

77✔
170
    for (const [i, slide] of this.slides.entries()) {
77✔
171
      if (slide.active && i !== idx) {
10✔
172
        slide.active = false;
3✔
173
      }
3✔
174
    }
10✔
175

3✔
176
    this.activateSlide(this.slides[idx]);
3✔
177
  }
77✔
178

10✔
179
  /**
10✔
180
   * Whether the carousel should skip rotating to the first slide after it reaches the last.
10✔
181
   * @attr disable-loop
10✔
182
   */
10✔
183
  @property({ type: Boolean, reflect: true, attribute: 'disable-loop' })
10✔
184
  public disableLoop = false;
10✔
185

10✔
186
  /**
10✔
187
   * Whether the carousel should ignore use interactions and not pause on them.
10✔
188
   * @attr disable-pause-on-interaction
10✔
189
   */
10✔
190
  @property({
10✔
191
    type: Boolean,
10✔
192
    reflect: true,
10✔
193
    attribute: 'disable-pause-on-interaction',
10✔
194
  })
10✔
195
  public disablePauseOnInteraction = false;
10✔
196

10✔
197
  /**
10✔
198
   * Whether the carousel should skip rendering of the default navigation buttons.
10✔
199
   * @attr hide-navigation
10✔
200
   */
10✔
201
  @property({ type: Boolean, reflect: true, attribute: 'hide-navigation' })
10✔
202
  public hideNavigation = false;
10✔
203

10✔
204
  /**
10✔
205
   * Whether the carousel should render the indicator controls (dots).
10✔
206
   * @attr hide-indicators
10✔
207
   */
10✔
208
  @property({ type: Boolean, reflect: true, attribute: 'hide-indicators' })
10✔
209
  public hideIndicators = false;
10✔
210

10✔
211
  /**
10✔
212
   * Whether the carousel has vertical alignment.
10✔
213
   * @attr vertical
10✔
214
   */
10✔
215
  @property({ type: Boolean, reflect: true })
10✔
216
  public vertical = false;
10✔
217

10✔
218
  /**
10✔
219
   * Sets the orientation of the indicator controls (dots).
10✔
220
   * @attr indicators-orientation
10✔
221
   */
10✔
222
  @property({ reflect: false, attribute: 'indicators-orientation' })
10✔
223
  public indicatorsOrientation: CarouselIndicatorsOrientation = 'end';
10✔
224

10✔
225
  /**
10✔
226
   * The format used to set the aria-label on the carousel indicators.
10✔
227
   * Instances of '{0}' will be replaced with the index of the corresponding slide.
10✔
228
   *
10✔
229
   * @attr indicators-label-format
10✔
230
   */
10✔
231
  @property({ attribute: 'indicators-label-format' })
10✔
232
  public indicatorsLabelFormat = 'Slide {0}';
10✔
233

10✔
234
  /**
10✔
235
   * The format used to set the aria-label on the carousel slides and the text displayed
10✔
236
   * when the number of indicators is greater than tha maximum indicator count.
10✔
237
   * Instances of '{0}' will be replaced with the index of the corresponding slide.
10✔
238
   * Instances of '{1}' will be replaced with the total amount of slides.
10✔
239
   *
10✔
240
   * @attr slides-label-format
10✔
241
   */
10✔
242
  @property({ attribute: 'slides-label-format' })
10✔
243
  public slidesLabelFormat = '{0} of {1}';
10✔
244

10✔
245
  /**
10✔
246
   * The duration in milliseconds between changing the active slide.
10✔
247
   * @attr interval
10✔
248
   */
10✔
249
  @property({ type: Number, reflect: false })
10✔
250
  public interval: number | undefined;
10✔
251

10✔
252
  /**
10✔
253
   * Controls the maximum indicator controls (dots) that can be shown. Default value is `10`.
10✔
254
   * @attr maximum-indicators-count
10✔
255
   */
10✔
256
  @property({
10✔
257
    type: Number,
10✔
258
    reflect: false,
10✔
259
    attribute: 'maximum-indicators-count',
10✔
260
  })
10✔
261
  public maximumIndicatorsCount = 10;
10✔
262

10✔
263
  /**
10✔
264
   * The animation type.
10✔
265
   * @attr animation-type
10✔
266
   */
10✔
267
  @property({ attribute: 'animation-type' })
10✔
268
  public animationType: HorizontalTransitionAnimation = 'slide';
10✔
269

10✔
270
  /* blazorSuppress */
10✔
271
  /**
10✔
272
   * The slides of the carousel.
10✔
273
   */
10✔
274
  @queryAssignedElements({ selector: IgcCarouselSlideComponent.tagName })
10✔
275
  public slides!: Array<IgcCarouselSlideComponent>;
10✔
276

10✔
277
  /**
10✔
278
   * The total number of slides.
10✔
279
   */
10✔
280
  public get total(): number {
10✔
281
    return this.slides.length;
360✔
282
  }
360✔
283

10✔
284
  /**
10✔
285
   * The index of the current active slide.
10✔
286
   */
10✔
287
  public get current(): number {
10✔
288
    return Math.max(0, this.slides.indexOf(this._activeSlide));
174✔
289
  }
174✔
290

10✔
291
  /**
10✔
292
   * Whether the carousel is in playing state.
10✔
293
   */
10✔
294
  public get isPlaying(): boolean {
10✔
295
    return this._playing;
60✔
296
  }
60✔
297

10✔
298
  /**
10✔
299
   * Whether the carousel in in paused state.
10✔
300
   */
10✔
301
  public get isPaused(): boolean {
10✔
302
    return this._paused;
18✔
303
  }
18✔
304

10✔
305
  @watch('animationType')
10✔
306
  @watch('slidesLabelFormat')
10✔
307
  @watch('indicatorsLabelFormat')
10✔
308
  protected contextChanged() {
10✔
309
    this._context.setValue(this, true);
130✔
310
  }
130✔
311

10✔
312
  @watch('interval')
10✔
313
  protected intervalChange() {
10✔
314
    if (!this.isPlaying) {
5✔
315
      this._playing = true;
5✔
316
    }
5✔
317

5✔
318
    this.restartInterval();
5✔
319
  }
5✔
320

10✔
321
  constructor() {
10✔
322
    super();
45✔
323

45✔
324
    addInternalsController(this, {
45✔
325
      initialARIA: {
45✔
326
        role: 'region',
45✔
327
        ariaRoleDescription: 'carousel',
45✔
328
      },
45✔
329
    });
45✔
330

45✔
331
    addThemingController(this, all);
45✔
332

45✔
333
    addSafeEventListener(this, 'pointerenter', this.handlePointerEnter);
45✔
334
    addSafeEventListener(this, 'pointerleave', this.handlePointerLeave);
45✔
335
    addSafeEventListener(this, 'pointerdown', () => {
45✔
336
      this._hasKeyboardFocus = false;
15✔
337
    });
45✔
338
    addSafeEventListener(this, 'keyup', () => {
45✔
339
      this._hasKeyboardFocus = true;
13✔
340
    });
45✔
341

45✔
342
    addGesturesController(this, {
45✔
343
      ref: this._carouselSlidesContainerRef,
45✔
344
      touchOnly: true,
45✔
345
    })
45✔
346
      .set('swipe-left', this.handleHorizontalSwipe)
45✔
347
      .set('swipe-right', this.handleHorizontalSwipe)
45✔
348
      .set('swipe-up', this.handleVerticalSwipe)
45✔
349
      .set('swipe-down', this.handleVerticalSwipe);
45✔
350

45✔
351
    addKeybindings(this, {
45✔
352
      ref: this._indicatorsContainerRef,
45✔
353
      bindingDefaults: { preventDefault: true },
45✔
354
    })
45✔
355
      .set(arrowLeft, this.handleArrowLeft)
45✔
356
      .set(arrowRight, this.handleArrowRight)
45✔
357
      .set(homeKey, this.handleHomeKey)
45✔
358
      .set(endKey, this.handleEndKey);
45✔
359

45✔
360
    addKeybindings(this, {
45✔
361
      ref: this._prevButtonRef,
45✔
362
      bindingDefaults: { preventDefault: true },
45✔
363
    }).setActivateHandler(this.handleNavigationInteractionPrevious);
45✔
364

45✔
365
    addKeybindings(this, {
45✔
366
      ref: this._nextButtonRef,
45✔
367
      bindingDefaults: { preventDefault: true },
45✔
368
    }).setActivateHandler(this.handleNavigationInteractionNext);
45✔
369

45✔
370
    createMutationController(this, {
45✔
371
      callback: this._observerCallback,
45✔
372
      filter: [IgcCarouselSlideComponent.tagName],
45✔
373
      config: {
45✔
374
        attributeFilter: ['active'],
45✔
375
        childList: true,
45✔
376
        subtree: true,
45✔
377
      },
45✔
378
    });
45✔
379
  }
45✔
380

10✔
381
  private handleSlotChange(): void {
10✔
382
    if (this.total) {
44✔
383
      this.activateSlide(
44✔
384
        this.slides.findLast((slide) => slide.active) ?? first(this.slides)
44✔
385
      );
44✔
386
    }
44✔
387
  }
44✔
388

10✔
389
  private handleIndicatorSlotChange(): void {
10✔
390
    this.requestUpdate();
3✔
391
  }
3✔
392

10✔
393
  private handlePointerEnter(): void {
10✔
394
    this._hasMouseStop = true;
4✔
395
    if (this._hasKeyboardFocus || this._hasMouseFocus) {
4!
NEW
396
      return;
×
UNCOV
397
    }
×
398
    this.handlePauseOnInteraction();
4✔
399
  }
4✔
400

10✔
401
  private handlePointerLeave(): void {
10✔
402
    this._hasMouseStop = false;
5✔
403
    if (this._hasKeyboardFocus || this._hasMouseFocus) {
5✔
404
      return;
2✔
405
    }
2✔
406
    this.handlePauseOnInteraction();
3✔
407
  }
5✔
408

10✔
409
  private handleFocusIn(): void {
10✔
410
    if (this._hasKeyboardFocus || this._hasMouseStop || this._hasMouseFocus) {
9✔
411
      this._hasMouseFocus = !this._hasKeyboardFocus && this._hasMouseStop;
7✔
412
      return;
7✔
413
    }
7✔
414
    this.handlePauseOnInteraction();
2✔
415
  }
9✔
416

10✔
417
  private handleFocusOut(event: FocusEvent): void {
10✔
418
    const node = event.relatedTarget as Node;
3✔
419

3✔
420
    if (this.contains(node) || this.renderRoot.contains(node)) {
3!
UNCOV
421
      return;
×
UNCOV
422
    }
×
423

3✔
424
    if (this._hasKeyboardFocus || this._hasMouseFocus) {
3✔
425
      this._hasKeyboardFocus = this._hasMouseFocus = false;
3✔
426

3✔
427
      if (!this._hasMouseStop) {
3✔
428
        this.handlePauseOnInteraction();
3✔
429
      }
3✔
430
    }
3✔
431
  }
3✔
432

10✔
433
  private handlePauseOnInteraction(): void {
10✔
434
    if (!this.interval || this.disablePauseOnInteraction) return;
12✔
435

6✔
436
    if (this.isPlaying) {
12✔
437
      this.pause();
3✔
438
      this.emitEvent('igcPaused');
3✔
439
    } else {
3✔
440
      this.play();
3✔
441
      this.emitEvent('igcPlaying');
3✔
442
    }
3✔
443
  }
12✔
444

10✔
445
  private async handleArrowLeft(): Promise<void> {
10✔
446
    this._hasKeyboardInteractionOnIndicators = true;
2✔
447
    this.handleInteraction(isLTR(this) ? this.prev : this.next);
2✔
448
  }
2✔
449

10✔
450
  private async handleArrowRight(): Promise<void> {
10✔
451
    this._hasKeyboardInteractionOnIndicators = true;
2✔
452
    this.handleInteraction(isLTR(this) ? this.next : this.prev);
2✔
453
  }
2✔
454

10✔
455
  private async handleHomeKey(): Promise<void> {
10✔
456
    this._hasKeyboardInteractionOnIndicators = true;
2✔
457
    this.handleInteraction(() =>
2✔
458
      this.select(isLTR(this) ? first(this.slides) : last(this.slides))
2✔
459
    );
2✔
460
  }
2✔
461

10✔
462
  private async handleEndKey(): Promise<void> {
10✔
463
    this._hasKeyboardInteractionOnIndicators = true;
2✔
464
    this.handleInteraction(() =>
2✔
465
      this.select(isLTR(this) ? last(this.slides) : first(this.slides))
2✔
466
    );
2✔
467
  }
2✔
468

10✔
469
  private handleVerticalSwipe({ data: { direction } }: SwipeEvent) {
10✔
470
    if (this.vertical) {
4✔
471
      this.handleInteraction(direction === 'up' ? this.next : this.prev);
2✔
472
    }
2✔
473
  }
4✔
474

10✔
475
  private handleHorizontalSwipe({ data: { direction } }: SwipeEvent) {
10✔
476
    if (!this.vertical) {
6✔
477
      this.handleInteraction(async () => {
4✔
478
        if (isLTR(this)) {
4✔
479
          direction === 'left' ? await this.next() : await this.prev();
2✔
480
        } else {
4✔
481
          direction === 'left' ? await this.prev() : await this.next();
2✔
482
        }
1✔
483
      });
4✔
484
    }
4✔
485
  }
6✔
486

10✔
487
  private async handleIndicatorClick(event: PointerEvent): Promise<void> {
10✔
488
    const indicator = findElementFromEventPath<IgcCarouselIndicatorComponent>(
2✔
489
      IgcCarouselIndicatorComponent.tagName,
2✔
490
      event
2✔
491
    )!;
2✔
492

2✔
493
    const index = this.hasProjectedIndicators
2!
UNCOV
494
      ? this._projectedIndicators.indexOf(indicator)
×
495
      : Array.from(this._defaultIndicators).indexOf(indicator);
2✔
496

2✔
497
    this.handleInteraction(() =>
2✔
498
      this.select(this.slides[index], index > this.current ? 'next' : 'prev')
2✔
499
    );
2✔
500
  }
2✔
501

10✔
502
  private handleNavigationInteractionNext() {
10✔
503
    this.handleInteraction(this.next);
3✔
504
  }
3✔
505

10✔
506
  private handleNavigationInteractionPrevious() {
10✔
507
    this.handleInteraction(this.prev);
3✔
508
  }
3✔
509

10✔
510
  private async handleInteraction(
10✔
511
    callback: () => Promise<unknown>
22✔
512
  ): Promise<void> {
22✔
513
    if (this.interval) {
22!
UNCOV
514
      this.resetInterval();
×
UNCOV
515
    }
×
516

22✔
517
    await callback.call(this);
22✔
518
    this.emitEvent('igcSlideChanged', { detail: this.current });
20✔
519

20✔
520
    if (this.interval) {
22!
UNCOV
521
      this.restartInterval();
×
UNCOV
522
    }
×
523
  }
22✔
524

10✔
525
  private activateSlide(slide: IgcCarouselSlideComponent): void {
10✔
526
    if (this._activeSlide) {
76✔
527
      this._activeSlide.active = false;
33✔
528
    }
33✔
529

76✔
530
    this._activeSlide = slide;
76✔
531
    this._activeSlide.active = true;
76✔
532

76✔
533
    if (this._hasKeyboardInteractionOnIndicators) {
76✔
534
      this.hasProjectedIndicators
8!
UNCOV
535
        ? this._projectedIndicators[this.current].focus()
×
536
        : this._defaultIndicators[this.current].focus();
8✔
537

8✔
538
      this._hasKeyboardInteractionOnIndicators = false;
8✔
539
    }
8✔
540
  }
76✔
541

10✔
542
  private updateProjectedIndicators(): void {
10✔
543
    for (const [idx, slide] of this.slides.entries()) {
3✔
544
      const indicator = this._projectedIndicators[idx];
3✔
545
      indicator.active = slide.active;
3✔
546
      indicator.index = idx;
3✔
547

3✔
548
      this.setAttribute('aria-controls', slide.id);
3✔
549
    }
3✔
550
  }
3✔
551

10✔
552
  private resetInterval(): void {
10✔
553
    if (this._lastInterval) {
13✔
554
      clearInterval(this._lastInterval);
3✔
555
      this._lastInterval = null;
3✔
556
    }
3✔
557
  }
13✔
558

10✔
559
  private restartInterval(): void {
10✔
560
    this.resetInterval();
9✔
561

9✔
562
    if (asNumber(this.interval) > 0) {
9✔
563
      this._lastInterval = setInterval(() => {
8✔
564
        if (this.isPlaying && this.total) {
5✔
565
          this.next();
3✔
566
          this.emitEvent('igcSlideChanged', { detail: this.current });
3✔
567
        } else {
5✔
568
          this.pause();
2✔
569
        }
2✔
570
      }, this.interval);
8✔
571
    }
8✔
572
  }
9✔
573

10✔
574
  private async animateSlides(
10✔
575
    nextSlide: IgcCarouselSlideComponent,
29✔
576
    currentSlide: IgcCarouselSlideComponent,
29✔
577
    dir: 'next' | 'prev'
29✔
578
  ): Promise<void> {
29✔
579
    if (dir === 'next') {
29✔
580
      // Animate slides in next direction
16✔
581
      currentSlide.previous = true;
16✔
582
      currentSlide.toggleAnimation('out');
16✔
583
      this.activateSlide(nextSlide);
16✔
584
      await nextSlide.toggleAnimation('in');
16✔
585
      currentSlide.previous = false;
15✔
586
    } else {
29✔
587
      // Animate slides in previous direction
13✔
588
      currentSlide.previous = true;
13✔
589
      currentSlide.toggleAnimation('in', 'reverse');
13✔
590
      this.activateSlide(nextSlide);
13✔
591
      await nextSlide.toggleAnimation('out', 'reverse');
13✔
592
      currentSlide.previous = false;
12✔
593
    }
12✔
594
  }
29✔
595

10✔
596
  /**
10✔
597
   * Resumes playing of the carousel slides.
10✔
598
   */
10✔
599
  public play(): void {
10✔
600
    if (!this.isPlaying) {
4✔
601
      this._paused = false;
4✔
602
      this._playing = true;
4✔
603
      this.restartInterval();
4✔
604
    }
4✔
605
  }
4✔
606

10✔
607
  /**
10✔
608
   * Pauses the carousel rotation of slides.
10✔
609
   */
10✔
610
  public pause(): void {
10✔
611
    if (this.isPlaying) {
8✔
612
      this._playing = false;
4✔
613
      this._paused = true;
4✔
614
      this.resetInterval();
4✔
615
    }
4✔
616
  }
8✔
617

10✔
618
  /**
10✔
619
   * Switches to the next slide, runs any animations, and returns if the operation was successful.
10✔
620
   */
10✔
621
  public async next(): Promise<boolean> {
10✔
622
    if (this.disableLoop && this.nextIndex === 0) {
13✔
623
      this.pause();
1✔
624
      return false;
1✔
625
    }
1✔
626

12✔
627
    return await this.select(this.slides[this.nextIndex], 'next');
12✔
628
  }
13✔
629

10✔
630
  /**
10✔
631
   * Switches to the previous slide, runs any animations, and returns if the operation was successful.
10✔
632
   */
10✔
633
  public async prev(): Promise<boolean> {
10✔
634
    if (this.disableLoop && this.prevIndex === this.total - 1) {
10✔
635
      this.pause();
1✔
636
      return false;
1✔
637
    }
1✔
638

9✔
639
    return await this.select(this.slides[this.prevIndex], 'prev');
9✔
640
  }
10✔
641

10✔
642
  /* blazorSuppress */
10✔
643
  /**
10✔
644
   * Switches to the passed-in slide, runs any animations, and returns if the operation was successful.
10✔
645
   */
10✔
646
  public async select(
10✔
647
    slide: IgcCarouselSlideComponent,
10✔
648
    animationDirection?: 'next' | 'prev'
10✔
649
  ): Promise<boolean>;
10✔
650
  /**
10✔
651
   * Switches to slide by index, runs any animations, and returns if the operation was successful.
10✔
652
   */
10✔
653
  public async select(
10✔
654
    index: number,
10✔
655
    animationDirection?: 'next' | 'prev'
10✔
656
  ): Promise<boolean>;
10✔
657
  public async select(
10✔
658
    slideOrIndex: IgcCarouselSlideComponent | number,
33✔
659
    animationDirection?: 'next' | 'prev'
33✔
660
  ): Promise<boolean> {
33✔
661
    let index: number;
33✔
662
    let slide: IgcCarouselSlideComponent | undefined;
33✔
663

33✔
664
    if (typeof slideOrIndex === 'number') {
33✔
665
      index = slideOrIndex;
3✔
666
      slide = this.slides.at(index);
3✔
667
    } else {
33✔
668
      slide = slideOrIndex;
30✔
669
      index = this.slides.indexOf(slide);
30✔
670
    }
30✔
671

33✔
672
    if (index === this.current || index === -1 || !slide) {
33✔
673
      return false;
4✔
674
    }
4✔
675

29✔
676
    const dir = animationDirection ?? (index > this.current ? 'next' : 'prev');
33✔
677

33✔
678
    await this.animateSlides(slide, this._activeSlide, dir);
33✔
679
    return true;
27✔
680
  }
33✔
681

10✔
682
  private navigationTemplate() {
10✔
683
    return html`
140✔
684
      <igc-button
140✔
685
        ${ref(this._prevButtonRef)}
140✔
686
        type="button"
140✔
687
        part="navigation previous"
140✔
688
        aria-label="Previous slide"
140✔
689
        aria-controls=${this._carouselId}
140✔
690
        ?disabled=${this.disableLoop && this.current === 0}
140✔
691
        @click=${this.handleNavigationInteractionPrevious}
140✔
692
      >
140✔
693
        <slot name="previous-button">
140✔
694
          <igc-icon
140✔
695
            name="carousel_prev"
140✔
696
            collection="default"
140✔
697
            aria-hidden="true"
140✔
698
          ></igc-icon>
140✔
699
        </slot>
140✔
700
      </igc-button>
140✔
701
      <igc-button
140✔
702
        ${ref(this._nextButtonRef)}
140✔
703
        type="button"
140✔
704
        part="navigation next"
140✔
705
        aria-label="Next slide"
140✔
706
        aria-controls=${this._carouselId}
140✔
707
        ?disabled=${this.disableLoop && this.current === this.total - 1}
140✔
708
        @click=${this.handleNavigationInteractionNext}
140✔
709
      >
140✔
710
        <slot name="next-button">
140✔
711
          <igc-icon
140✔
712
            name="carousel_next"
140✔
713
            collection="default"
140✔
714
            aria-hidden="true"
140✔
715
          ></igc-icon>
140✔
716
        </slot>
140✔
717
      </igc-button>
140✔
718
    `;
140✔
719
  }
140✔
720

10✔
721
  protected *renderIndicators() {
10✔
722
    for (const [i, slide] of this.slides.entries()) {
133✔
723
      const forward = slide.active ? 'visible' : 'hidden';
267✔
724
      const backward = slide.active ? 'hidden' : 'visible';
267✔
725

267✔
726
      yield html`
267✔
727
        <igc-carousel-indicator
267✔
728
          exportparts="indicator, active, inactive"
267✔
729
          .active=${slide.active}
267✔
730
          .index=${i}
267✔
731
        >
267✔
732
          <div
267✔
733
            part="dot"
267✔
734
            style=${styleMap({ visibility: backward, zIndex: 1 })}
267✔
735
          ></div>
267✔
736
          <div
267✔
737
            part="dot active"
267✔
738
            slot="active"
267✔
739
            style=${styleMap({ visibility: forward })}
267✔
740
          ></div>
267✔
741
        </igc-carousel-indicator>
267✔
742
      `;
267✔
743
    }
267✔
744
  }
133✔
745

10✔
746
  private indicatorTemplate() {
10✔
747
    const parts = {
136✔
748
      indicators: true,
136✔
749
      start: this.indicatorsOrientation === 'start',
136✔
750
    };
136✔
751

136✔
752
    return html`
136✔
753
      <igc-carousel-indicator-container>
136✔
754
        <div
136✔
755
          ${ref(this._indicatorsContainerRef)}
136✔
756
          role="tablist"
136✔
757
          part=${partMap(parts)}
136✔
758
        >
136✔
759
          <slot
136✔
760
            name="indicator"
136✔
761
            @slotchange=${this.handleIndicatorSlotChange}
136✔
762
            @click=${this.handleIndicatorClick}
136✔
763
          >
136✔
764
            ${this.hasProjectedIndicators
136✔
765
              ? this.updateProjectedIndicators()
3✔
766
              : this.renderIndicators()}
136✔
767
          </slot>
136✔
768
        </div>
136✔
769
      </igc-carousel-indicator-container>
136✔
770
    `;
136✔
771
  }
136✔
772

10✔
773
  private labelTemplate() {
10✔
774
    const parts = {
3✔
775
      label: true,
3✔
776
      indicators: true,
3✔
777
      start: this.indicatorsOrientation === 'start',
3✔
778
    };
3✔
779
    const value = formatString(
3✔
780
      this.slidesLabelFormat,
3✔
781
      this.current + 1,
3✔
782
      this.total
3✔
783
    );
3✔
784

3✔
785
    return html`
3✔
786
      <div part=${partMap(parts)}>
3✔
787
        <span>${value}</span>
3✔
788
      </div>
3✔
789
    `;
3✔
790
  }
3✔
791

10✔
792
  protected override render() {
10✔
793
    return html`
141✔
794
      <section @focusin=${this.handleFocusIn} @focusout=${this.handleFocusOut}>
141✔
795
        ${this.hideNavigation ? nothing : this.navigationTemplate()}
141✔
796
        ${this.hideIndicators || this.showIndicatorsLabel
141✔
797
          ? nothing
5✔
798
          : this.indicatorTemplate()}
141✔
799
        ${!this.hideIndicators && this.showIndicatorsLabel
141✔
800
          ? this.labelTemplate()
3✔
801
          : nothing}
141✔
802
        <div
141✔
803
          ${ref(this._carouselSlidesContainerRef)}
141✔
804
          id=${this._carouselId}
141✔
805
          aria-live=${this.interval && this.isPlaying ? 'off' : 'polite'}
141✔
806
        >
141✔
807
          <slot @slotchange=${this.handleSlotChange}></slot>
141✔
808
        </div>
141✔
809
      </section>
141✔
810
    `;
141✔
811
  }
141✔
812
}
10✔
813

10✔
814
declare global {
10✔
815
  interface HTMLElementTagNameMap {
10✔
816
    'igc-carousel': IgcCarouselComponent;
10✔
817
  }
10✔
818
}
10✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc