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

IgniteUI / igniteui-webcomponents / 16187746836

10 Jul 2025 06:24AM UTC coverage: 98.301% (+0.02%) from 98.286%
16187746836

Pull #1767

github

web-flow
Merge c6690344d into e298a6f8c
Pull Request #1767: feat: Slot controller

4944 of 5193 branches covered (95.21%)

Branch coverage included in aggregate %.

1233 of 1243 new or added lines in 23 files covered. (99.2%)

3 existing lines in 1 file now uncovered.

31977 of 32366 relevant lines covered (98.8%)

1728.93 hits per line

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

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

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

10✔
66
let nextId = 1;
10✔
67
const Slots = setSlots('indicator', 'previous-button', 'next-button');
10✔
68

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

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

10✔
109
  //#region Internal state
10✔
110

10✔
111
  private readonly _carouselId = `igc-carousel-${nextId++}`;
10✔
112
  private _paused = false;
10✔
113
  private _lastInterval!: ReturnType<typeof setInterval> | null;
10✔
114
  private _hasKeyboardInteractionOnIndicators = false;
10✔
115
  private _hasPointerInteraction = false;
10✔
116
  private _hasInnerFocus = false;
10✔
117

10✔
118
  private _slides: IgcCarouselSlideComponent[] = [];
10✔
119
  private _projectedIndicators: IgcCarouselIndicatorComponent[] = [];
10✔
120

10✔
121
  @state()
10✔
122
  private _activeSlide!: IgcCarouselSlideComponent;
10✔
123

10✔
124
  @state()
10✔
125
  private _playing = false;
10✔
126

10✔
127
  private readonly _slots = addSlotController(this, {
10✔
128
    slots: Slots,
10✔
129
    onChange: this._handleSlotChange,
10✔
130
    initial: true,
10✔
131
  });
10✔
132

10✔
133
  private readonly _context = new ContextProvider(this, {
10✔
134
    context: carouselContext,
10✔
135
    initialValue: this,
10✔
136
  });
10✔
137

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

10✔
141
  private readonly _carouselSlidesContainerRef = createRef<HTMLDivElement>();
10✔
142
  private readonly _indicatorsContainerRef = createRef<HTMLDivElement>();
10✔
143
  private readonly _prevButtonRef = createRef<IgcButtonComponent>();
10✔
144
  private readonly _nextButtonRef = createRef<IgcButtonComponent>();
10✔
145

10✔
146
  private get _hasProjectedIndicators(): boolean {
10✔
147
    return !isEmpty(this._projectedIndicators);
163✔
148
  }
163✔
149

10✔
150
  private get _showIndicatorsLabel(): boolean {
10✔
151
    return this.total > this.maximumIndicatorsCount;
308✔
152
  }
308✔
153

10✔
154
  private get _nextIndex(): number {
10✔
155
    return wrap(0, this.total - 1, this.current + 1);
19✔
156
  }
19✔
157

10✔
158
  private get _previousIndex(): number {
10✔
159
    return wrap(0, this.total - 1, this.current - 1);
11✔
160
  }
11✔
161

10✔
162
  //#endregion
10✔
163

10✔
164
  //#region Public attributes and properties
10✔
165

10✔
166
  /**
10✔
167
   * Whether the carousel should skip rotating to the first slide after it reaches the last.
10✔
168
   * @attr disable-loop
10✔
169
   */
10✔
170
  @property({ type: Boolean, reflect: true, attribute: 'disable-loop' })
10✔
171
  public disableLoop = false;
10✔
172

10✔
173
  /**
10✔
174
   * Whether the carousel should ignore use interactions and not pause on them.
10✔
175
   * @attr disable-pause-on-interaction
10✔
176
   */
10✔
177
  @property({
10✔
178
    type: Boolean,
10✔
179
    reflect: true,
10✔
180
    attribute: 'disable-pause-on-interaction',
10✔
181
  })
10✔
182
  public disablePauseOnInteraction = false;
10✔
183

10✔
184
  /**
10✔
185
   * Whether the carousel should skip rendering of the default navigation buttons.
10✔
186
   * @attr hide-navigation
10✔
187
   */
10✔
188
  @property({ type: Boolean, reflect: true, attribute: 'hide-navigation' })
10✔
189
  public hideNavigation = false;
10✔
190

10✔
191
  /**
10✔
192
   * Whether the carousel should render the indicator controls (dots).
10✔
193
   * @attr hide-indicators
10✔
194
   */
10✔
195
  @property({ type: Boolean, reflect: true, attribute: 'hide-indicators' })
10✔
196
  public hideIndicators = false;
10✔
197

10✔
198
  /**
10✔
199
   * Whether the carousel has vertical alignment.
10✔
200
   * @attr vertical
10✔
201
   */
10✔
202
  @property({ type: Boolean, reflect: true })
10✔
203
  public vertical = false;
10✔
204

10✔
205
  /**
10✔
206
   * Sets the orientation of the indicator controls (dots).
10✔
207
   * @attr indicators-orientation
10✔
208
   */
10✔
209
  @property({ attribute: 'indicators-orientation' })
10✔
210
  public indicatorsOrientation: CarouselIndicatorsOrientation = 'end';
10✔
211

10✔
212
  /**
10✔
213
   * The format used to set the aria-label on the carousel indicators.
10✔
214
   * Instances of '{0}' will be replaced with the index of the corresponding slide.
10✔
215
   *
10✔
216
   * @attr indicators-label-format
10✔
217
   */
10✔
218
  @property({ attribute: 'indicators-label-format' })
10✔
219
  public indicatorsLabelFormat = 'Slide {0}';
10✔
220

10✔
221
  /**
10✔
222
   * The format used to set the aria-label on the carousel slides and the text displayed
10✔
223
   * when the number of indicators is greater than tha maximum indicator count.
10✔
224
   * Instances of '{0}' will be replaced with the index of the corresponding slide.
10✔
225
   * Instances of '{1}' will be replaced with the total amount of slides.
10✔
226
   *
10✔
227
   * @attr slides-label-format
10✔
228
   */
10✔
229
  @property({ attribute: 'slides-label-format' })
10✔
230
  public slidesLabelFormat = '{0} of {1}';
10✔
231

10✔
232
  /**
10✔
233
   * The duration in milliseconds between changing the active slide.
10✔
234
   * @attr interval
10✔
235
   */
10✔
236
  @property({ type: Number })
10✔
237
  public interval: number | undefined;
10✔
238

10✔
239
  /**
10✔
240
   * Controls the maximum indicator controls (dots) that can be shown. Default value is `10`.
10✔
241
   * @attr maximum-indicators-count
10✔
242
   */
10✔
243
  @property({ type: Number, attribute: 'maximum-indicators-count' })
10✔
244
  public maximumIndicatorsCount = 10;
10✔
245

10✔
246
  /**
10✔
247
   * The animation type.
10✔
248
   * @attr animation-type
10✔
249
   */
10✔
250
  @property({ attribute: 'animation-type' })
10✔
251
  public animationType: HorizontalTransitionAnimation = 'slide';
10✔
252

10✔
253
  /* blazorSuppress */
10✔
254
  /**
10✔
255
   * The slides of the carousel.
10✔
256
   */
10✔
257
  public get slides(): IgcCarouselSlideComponent[] {
10✔
258
    return Array.from(this._slides);
565✔
259
  }
565✔
260

10✔
261
  /**
10✔
262
   * The total number of slides.
10✔
263
   */
10✔
264
  public get total(): number {
10✔
265
    return this._slides.length;
360✔
266
  }
360✔
267

10✔
268
  /**
10✔
269
   * The index of the current active slide.
10✔
270
   */
10✔
271
  public get current(): number {
10✔
272
    return Math.max(0, this._slides.indexOf(this._activeSlide));
205✔
273
  }
205✔
274

10✔
275
  /**
10✔
276
   * Whether the carousel is in playing state.
10✔
277
   */
10✔
278
  public get isPlaying(): boolean {
10✔
279
    return this._playing;
51✔
280
  }
51✔
281

10✔
282
  /**
10✔
283
   * Whether the carousel in in paused state.
10✔
284
   */
10✔
285
  public get isPaused(): boolean {
10✔
286
    return this._paused;
18✔
287
  }
18✔
288

10✔
289
  //#endregion
10✔
290

10✔
291
  //#region Watchers
10✔
292

10✔
293
  @watch('animationType')
10✔
294
  @watch('slidesLabelFormat')
10✔
295
  @watch('indicatorsLabelFormat')
10✔
296
  protected _contextChanged(): void {
10✔
297
    this._context.setValue(this, true);
142✔
298
  }
142✔
299

10✔
300
  @watch('interval')
10✔
301
  protected _intervalChange(): void {
10✔
302
    if (!this.isPlaying) {
6✔
303
      this._playing = true;
6✔
304
    }
6✔
305

6✔
306
    this._restartInterval();
6✔
307
  }
6✔
308

10✔
309
  //#endregion
10✔
310

10✔
311
  //#region Life-cycle hooks and observer callback
10✔
312

10✔
313
  constructor() {
10✔
314
    super();
49✔
315

49✔
316
    addInternalsController(this, {
49✔
317
      initialARIA: {
49✔
318
        role: 'region',
49✔
319
        ariaRoleDescription: 'carousel',
49✔
320
      },
49✔
321
    });
49✔
322

49✔
323
    addThemingController(this, all);
49✔
324

49✔
325
    addSafeEventListener(this, 'pointerenter', this._handlePointerInteraction);
49✔
326
    addSafeEventListener(this, 'pointerleave', this._handlePointerInteraction);
49✔
327
    addSafeEventListener(this, 'focusin', this._handleFocusInteraction);
49✔
328
    addSafeEventListener(this, 'focusout', this._handleFocusInteraction);
49✔
329

49✔
330
    addGesturesController(this, {
49✔
331
      ref: this._carouselSlidesContainerRef,
49✔
332
      touchOnly: true,
49✔
333
    })
49✔
334
      .set('swipe-left', this._handleHorizontalSwipe)
49✔
335
      .set('swipe-right', this._handleHorizontalSwipe)
49✔
336
      .set('swipe-up', this._handleVerticalSwipe)
49✔
337
      .set('swipe-down', this._handleVerticalSwipe);
49✔
338

49✔
339
    addKeybindings(this, {
49✔
340
      ref: this._indicatorsContainerRef,
49✔
341
      bindingDefaults: { preventDefault: true },
49✔
342
    })
49✔
343
      .set(arrowLeft, this._handleArrowLeft)
49✔
344
      .set(arrowRight, this._handleArrowRight)
49✔
345
      .set(homeKey, this._handleHomeKey)
49✔
346
      .set(endKey, this._handleEndKey);
49✔
347

49✔
348
    addKeybindings(this, {
49✔
349
      ref: this._prevButtonRef,
49✔
350
      bindingDefaults: { preventDefault: true },
49✔
351
    }).setActivateHandler(this._handleNavigationInteractionPrevious);
49✔
352

49✔
353
    addKeybindings(this, {
49✔
354
      ref: this._nextButtonRef,
49✔
355
      bindingDefaults: { preventDefault: true },
49✔
356
    }).setActivateHandler(this._handleNavigationInteractionNext);
49✔
357

49✔
358
    createMutationController(this, {
49✔
359
      callback: this._observerCallback,
49✔
360
      filter: [IgcCarouselSlideComponent.tagName],
49✔
361
      config: {
49✔
362
        attributeFilter: ['active'],
49✔
363
        childList: true,
49✔
364
        subtree: true,
49✔
365
      },
49✔
366
    });
49✔
367
  }
49✔
368

10✔
369
  protected override async firstUpdated(): Promise<void> {
10✔
370
    await this.updateComplete;
47✔
371

47✔
372
    if (!isEmpty(this._slides)) {
47✔
373
      this._activateSlide(
47✔
374
        this._slides.findLast((slide) => slide.active) ?? first(this._slides)
47✔
375
      );
47✔
376
    }
47✔
377
  }
47✔
378

10✔
379
  private _observerCallback({
10✔
380
    changes: { added, attributes },
85✔
381
  }: MutationControllerParams<IgcCarouselSlideComponent>) {
85✔
382
    const activeSlides = this._slides.filter((slide) => slide.active);
85✔
383

85✔
384
    if (activeSlides.length <= 1) {
85✔
385
      return;
82✔
386
    }
82✔
387
    const idx = this._slides.indexOf(
3✔
388
      added.length ? last(added).node : last(attributes).node
85!
389
    );
85✔
390

85✔
391
    for (const [i, slide] of this._slides.entries()) {
85✔
392
      if (slide.active && i !== idx) {
10✔
393
        slide.active = false;
3✔
394
      }
3✔
395
    }
10✔
396

3✔
397
    this._activateSlide(this._slides[idx]);
3✔
398
  }
85✔
399

10✔
400
  //#endregion
10✔
401

10✔
402
  //#region Event listeners
10✔
403

10✔
404
  private _handleSlotChange(
10✔
405
    params: SlotChangeCallbackParameters<InferSlotNames<typeof Slots>>
104✔
406
  ): void {
104✔
407
    if (params.isDefault || params.isInitial) {
104✔
408
      this._slides = this._slots.getAssignedElements('[default]', {
95✔
409
        selector: IgcCarouselSlideComponent.tagName,
95✔
410
      });
95✔
411
    }
95✔
412

104✔
413
    if (params.slot === 'indicator') {
104✔
414
      this._projectedIndicators = this._slots.getAssignedElements('indicator', {
3✔
415
        selector: IgcCarouselIndicatorComponent.tagName,
3✔
416
      });
3✔
417
    }
3✔
418
  }
104✔
419

10✔
420
  private _handlePointerInteraction(event: PointerEvent): void {
10✔
421
    this._hasPointerInteraction = event.type === 'pointerenter';
10✔
422

10✔
423
    if (!this._hasInnerFocus) {
10✔
424
      this._handlePauseOnInteraction();
7✔
425
    }
7✔
426
  }
10✔
427

10✔
428
  private _handleFocusInteraction(event: FocusEvent): void {
10✔
429
    // focusin - element that lost focus
8✔
430
    // focusout - element that gained focus
8✔
431
    const node = event.relatedTarget as Node;
8✔
432

8✔
433
    if (this.contains(node)) {
8!
UNCOV
434
      return;
×
435
    }
×
436

8✔
437
    this._hasInnerFocus = event.type === 'focusin';
8✔
438

8✔
439
    if (!this._hasPointerInteraction) {
8✔
440
      this._handlePauseOnInteraction();
5✔
441
    }
5✔
442
  }
8✔
443

10✔
444
  private async _handleIndicatorClick(event: PointerEvent): Promise<void> {
10✔
445
    const indicator = findElementFromEventPath<IgcCarouselIndicatorComponent>(
4✔
446
      IgcCarouselIndicatorComponent.tagName,
4✔
447
      event
4✔
448
    )!;
4✔
449

4✔
450
    const index = this._hasProjectedIndicators
4!
NEW
451
      ? this._projectedIndicators.indexOf(indicator)
×
452
      : Array.from(this._defaultIndicators).indexOf(indicator);
4✔
453

4✔
454
    this._handleInteraction(() =>
4✔
455
      this.select(this._slides[index], index > this.current ? 'next' : 'prev')
4✔
456
    );
4✔
457
  }
4✔
458

10✔
459
  //#endregion
10✔
460

10✔
461
  //#region Keyboard event listeners
10✔
462

10✔
463
  private async _handleArrowLeft(): Promise<void> {
10✔
464
    this._hasKeyboardInteractionOnIndicators = true;
2✔
465
    this._handleInteraction(isLTR(this) ? this.prev : this.next);
2✔
466
  }
2✔
467

10✔
468
  private async _handleArrowRight(): Promise<void> {
10✔
469
    this._hasKeyboardInteractionOnIndicators = true;
2✔
470
    this._handleInteraction(isLTR(this) ? this.next : this.prev);
2✔
471
  }
2✔
472

10✔
473
  private async _handleHomeKey(): Promise<void> {
10✔
474
    this._hasKeyboardInteractionOnIndicators = true;
2✔
475
    this._handleInteraction(() =>
2✔
476
      this.select(isLTR(this) ? first(this._slides) : last(this._slides))
2✔
477
    );
2✔
478
  }
2✔
479

10✔
480
  private async _handleEndKey(): Promise<void> {
10✔
481
    this._hasKeyboardInteractionOnIndicators = true;
2✔
482
    this._handleInteraction(() =>
2✔
483
      this.select(isLTR(this) ? last(this._slides) : first(this._slides))
2✔
484
    );
2✔
485
  }
2✔
486

10✔
487
  //#endregion
10✔
488

10✔
489
  //#region Gestures event listeners
10✔
490

10✔
491
  private _handleVerticalSwipe({ data: { direction } }: SwipeEvent): void {
10✔
492
    if (this.vertical) {
7✔
493
      this._handleInteraction(direction === 'up' ? this.next : this.prev);
5✔
494
    }
5✔
495
  }
7✔
496

10✔
497
  private _handleHorizontalSwipe({ data: { direction } }: SwipeEvent): void {
10✔
498
    if (!this.vertical) {
9✔
499
      const callback = () => {
7✔
500
        if (isLTR(this)) {
7✔
501
          return direction === 'left' ? this.next : this.prev;
5✔
502
        }
5✔
503
        return direction === 'left' ? this.prev : this.next;
7✔
504
      };
7✔
505

7✔
506
      this._handleInteraction(callback());
7✔
507
    }
7✔
508
  }
9✔
509

10✔
510
  //#endregion
10✔
511

10✔
512
  //#region Internal API
10✔
513

10✔
514
  private _handleNavigationInteractionNext(): void {
10✔
515
    this._handleInteraction(this.next);
3✔
516
  }
3✔
517

10✔
518
  private _handleNavigationInteractionPrevious(): void {
10✔
519
    this._handleInteraction(this.prev);
3✔
520
  }
3✔
521

10✔
522
  private async _handleInteraction(
10✔
523
    callback: () => Promise<boolean>
30✔
524
  ): Promise<void> {
30✔
525
    if (this.interval) {
30!
NEW
UNCOV
526
      this._resetInterval();
×
527
    }
×
528

30✔
529
    if (await callback.call(this)) {
30✔
530
      this.emitEvent('igcSlideChanged', { detail: this.current });
25✔
531
    }
25✔
532

30✔
533
    if (this.interval) {
30!
NEW
UNCOV
534
      this._restartInterval();
×
535
    }
×
536
  }
30✔
537

10✔
538
  private _handlePauseOnInteraction(): void {
10✔
539
    if (!this.interval || this.disablePauseOnInteraction) return;
12✔
540

6✔
541
    if (this.isPlaying) {
12✔
542
      this.pause();
3✔
543
      this.emitEvent('igcPaused');
3✔
544
    } else {
3✔
545
      this.play();
3✔
546
      this.emitEvent('igcPlaying');
3✔
547
    }
3✔
548
  }
12✔
549

10✔
550
  private _activateSlide(slide: IgcCarouselSlideComponent): void {
10✔
551
    if (this._activeSlide) {
81✔
552
      this._activeSlide.active = false;
34✔
553
    }
34✔
554

81✔
555
    this._activeSlide = slide;
81✔
556
    this._activeSlide.active = true;
81✔
557

81✔
558
    if (this._hasKeyboardInteractionOnIndicators) {
81✔
559
      this._hasProjectedIndicators
8!
560
        ? this._projectedIndicators[this.current].focus()
×
561
        : this._defaultIndicators[this.current].focus();
8✔
562

8✔
563
      this._hasKeyboardInteractionOnIndicators = false;
8✔
564
    }
8✔
565
  }
81✔
566

10✔
567
  private _updateProjectedIndicators(): void {
10✔
568
    for (const [idx, slide] of this._slides.entries()) {
3✔
569
      const indicator = this._projectedIndicators[idx];
3✔
570
      indicator.active = slide.active;
3✔
571
      indicator.index = idx;
3✔
572
    }
3✔
573

3✔
574
    if (this._activeSlide) {
3✔
575
      this.setAttribute('aria-controls', this._activeSlide.id);
3✔
576
    }
3✔
577
  }
3✔
578

10✔
579
  private _resetInterval(): void {
10✔
580
    if (this._lastInterval) {
15✔
581
      clearInterval(this._lastInterval);
4✔
582
      this._lastInterval = null;
4✔
583
    }
4✔
584
  }
15✔
585

10✔
586
  private _restartInterval(): void {
10✔
587
    this._resetInterval();
10✔
588

10✔
589
    if (asNumber(this.interval) > 0) {
10✔
590
      this._lastInterval = setInterval(() => {
9✔
591
        if (
8✔
592
          this.isPlaying &&
8✔
593
          this.total &&
6✔
594
          !(this.disableLoop && this._nextIndex === 0)
6✔
595
        ) {
8✔
596
          this.select(this.slides[this._nextIndex], 'next');
5✔
597
          this.emitEvent('igcSlideChanged', { detail: this.current });
5✔
598
        } else {
8✔
599
          this.pause();
3✔
600
        }
3✔
601
      }, this.interval);
9✔
602
    }
9✔
603
  }
10✔
604

10✔
605
  private async _animateSlides(
10✔
606
    nextSlide: IgcCarouselSlideComponent,
31✔
607
    currentSlide: IgcCarouselSlideComponent,
31✔
608
    dir: 'next' | 'prev'
31✔
609
  ): Promise<void> {
31✔
610
    if (dir === 'next') {
31✔
611
      // Animate slides in next direction
18✔
612
      currentSlide.previous = true;
18✔
613
      currentSlide.toggleAnimation('out');
18✔
614
      this._activateSlide(nextSlide);
18✔
615
      await nextSlide.toggleAnimation('in');
18✔
616
      currentSlide.previous = false;
18✔
617
    } else {
31✔
618
      // Animate slides in previous direction
13✔
619
      currentSlide.previous = true;
13✔
620
      currentSlide.toggleAnimation('in', 'reverse');
13✔
621
      this._activateSlide(nextSlide);
13✔
622
      await nextSlide.toggleAnimation('out', 'reverse');
13✔
623
      currentSlide.previous = false;
13✔
624
    }
13✔
625
  }
31✔
626

10✔
627
  //#endregion
10✔
628

10✔
629
  //#region Public API
10✔
630

10✔
631
  /**
10✔
632
   * Resumes playing of the carousel slides.
10✔
633
   */
10✔
634
  public play(): void {
10✔
635
    if (!this.isPlaying) {
4✔
636
      this._paused = false;
4✔
637
      this._playing = true;
4✔
638
      this._restartInterval();
4✔
639
    }
4✔
640
  }
4✔
641

10✔
642
  /**
10✔
643
   * Pauses the carousel rotation of slides.
10✔
644
   */
10✔
645
  public pause(): void {
10✔
646
    if (this.isPlaying) {
9✔
647
      this._playing = false;
5✔
648
      this._paused = true;
5✔
649
      this._resetInterval();
5✔
650
    }
5✔
651
  }
9✔
652

10✔
653
  /**
10✔
654
   * Switches to the next slide, runs any animations, and returns if the operation was successful.
10✔
655
   */
10✔
656
  public async next(): Promise<boolean> {
10✔
657
    if (this.disableLoop && this._nextIndex === 0) {
10✔
658
      this.pause();
1✔
659
      return false;
1✔
660
    }
1✔
661

9✔
662
    return await this.select(this._slides[this._nextIndex], 'next');
9✔
663
  }
10✔
664

10✔
665
  /**
10✔
666
   * Switches to the previous slide, runs any animations, and returns if the operation was successful.
10✔
667
   */
10✔
668
  public async prev(): Promise<boolean> {
10✔
669
    if (this.disableLoop && this._previousIndex === this.total - 1) {
10✔
670
      this.pause();
1✔
671
      return false;
1✔
672
    }
1✔
673

9✔
674
    return await this.select(this._slides[this._previousIndex], 'prev');
9✔
675
  }
10✔
676

10✔
677
  /* blazorSuppress */
10✔
678
  /**
10✔
679
   * Switches to the passed-in slide, runs any animations, and returns if the operation was successful.
10✔
680
   */
10✔
681
  public async select(
10✔
682
    slide: IgcCarouselSlideComponent,
10✔
683
    animationDirection?: 'next' | 'prev'
10✔
684
  ): Promise<boolean>;
10✔
685
  /**
10✔
686
   * Switches to slide by index, runs any animations, and returns if the operation was successful.
10✔
687
   */
10✔
688
  public async select(
10✔
689
    index: number,
10✔
690
    animationDirection?: 'next' | 'prev'
10✔
691
  ): Promise<boolean>;
10✔
692
  public async select(
10✔
693
    slideOrIndex: IgcCarouselSlideComponent | number,
35✔
694
    animationDirection?: 'next' | 'prev'
35✔
695
  ): Promise<boolean> {
35✔
696
    let index: number;
35✔
697
    let slide: IgcCarouselSlideComponent | undefined;
35✔
698

35✔
699
    if (typeof slideOrIndex === 'number') {
35✔
700
      index = slideOrIndex;
3✔
701
      slide = this._slides.at(index);
3✔
702
    } else {
35✔
703
      slide = slideOrIndex;
32✔
704
      index = this._slides.indexOf(slide);
32✔
705
    }
32✔
706

35✔
707
    if (index === this.current || index === -1 || !slide) {
35✔
708
      return false;
4✔
709
    }
4✔
710

31✔
711
    const dir = animationDirection ?? (index > this.current ? 'next' : 'prev');
35✔
712

35✔
713
    await this._animateSlides(slide, this._activeSlide, dir);
35✔
714
    return true;
31✔
715
  }
35✔
716

10✔
717
  //#endregion
10✔
718

10✔
719
  //#region Template renderers
10✔
720

10✔
721
  private _renderNavigation() {
10✔
722
    return html`
155✔
723
      <igc-button
155✔
724
        ${ref(this._prevButtonRef)}
155✔
725
        type="button"
155✔
726
        part="navigation previous"
155✔
727
        aria-label="Previous slide"
155✔
728
        aria-controls=${this._carouselId}
155✔
729
        ?disabled=${this.disableLoop && this.current === 0}
155✔
730
        @click=${this._handleNavigationInteractionPrevious}
155✔
731
      >
155✔
732
        <slot name="previous-button">
155✔
733
          <igc-icon
155✔
734
            name="carousel_prev"
155✔
735
            collection="default"
155✔
736
            aria-hidden="true"
155✔
737
          ></igc-icon>
155✔
738
        </slot>
155✔
739
      </igc-button>
155✔
740

155✔
741
      <igc-button
155✔
742
        ${ref(this._nextButtonRef)}
155✔
743
        type="button"
155✔
744
        part="navigation next"
155✔
745
        aria-label="Next slide"
155✔
746
        aria-controls=${this._carouselId}
155✔
747
        ?disabled=${this.disableLoop && this.current === this.total - 1}
155✔
748
        @click=${this._handleNavigationInteractionNext}
155✔
749
      >
155✔
750
        <slot name="next-button">
155✔
751
          <igc-icon
155✔
752
            name="carousel_next"
155✔
753
            collection="default"
155✔
754
            aria-hidden="true"
155✔
755
          ></igc-icon>
155✔
756
        </slot>
155✔
757
      </igc-button>
155✔
758
    `;
155✔
759
  }
155✔
760

10✔
761
  protected *_renderIndicators() {
10✔
762
    for (const [i, slide] of this._slides.entries()) {
148✔
763
      const forward = slide.active ? 'visible' : 'hidden';
298✔
764
      const backward = slide.active ? 'hidden' : 'visible';
298✔
765

298✔
766
      yield html`
298✔
767
        <igc-carousel-indicator
298✔
768
          exportparts="indicator, active, inactive"
298✔
769
          .active=${slide.active}
298✔
770
          .index=${i}
298✔
771
        >
298✔
772
          <div
298✔
773
            part="dot"
298✔
774
            style=${styleMap({ visibility: backward, zIndex: 1 })}
298✔
775
          ></div>
298✔
776
          <div
298✔
777
            part="dot active"
298✔
778
            slot="active"
298✔
779
            style=${styleMap({ visibility: forward })}
298✔
780
          ></div>
298✔
781
        </igc-carousel-indicator>
298✔
782
      `;
298✔
783
    }
298✔
784
  }
148✔
785

10✔
786
  private _renderIndicatorContainer() {
10✔
787
    const parts = {
151✔
788
      indicators: true,
151✔
789
      start: this.indicatorsOrientation === 'start',
151✔
790
    };
151✔
791

151✔
792
    return html`
151✔
793
      <igc-carousel-indicator-container>
151✔
794
        <div
151✔
795
          ${ref(this._indicatorsContainerRef)}
151✔
796
          role="tablist"
151✔
797
          part=${partMap(parts)}
151✔
798
        >
151✔
799
          <slot name="indicator" @click=${this._handleIndicatorClick}>
151✔
800
            ${cache(
151✔
801
              this._hasProjectedIndicators
151✔
802
                ? this._updateProjectedIndicators()
3✔
803
                : this._renderIndicators()
148✔
804
            )}
151✔
805
          </slot>
151✔
806
        </div>
151✔
807
      </igc-carousel-indicator-container>
151✔
808
    `;
151✔
809
  }
151✔
810

10✔
811
  private _renderLabel() {
10✔
812
    const parts = {
3✔
813
      label: true,
3✔
814
      indicators: true,
3✔
815
      start: this.indicatorsOrientation === 'start',
3✔
816
    };
3✔
817
    const value = formatString(
3✔
818
      this.slidesLabelFormat,
3✔
819
      this.current + 1,
3✔
820
      this.total
3✔
821
    );
3✔
822

3✔
823
    return html`
3✔
824
      <div part=${partMap(parts)}>
3✔
825
        <span>${value}</span>
3✔
826
      </div>
3✔
827
    `;
3✔
828
  }
3✔
829

10✔
830
  protected override render() {
10✔
831
    const hasNoIndicators = this.hideIndicators || this._showIndicatorsLabel;
156✔
832
    const hasLabel = !this.hideIndicators && this._showIndicatorsLabel;
156✔
833

156✔
834
    return html`
156✔
835
      <section>
156✔
836
        ${cache(this.hideNavigation ? nothing : this._renderNavigation())}
156✔
837
        ${hasNoIndicators ? nothing : this._renderIndicatorContainer()}
156✔
838
        ${hasLabel ? this._renderLabel() : nothing}
156✔
839
        <div
156✔
840
          ${ref(this._carouselSlidesContainerRef)}
156✔
841
          id=${this._carouselId}
156✔
842
          aria-live=${this.interval && this._playing ? 'off' : 'polite'}
156✔
843
        >
156✔
844
          <slot></slot>
156✔
845
        </div>
156✔
846
      </section>
156✔
847
    `;
156✔
848
  }
156✔
849
}
10✔
850

10✔
851
declare global {
10✔
852
  interface HTMLElementTagNameMap {
10✔
853
    'igc-carousel': IgcCarouselComponent;
10✔
854
  }
10✔
855
}
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