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

IgniteUI / igniteui-webcomponents / 16120745768

07 Jul 2025 03:08PM UTC coverage: 98.286% (+0.02%) from 98.271%
16120745768

Pull #1774

github

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

4943 of 5187 branches covered (95.3%)

Branch coverage included in aggregate %.

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

7 existing lines in 1 file now uncovered.

31760 of 32156 relevant lines covered (98.77%)

1729.91 hits per line

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

98.61
/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 _paused = false;
10✔
109
  private _lastInterval!: ReturnType<typeof setInterval> | null;
10✔
110
  private _hasKeyboardInteractionOnIndicators = false;
10✔
111
  private _hasPointerInteraction = false;
10✔
112
  private _hasInnerFocus = 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
  private _observerCallback({
10✔
156
    changes: { added, attributes },
77✔
157
  }: MutationControllerParams<IgcCarouselSlideComponent>) {
77✔
158
    const activeSlides = this.slides.filter((slide) => slide.active);
77✔
159

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

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

3✔
173
    this.activateSlide(this.slides[idx]);
3✔
174
  }
77✔
175

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

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

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

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

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

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

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

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

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

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

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

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

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

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

10✔
288
  /**
10✔
289
   * Whether the carousel is in playing state.
10✔
290
   */
10✔
291
  public get isPlaying(): boolean {
10✔
292
    return this._playing;
46✔
293
  }
46✔
294

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

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

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

5✔
315
    this.restartInterval();
5✔
316
  }
5✔
317

10✔
318
  constructor() {
10✔
319
    super();
45✔
320

45✔
321
    addInternalsController(this, {
45✔
322
      initialARIA: {
45✔
323
        role: 'region',
45✔
324
        ariaRoleDescription: 'carousel',
45✔
325
      },
45✔
326
    });
45✔
327

45✔
328
    addThemingController(this, all);
45✔
329

45✔
330
    addSafeEventListener(this, 'pointerenter', this.handlePointerInteraction);
45✔
331
    addSafeEventListener(this, 'pointerleave', this.handlePointerInteraction);
45✔
332
    addSafeEventListener(this, 'pointerdown', () => {
45✔
333
      this._hasInnerFocus = false;
15✔
334
    });
45✔
335
    addSafeEventListener(this, 'focusin', this.handleFocusInteraction);
45✔
336
    addSafeEventListener(this, 'focusout', this.handleFocusInteraction);
45✔
337

45✔
338
    addGesturesController(this, {
45✔
339
      ref: this._carouselSlidesContainerRef,
45✔
340
      touchOnly: true,
45✔
341
    })
45✔
342
      .set('swipe-left', this.handleHorizontalSwipe)
45✔
343
      .set('swipe-right', this.handleHorizontalSwipe)
45✔
344
      .set('swipe-up', this.handleVerticalSwipe)
45✔
345
      .set('swipe-down', this.handleVerticalSwipe);
45✔
346

45✔
347
    addKeybindings(this, {
45✔
348
      ref: this._indicatorsContainerRef,
45✔
349
      bindingDefaults: { preventDefault: true },
45✔
350
    })
45✔
351
      .set(arrowLeft, this.handleArrowLeft)
45✔
352
      .set(arrowRight, this.handleArrowRight)
45✔
353
      .set(homeKey, this.handleHomeKey)
45✔
354
      .set(endKey, this.handleEndKey);
45✔
355

45✔
356
    addKeybindings(this, {
45✔
357
      ref: this._prevButtonRef,
45✔
358
      bindingDefaults: { preventDefault: true },
45✔
359
    }).setActivateHandler(this.handleNavigationInteractionPrevious);
45✔
360

45✔
361
    addKeybindings(this, {
45✔
362
      ref: this._nextButtonRef,
45✔
363
      bindingDefaults: { preventDefault: true },
45✔
364
    }).setActivateHandler(this.handleNavigationInteractionNext);
45✔
365

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

10✔
377
  private handleSlotChange(): void {
10✔
378
    if (this.total) {
44✔
379
      this.activateSlide(
44✔
380
        this.slides.findLast((slide) => slide.active) ?? first(this.slides)
44✔
381
      );
44✔
382
    }
44✔
383
  }
44✔
384

10✔
385
  private handleIndicatorSlotChange(): void {
10✔
386
    this.requestUpdate();
3✔
387
  }
3✔
388

10✔
389
  private handlePointerInteraction(event: PointerEvent): void {
10✔
390
    this._hasPointerInteraction = event.type === 'pointerenter';
9✔
391

9✔
392
    if (!this._hasInnerFocus) {
9✔
393
      this.handlePauseOnInteraction();
7✔
394
    }
7✔
395
  }
9✔
396

10✔
397
  private handleFocusInteraction(event: FocusEvent): void {
10✔
398
    // focusin - element that lost focus
7✔
399
    // focusout - element that gained focus
7✔
400
    const node = event.relatedTarget as Node;
7✔
401

7✔
402
    if (this.contains(node)) {
7!
NEW
403
      return;
×
UNCOV
404
    }
×
405

7✔
406
    this._hasInnerFocus = event.type === 'focusin';
7✔
407

7✔
408
    if (!this._hasPointerInteraction) {
7✔
409
      this.handlePauseOnInteraction();
5✔
410
    }
5✔
411
  }
7✔
412

10✔
413
  private handlePauseOnInteraction(): void {
10✔
414
    if (!this.interval || this.disablePauseOnInteraction) return;
12✔
415

6✔
416
    if (this.isPlaying) {
12✔
417
      this.pause();
3✔
418
      this.emitEvent('igcPaused');
3✔
419
    } else {
3✔
420
      this.play();
3✔
421
      this.emitEvent('igcPlaying');
3✔
422
    }
3✔
423
  }
12✔
424

10✔
425
  private async handleArrowLeft(): Promise<void> {
10✔
426
    this._hasKeyboardInteractionOnIndicators = true;
2✔
427
    this.handleInteraction(isLTR(this) ? this.prev : this.next);
2✔
428
  }
2✔
429

10✔
430
  private async handleArrowRight(): Promise<void> {
10✔
431
    this._hasKeyboardInteractionOnIndicators = true;
2✔
432
    this.handleInteraction(isLTR(this) ? this.next : this.prev);
2✔
433
  }
2✔
434

10✔
435
  private async handleHomeKey(): Promise<void> {
10✔
436
    this._hasKeyboardInteractionOnIndicators = true;
2✔
437
    this.handleInteraction(() =>
2✔
438
      this.select(isLTR(this) ? first(this.slides) : last(this.slides))
2✔
439
    );
2✔
440
  }
2✔
441

10✔
442
  private async handleEndKey(): Promise<void> {
10✔
443
    this._hasKeyboardInteractionOnIndicators = true;
2✔
444
    this.handleInteraction(() =>
2✔
445
      this.select(isLTR(this) ? last(this.slides) : first(this.slides))
2✔
446
    );
2✔
447
  }
2✔
448

10✔
449
  private handleVerticalSwipe({ data: { direction } }: SwipeEvent) {
10✔
450
    if (this.vertical) {
4✔
451
      this.handleInteraction(direction === 'up' ? this.next : this.prev);
2✔
452
    }
2✔
453
  }
4✔
454

10✔
455
  private handleHorizontalSwipe({ data: { direction } }: SwipeEvent) {
10✔
456
    if (!this.vertical) {
6✔
457
      this.handleInteraction(async () => {
4✔
458
        if (isLTR(this)) {
4✔
459
          direction === 'left' ? await this.next() : await this.prev();
2✔
460
        } else {
4✔
461
          direction === 'left' ? await this.prev() : await this.next();
2✔
462
        }
1✔
463
      });
4✔
464
    }
4✔
465
  }
6✔
466

10✔
467
  private async handleIndicatorClick(event: PointerEvent): Promise<void> {
10✔
468
    const indicator = findElementFromEventPath<IgcCarouselIndicatorComponent>(
2✔
469
      IgcCarouselIndicatorComponent.tagName,
2✔
470
      event
2✔
471
    )!;
2✔
472

2✔
473
    const index = this.hasProjectedIndicators
2!
UNCOV
474
      ? this._projectedIndicators.indexOf(indicator)
×
475
      : Array.from(this._defaultIndicators).indexOf(indicator);
2✔
476

2✔
477
    this.handleInteraction(() =>
2✔
478
      this.select(this.slides[index], index > this.current ? 'next' : 'prev')
2✔
479
    );
2✔
480
  }
2✔
481

10✔
482
  private handleNavigationInteractionNext() {
10✔
483
    this.handleInteraction(this.next);
3✔
484
  }
3✔
485

10✔
486
  private handleNavigationInteractionPrevious() {
10✔
487
    this.handleInteraction(this.prev);
3✔
488
  }
3✔
489

10✔
490
  private async handleInteraction(
10✔
491
    callback: () => Promise<unknown>
22✔
492
  ): Promise<void> {
22✔
493
    if (this.interval) {
22!
UNCOV
494
      this.resetInterval();
×
UNCOV
495
    }
×
496

22✔
497
    await callback.call(this);
22✔
498
    this.emitEvent('igcSlideChanged', { detail: this.current });
20✔
499

20✔
500
    if (this.interval) {
22!
UNCOV
501
      this.restartInterval();
×
UNCOV
502
    }
×
503
  }
22✔
504

10✔
505
  private activateSlide(slide: IgcCarouselSlideComponent): void {
10✔
506
    if (this._activeSlide) {
76✔
507
      this._activeSlide.active = false;
33✔
508
    }
33✔
509

76✔
510
    this._activeSlide = slide;
76✔
511
    this._activeSlide.active = true;
76✔
512

76✔
513
    if (this._hasKeyboardInteractionOnIndicators) {
76✔
514
      this.hasProjectedIndicators
8!
UNCOV
515
        ? this._projectedIndicators[this.current].focus()
×
516
        : this._defaultIndicators[this.current].focus();
8✔
517

8✔
518
      this._hasKeyboardInteractionOnIndicators = false;
8✔
519
    }
8✔
520
  }
76✔
521

10✔
522
  private updateProjectedIndicators(): void {
10✔
523
    for (const [idx, slide] of this.slides.entries()) {
3✔
524
      const indicator = this._projectedIndicators[idx];
3✔
525
      indicator.active = slide.active;
3✔
526
      indicator.index = idx;
3✔
527

3✔
528
      this.setAttribute('aria-controls', slide.id);
3✔
529
    }
3✔
530
  }
3✔
531

10✔
532
  private resetInterval(): void {
10✔
533
    if (this._lastInterval) {
13✔
534
      clearInterval(this._lastInterval);
3✔
535
      this._lastInterval = null;
3✔
536
    }
3✔
537
  }
13✔
538

10✔
539
  private restartInterval(): void {
10✔
540
    this.resetInterval();
9✔
541

9✔
542
    if (asNumber(this.interval) > 0) {
9✔
543
      this._lastInterval = setInterval(() => {
8✔
544
        if (this.isPlaying && this.total) {
5✔
545
          this.next();
3✔
546
          this.emitEvent('igcSlideChanged', { detail: this.current });
3✔
547
        } else {
5✔
548
          this.pause();
2✔
549
        }
2✔
550
      }, this.interval);
8✔
551
    }
8✔
552
  }
9✔
553

10✔
554
  private async animateSlides(
10✔
555
    nextSlide: IgcCarouselSlideComponent,
29✔
556
    currentSlide: IgcCarouselSlideComponent,
29✔
557
    dir: 'next' | 'prev'
29✔
558
  ): Promise<void> {
29✔
559
    if (dir === 'next') {
29✔
560
      // Animate slides in next direction
16✔
561
      currentSlide.previous = true;
16✔
562
      currentSlide.toggleAnimation('out');
16✔
563
      this.activateSlide(nextSlide);
16✔
564
      await nextSlide.toggleAnimation('in');
16✔
565
      currentSlide.previous = false;
15✔
566
    } else {
29✔
567
      // Animate slides in previous direction
13✔
568
      currentSlide.previous = true;
13✔
569
      currentSlide.toggleAnimation('in', 'reverse');
13✔
570
      this.activateSlide(nextSlide);
13✔
571
      await nextSlide.toggleAnimation('out', 'reverse');
13✔
572
      currentSlide.previous = false;
12✔
573
    }
12✔
574
  }
29✔
575

10✔
576
  /**
10✔
577
   * Resumes playing of the carousel slides.
10✔
578
   */
10✔
579
  public play(): void {
10✔
580
    if (!this.isPlaying) {
4✔
581
      this._paused = false;
4✔
582
      this._playing = true;
4✔
583
      this.restartInterval();
4✔
584
    }
4✔
585
  }
4✔
586

10✔
587
  /**
10✔
588
   * Pauses the carousel rotation of slides.
10✔
589
   */
10✔
590
  public pause(): void {
10✔
591
    if (this.isPlaying) {
8✔
592
      this._playing = false;
4✔
593
      this._paused = true;
4✔
594
      this.resetInterval();
4✔
595
    }
4✔
596
  }
8✔
597

10✔
598
  /**
10✔
599
   * Switches to the next slide, runs any animations, and returns if the operation was successful.
10✔
600
   */
10✔
601
  public async next(): Promise<boolean> {
10✔
602
    if (this.disableLoop && this.nextIndex === 0) {
13✔
603
      this.pause();
1✔
604
      return false;
1✔
605
    }
1✔
606

12✔
607
    return await this.select(this.slides[this.nextIndex], 'next');
12✔
608
  }
13✔
609

10✔
610
  /**
10✔
611
   * Switches to the previous slide, runs any animations, and returns if the operation was successful.
10✔
612
   */
10✔
613
  public async prev(): Promise<boolean> {
10✔
614
    if (this.disableLoop && this.prevIndex === this.total - 1) {
10✔
615
      this.pause();
1✔
616
      return false;
1✔
617
    }
1✔
618

9✔
619
    return await this.select(this.slides[this.prevIndex], 'prev');
9✔
620
  }
10✔
621

10✔
622
  /* blazorSuppress */
10✔
623
  /**
10✔
624
   * Switches to the passed-in slide, runs any animations, and returns if the operation was successful.
10✔
625
   */
10✔
626
  public async select(
10✔
627
    slide: IgcCarouselSlideComponent,
10✔
628
    animationDirection?: 'next' | 'prev'
10✔
629
  ): Promise<boolean>;
10✔
630
  /**
10✔
631
   * Switches to slide by index, runs any animations, and returns if the operation was successful.
10✔
632
   */
10✔
633
  public async select(
10✔
634
    index: number,
10✔
635
    animationDirection?: 'next' | 'prev'
10✔
636
  ): Promise<boolean>;
10✔
637
  public async select(
10✔
638
    slideOrIndex: IgcCarouselSlideComponent | number,
33✔
639
    animationDirection?: 'next' | 'prev'
33✔
640
  ): Promise<boolean> {
33✔
641
    let index: number;
33✔
642
    let slide: IgcCarouselSlideComponent | undefined;
33✔
643

33✔
644
    if (typeof slideOrIndex === 'number') {
33✔
645
      index = slideOrIndex;
3✔
646
      slide = this.slides.at(index);
3✔
647
    } else {
33✔
648
      slide = slideOrIndex;
30✔
649
      index = this.slides.indexOf(slide);
30✔
650
    }
30✔
651

33✔
652
    if (index === this.current || index === -1 || !slide) {
33✔
653
      return false;
4✔
654
    }
4✔
655

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

33✔
658
    await this.animateSlides(slide, this._activeSlide, dir);
33✔
659
    return true;
27✔
660
  }
33✔
661

10✔
662
  private navigationTemplate() {
10✔
663
    return html`
140✔
664
      <igc-button
140✔
665
        ${ref(this._prevButtonRef)}
140✔
666
        type="button"
140✔
667
        part="navigation previous"
140✔
668
        aria-label="Previous slide"
140✔
669
        aria-controls=${this._carouselId}
140✔
670
        ?disabled=${this.disableLoop && this.current === 0}
140✔
671
        @click=${this.handleNavigationInteractionPrevious}
140✔
672
      >
140✔
673
        <slot name="previous-button">
140✔
674
          <igc-icon
140✔
675
            name="carousel_prev"
140✔
676
            collection="default"
140✔
677
            aria-hidden="true"
140✔
678
          ></igc-icon>
140✔
679
        </slot>
140✔
680
      </igc-button>
140✔
681
      <igc-button
140✔
682
        ${ref(this._nextButtonRef)}
140✔
683
        type="button"
140✔
684
        part="navigation next"
140✔
685
        aria-label="Next slide"
140✔
686
        aria-controls=${this._carouselId}
140✔
687
        ?disabled=${this.disableLoop && this.current === this.total - 1}
140✔
688
        @click=${this.handleNavigationInteractionNext}
140✔
689
      >
140✔
690
        <slot name="next-button">
140✔
691
          <igc-icon
140✔
692
            name="carousel_next"
140✔
693
            collection="default"
140✔
694
            aria-hidden="true"
140✔
695
          ></igc-icon>
140✔
696
        </slot>
140✔
697
      </igc-button>
140✔
698
    `;
140✔
699
  }
140✔
700

10✔
701
  protected *renderIndicators() {
10✔
702
    for (const [i, slide] of this.slides.entries()) {
133✔
703
      const forward = slide.active ? 'visible' : 'hidden';
267✔
704
      const backward = slide.active ? 'hidden' : 'visible';
267✔
705

267✔
706
      yield html`
267✔
707
        <igc-carousel-indicator
267✔
708
          exportparts="indicator, active, inactive"
267✔
709
          .active=${slide.active}
267✔
710
          .index=${i}
267✔
711
        >
267✔
712
          <div
267✔
713
            part="dot"
267✔
714
            style=${styleMap({ visibility: backward, zIndex: 1 })}
267✔
715
          ></div>
267✔
716
          <div
267✔
717
            part="dot active"
267✔
718
            slot="active"
267✔
719
            style=${styleMap({ visibility: forward })}
267✔
720
          ></div>
267✔
721
        </igc-carousel-indicator>
267✔
722
      `;
267✔
723
    }
267✔
724
  }
133✔
725

10✔
726
  private indicatorTemplate() {
10✔
727
    const parts = {
136✔
728
      indicators: true,
136✔
729
      start: this.indicatorsOrientation === 'start',
136✔
730
    };
136✔
731

136✔
732
    return html`
136✔
733
      <igc-carousel-indicator-container>
136✔
734
        <div
136✔
735
          ${ref(this._indicatorsContainerRef)}
136✔
736
          role="tablist"
136✔
737
          part=${partMap(parts)}
136✔
738
        >
136✔
739
          <slot
136✔
740
            name="indicator"
136✔
741
            @slotchange=${this.handleIndicatorSlotChange}
136✔
742
            @click=${this.handleIndicatorClick}
136✔
743
          >
136✔
744
            ${this.hasProjectedIndicators
136✔
745
              ? this.updateProjectedIndicators()
3✔
746
              : this.renderIndicators()}
136✔
747
          </slot>
136✔
748
        </div>
136✔
749
      </igc-carousel-indicator-container>
136✔
750
    `;
136✔
751
  }
136✔
752

10✔
753
  private labelTemplate() {
10✔
754
    const parts = {
3✔
755
      label: true,
3✔
756
      indicators: true,
3✔
757
      start: this.indicatorsOrientation === 'start',
3✔
758
    };
3✔
759
    const value = formatString(
3✔
760
      this.slidesLabelFormat,
3✔
761
      this.current + 1,
3✔
762
      this.total
3✔
763
    );
3✔
764

3✔
765
    return html`
3✔
766
      <div part=${partMap(parts)}>
3✔
767
        <span>${value}</span>
3✔
768
      </div>
3✔
769
    `;
3✔
770
  }
3✔
771

10✔
772
  protected override render() {
10✔
773
    return html`
141✔
774
      <section>
141✔
775
        ${this.hideNavigation ? nothing : this.navigationTemplate()}
141✔
776
        ${this.hideIndicators || this.showIndicatorsLabel
141✔
777
          ? nothing
5✔
778
          : this.indicatorTemplate()}
141✔
779
        ${!this.hideIndicators && this.showIndicatorsLabel
141✔
780
          ? this.labelTemplate()
3✔
781
          : nothing}
141✔
782
        <div
141✔
783
          ${ref(this._carouselSlidesContainerRef)}
141✔
784
          id=${this._carouselId}
141✔
785
          aria-live=${this.interval && this._playing ? 'off' : 'polite'}
141✔
786
        >
141✔
787
          <slot @slotchange=${this.handleSlotChange}></slot>
141✔
788
        </div>
141✔
789
      </section>
141✔
790
    `;
141✔
791
  }
141✔
792
}
10✔
793

10✔
794
declare global {
10✔
795
  interface HTMLElementTagNameMap {
10✔
796
    'igc-carousel': IgcCarouselComponent;
10✔
797
  }
10✔
798
}
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

© 2026 Coveralls, Inc