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

IgniteUI / igniteui-webcomponents / 16135960321

08 Jul 2025 06:43AM UTC coverage: 98.288% (+0.02%) from 98.271%
16135960321

Pull #1767

github

web-flow
Merge 1adc67f23 into 50478dee2
Pull Request #1767: feat: Slot controller

4949 of 5199 branches covered (95.19%)

Branch coverage included in aggregate %.

949 of 956 new or added lines in 19 files covered. (99.27%)

10 existing lines in 2 files now uncovered.

31975 of 32368 relevant lines covered (98.79%)

1725.99 hits per line

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

98.02
/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 _hasInnerFocus = false;
10✔
112

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3✔
175
    this.activateSlide(this.slides[idx]);
3✔
176
  }
74✔
177

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

4✔
317
    this.restartInterval();
4✔
318
  }
4✔
319

10✔
320
  constructor() {
10✔
321
    super();
44✔
322

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

44✔
330
    addThemingController(this, all);
44✔
331

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

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

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

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

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

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

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

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

10✔
392
  private handlePointerEnter(): void {
10✔
393
    this._hasMouseStop = true;
3✔
394
    if (this._hasInnerFocus) {
3!
395
      return;
×
396
    }
×
397
    this.handlePauseOnInteraction();
3✔
398
  }
3✔
399

10✔
400
  private handlePointerLeave(): void {
10✔
401
    this._hasMouseStop = false;
4✔
402
    if (this._hasInnerFocus) {
4✔
403
      return;
1✔
404
    }
1✔
405
    this.handlePauseOnInteraction();
3✔
406
  }
4✔
407

10✔
408
  private handleFocusIn(): void {
10✔
409
    if (this._hasInnerFocus || this._hasMouseStop) {
8✔
410
      return;
6✔
411
    }
6✔
412
    this.handlePauseOnInteraction();
2✔
413
  }
8✔
414

10✔
415
  private handleFocusOut(event: FocusEvent): void {
10✔
416
    const node = event.relatedTarget as Node;
2✔
417

2✔
418
    if (this.contains(node) || this.renderRoot.contains(node)) {
2!
419
      return;
×
420
    }
×
421

2✔
422
    if (this._hasInnerFocus) {
2✔
423
      this._hasInnerFocus = false;
2✔
424

2✔
425
      if (!this._hasMouseStop) {
2✔
426
        this.handlePauseOnInteraction();
2✔
427
      }
2✔
428
    }
2✔
429
  }
2✔
430

10✔
431
  private handlePauseOnInteraction(): void {
10✔
432
    if (!this.interval || this.disablePauseOnInteraction) return;
10✔
433

4✔
434
    if (this.isPlaying) {
10✔
435
      this.pause();
2✔
436
      this.emitEvent('igcPaused');
2✔
437
    } else {
2✔
438
      this.play();
2✔
439
      this.emitEvent('igcPlaying');
2✔
440
    }
2✔
441
  }
10✔
442

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

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

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

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

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

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

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

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

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

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

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

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

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

20✔
518
    if (this.interval) {
22!
519
      this.restartInterval();
×
520
    }
×
521
  }
22✔
522

10✔
523
  private activateSlide(slide: IgcCarouselSlideComponent): void {
10✔
524
    if (this._activeSlide) {
73✔
525
      this._activeSlide.active = false;
31✔
526
    }
31✔
527

73✔
528
    this._activeSlide = slide;
73✔
529
    this._activeSlide.active = true;
73✔
530

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

8✔
536
      this._hasKeyboardInteractionOnIndicators = false;
8✔
537
    }
8✔
538
  }
73✔
539

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

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

10✔
550
  private resetInterval(): void {
10✔
551
    if (this._lastInterval) {
10✔
552
      clearInterval(this._lastInterval);
2✔
553
      this._lastInterval = null;
2✔
554
    }
2✔
555
  }
10✔
556

10✔
557
  private restartInterval(): void {
10✔
558
    this.resetInterval();
7✔
559

7✔
560
    if (asNumber(this.interval) > 0) {
7✔
561
      this._lastInterval = setInterval(() => {
6✔
562
        if (this.isPlaying && this.total) {
1✔
563
          this.next();
1✔
564
          this.emitEvent('igcSlideChanged', { detail: this.current });
1✔
565
        } else {
1!
566
          this.pause();
×
567
        }
×
568
      }, this.interval);
6✔
569
    }
6✔
570
  }
7✔
571

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

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

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

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

10✔
625
    return await this.select(this.slides[this.nextIndex], 'next');
10✔
626
  }
11✔
627

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

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

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

31✔
662
    if (typeof slideOrIndex === 'number') {
31✔
663
      index = slideOrIndex;
3✔
664
      slide = this.slides.at(index);
3✔
665
    } else {
31✔
666
      slide = slideOrIndex;
28✔
667
      index = this.slides.indexOf(slide);
28✔
668
    }
28✔
669

31✔
670
    if (index === this.current || index === -1 || !slide) {
31✔
671
      return false;
4✔
672
    }
4✔
673

27✔
674
    const dir = animationDirection ?? (index > this.current ? 'next' : 'prev');
31✔
675

31✔
676
    await this.animateSlides(slide, this._activeSlide, dir);
31✔
677
    return true;
25✔
678
  }
31✔
679

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

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

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

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

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

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

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

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

10✔
812
declare global {
10✔
813
  interface HTMLElementTagNameMap {
10✔
814
    'igc-carousel': IgcCarouselComponent;
10✔
815
  }
10✔
816
}
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