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

IgniteUI / igniteui-webcomponents / 18190547640

02 Oct 2025 10:35AM UTC coverage: 98.119% (-0.06%) from 98.178%
18190547640

Pull #1835

github

web-flow
Merge 3ff39a907 into a9ac4512d
Pull Request #1835: feat(localization): Integration of new i18nManager resource strings.

5316 of 5598 branches covered (94.96%)

Branch coverage included in aggregate %.

602 of 629 new or added lines in 23 files covered. (95.71%)

1 existing line in 1 file now uncovered.

35112 of 35605 relevant lines covered (98.62%)

1641.16 hits per line

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

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

10✔
65
export interface IgcCarouselComponentEventMap {
10✔
66
  igcSlideChanged: CustomEvent<number>;
10✔
67
  igcPlaying: CustomEvent<void>;
10✔
68
  igcPaused: CustomEvent<void>;
10✔
69
}
10✔
70

10✔
71
let nextId = 1;
10✔
72
const Slots = setSlots('indicator', 'previous-button', 'next-button');
10✔
73

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

10✔
102
  /* blazorSuppress */
10✔
103
  public static register(): void {
10✔
104
    registerComponent(
1✔
105
      IgcCarouselComponent,
1✔
106
      IgcCarouselIndicatorComponent,
1✔
107
      IgcCarouselIndicatorContainerComponent,
1✔
108
      IgcCarouselSlideComponent,
1✔
109
      IgcIconComponent,
1✔
110
      IgcButtonComponent
1✔
111
    );
1✔
112
  }
1✔
113

10✔
114
  //#region Internal state
10✔
115

10✔
116
  private readonly _carouselId = `igc-carousel-${nextId++}`;
10✔
117
  private _paused = false;
10✔
118
  private _lastInterval!: ReturnType<typeof setInterval> | null;
10✔
119
  private _hasKeyboardInteractionOnIndicators = false;
10✔
120
  private _hasPointerInteraction = false;
10✔
121
  private _hasInnerFocus = false;
10✔
122

10✔
123
  private _slides: IgcCarouselSlideComponent[] = [];
10✔
124
  private _projectedIndicators: IgcCarouselIndicatorComponent[] = [];
10✔
125

10✔
126
  @state()
10✔
127
  private _activeSlide!: IgcCarouselSlideComponent;
10✔
128

10✔
129
  @state()
10✔
130
  private _playing = false;
10✔
131

10✔
132
  private readonly _slots = addSlotController(this, {
10✔
133
    slots: Slots,
10✔
134
    onChange: this._handleSlotChange,
10✔
135
    initial: true,
10✔
136
  });
10✔
137

10✔
138
  private readonly _i18nController =
10✔
139
    addI18nController<ICarouselResourceStrings>(this, {
10✔
140
      defaultEN: CarouselResourceStringsEN,
10✔
141
    });
10✔
142

10✔
143
  private readonly _context = new ContextProvider(this, {
10✔
144
    context: carouselContext,
10✔
145
    initialValue: this,
10✔
146
  });
10✔
147

10✔
148
  @queryAll(IgcCarouselIndicatorComponent.tagName)
10✔
149
  private readonly _defaultIndicators!: NodeListOf<IgcCarouselIndicatorComponent>;
10✔
150

10✔
151
  private readonly _carouselSlidesContainerRef = createRef<HTMLDivElement>();
10✔
152
  private readonly _indicatorsContainerRef = createRef<HTMLDivElement>();
10✔
153
  private readonly _prevButtonRef = createRef<IgcButtonComponent>();
10✔
154
  private readonly _nextButtonRef = createRef<IgcButtonComponent>();
10✔
155

10✔
156
  private get _hasProjectedIndicators(): boolean {
10✔
157
    return !isEmpty(this._projectedIndicators);
163✔
158
  }
163✔
159

10✔
160
  private get _showIndicatorsLabel(): boolean {
10✔
161
    return this.total > this.maximumIndicatorsCount;
308✔
162
  }
308✔
163

10✔
164
  private get _nextIndex(): number {
10✔
165
    return wrap(0, this.total - 1, this.current + 1);
19✔
166
  }
19✔
167

10✔
168
  private get _previousIndex(): number {
10✔
169
    return wrap(0, this.total - 1, this.current - 1);
11✔
170
  }
11✔
171

10✔
172
  //#endregion
10✔
173

10✔
174
  //#region Public attributes and properties
10✔
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({ 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 set indicatorsLabelFormat(value: string) {
10✔
NEW
230
    this._indicatorsLabelFormat = value;
×
NEW
231
  }
×
232

10✔
233
  public get indicatorsLabelFormat() {
10✔
234
    return (
296✔
235
      this._indicatorsLabelFormat ??
296✔
236
      `${this.resourceStrings.carousel_slide} {0}`
296✔
237
    );
296✔
238
  }
296✔
239
  private _indicatorsLabelFormat: string | undefined;
10✔
240

10✔
241
  /**
10✔
242
   * The format used to set the aria-label on the carousel slides and the text displayed
10✔
243
   * when the number of indicators is greater than tha maximum indicator count.
10✔
244
   * Instances of '{0}' will be replaced with the index of the corresponding slide.
10✔
245
   * Instances of '{1}' will be replaced with the total amount of slides.
10✔
246
   *
10✔
247
   * @attr slides-label-format
10✔
248
   */
10✔
249
  @property({ attribute: 'slides-label-format' })
10✔
250
  public set slidesLabelFormat(value: string) {
10✔
251
    this._slidesLabelFormat = value;
1✔
252
  }
1✔
253

10✔
254
  public get slidesLabelFormat() {
10✔
255
    return (
380✔
256
      this._slidesLabelFormat ?? `{0} ${this.resourceStrings.carousel_of} {1}`
380✔
257
    );
380✔
258
  }
380✔
259

10✔
260
  private _slidesLabelFormat: string | undefined;
10✔
261

10✔
262
  /**
10✔
263
   * The duration in milliseconds between changing the active slide.
10✔
264
   * @attr interval
10✔
265
   */
10✔
266
  @property({ type: Number })
10✔
267
  public interval: number | undefined;
10✔
268

10✔
269
  /**
10✔
270
   * Controls the maximum indicator controls (dots) that can be shown. Default value is `10`.
10✔
271
   * @attr maximum-indicators-count
10✔
272
   */
10✔
273
  @property({ type: Number, attribute: 'maximum-indicators-count' })
10✔
274
  public maximumIndicatorsCount = 10;
10✔
275

10✔
276
  /**
10✔
277
   * The animation type.
10✔
278
   * @attr animation-type
10✔
279
   */
10✔
280
  @property({ attribute: 'animation-type' })
10✔
281
  public animationType: HorizontalTransitionAnimation = 'slide';
10✔
282

10✔
283
  /**
10✔
284
   * Gets/Sets the locale used for formatting and displaying the dates in the component.
10✔
285
   * @attr locale
10✔
286
   */
10✔
287
  @property()
10✔
288
  public set locale(value: string) {
10✔
NEW
289
    this._i18nController.locale = value;
×
NEW
290
  }
×
291

10✔
292
  public get locale() {
10✔
293
    return this._i18nController.locale;
47✔
294
  }
47✔
295

10✔
296
  /**
10✔
297
   * The resource strings for localization.
10✔
298
   */
10✔
299
  @property({ attribute: false })
10✔
300
  public set resourceStrings(value: ICarouselResourceStrings) {
10✔
NEW
301
    this._i18nController.resourceStrings = value;
×
NEW
302
  }
×
303

10✔
304
  public get resourceStrings(): ICarouselResourceStrings {
10✔
305
    return this._i18nController.resourceStrings;
1,027✔
306
  }
1,027✔
307

10✔
308
  /* blazorSuppress */
10✔
309
  /**
10✔
310
   * The slides of the carousel.
10✔
311
   */
10✔
312
  public get slides(): IgcCarouselSlideComponent[] {
10✔
313
    return Array.from(this._slides);
565✔
314
  }
565✔
315

10✔
316
  /**
10✔
317
   * The total number of slides.
10✔
318
   */
10✔
319
  public get total(): number {
10✔
320
    return this._slides.length;
360✔
321
  }
360✔
322

10✔
323
  /**
10✔
324
   * The index of the current active slide.
10✔
325
   */
10✔
326
  public get current(): number {
10✔
327
    return Math.max(0, this._slides.indexOf(this._activeSlide));
205✔
328
  }
205✔
329

10✔
330
  /**
10✔
331
   * Whether the carousel is in playing state.
10✔
332
   */
10✔
333
  public get isPlaying(): boolean {
10✔
334
    return this._playing;
51✔
335
  }
51✔
336

10✔
337
  /**
10✔
338
   * Whether the carousel in in paused state.
10✔
339
   */
10✔
340
  public get isPaused(): boolean {
10✔
341
    return this._paused;
18✔
342
  }
18✔
343

10✔
344
  //#endregion
10✔
345

10✔
346
  //#region Watchers
10✔
347

10✔
348
  @watch('animationType')
10✔
349
  @watch('slidesLabelFormat')
10✔
350
  @watch('indicatorsLabelFormat')
10✔
351
  protected _contextChanged(): void {
10✔
352
    this._context.setValue(this, true);
142✔
353
  }
142✔
354

10✔
355
  @watch('interval')
10✔
356
  protected _intervalChange(): void {
10✔
357
    if (!this.isPlaying) {
6✔
358
      this._playing = true;
6✔
359
    }
6✔
360

6✔
361
    this._restartInterval();
6✔
362
  }
6✔
363

10✔
364
  //#endregion
10✔
365

10✔
366
  //#region Life-cycle hooks and observer callback
10✔
367

10✔
368
  constructor() {
10✔
369
    super();
49✔
370

49✔
371
    addInternalsController(this, {
49✔
372
      initialARIA: {
49✔
373
        role: 'region',
49✔
374
        ariaRoleDescription: 'carousel',
49✔
375
      },
49✔
376
    });
49✔
377

49✔
378
    addThemingController(this, all);
49✔
379

49✔
380
    addSafeEventListener(this, 'pointerenter', this._handlePointerInteraction);
49✔
381
    addSafeEventListener(this, 'pointerleave', this._handlePointerInteraction);
49✔
382
    addSafeEventListener(this, 'focusin', this._handleFocusInteraction);
49✔
383
    addSafeEventListener(this, 'focusout', this._handleFocusInteraction);
49✔
384

49✔
385
    addGesturesController(this, {
49✔
386
      ref: this._carouselSlidesContainerRef,
49✔
387
      touchOnly: true,
49✔
388
    })
49✔
389
      .set('swipe-left', this._handleHorizontalSwipe)
49✔
390
      .set('swipe-right', this._handleHorizontalSwipe)
49✔
391
      .set('swipe-up', this._handleVerticalSwipe)
49✔
392
      .set('swipe-down', this._handleVerticalSwipe);
49✔
393

49✔
394
    addKeybindings(this, {
49✔
395
      ref: this._indicatorsContainerRef,
49✔
396
    })
49✔
397
      .set(arrowLeft, this._handleArrowLeft)
49✔
398
      .set(arrowRight, this._handleArrowRight)
49✔
399
      .set(homeKey, this._handleHomeKey)
49✔
400
      .set(endKey, this._handleEndKey);
49✔
401

49✔
402
    addKeybindings(this, {
49✔
403
      ref: this._prevButtonRef,
49✔
404
    }).setActivateHandler(this._handleNavigationInteractionPrevious);
49✔
405

49✔
406
    addKeybindings(this, {
49✔
407
      ref: this._nextButtonRef,
49✔
408
    }).setActivateHandler(this._handleNavigationInteractionNext);
49✔
409

49✔
410
    createMutationController(this, {
49✔
411
      callback: this._observerCallback,
49✔
412
      filter: [IgcCarouselSlideComponent.tagName],
49✔
413
      config: {
49✔
414
        attributeFilter: ['active'],
49✔
415
        childList: true,
49✔
416
        subtree: true,
49✔
417
      },
49✔
418
    });
49✔
419
  }
49✔
420

10✔
421
  protected override async firstUpdated(): Promise<void> {
10✔
422
    await this.updateComplete;
47✔
423

47✔
424
    if (!isEmpty(this._slides)) {
47✔
425
      this._activateSlide(
47✔
426
        this._slides.findLast((slide) => slide.active) ?? first(this._slides)
47✔
427
      );
47✔
428
    }
47✔
429
  }
47✔
430

10✔
431
  private _observerCallback({
10✔
432
    changes: { added, attributes },
85✔
433
  }: MutationControllerParams<IgcCarouselSlideComponent>) {
85✔
434
    const activeSlides = this._slides.filter((slide) => slide.active);
85✔
435

85✔
436
    if (activeSlides.length <= 1) {
85✔
437
      return;
82✔
438
    }
82✔
439

3✔
440
    const idx = this._slides.indexOf(
3✔
441
      added.length ? last(added).node : last(attributes).node
85!
442
    );
85✔
443

85✔
444
    for (const [i, slide] of this._slides.entries()) {
85✔
445
      if (slide.active && i !== idx) {
10✔
446
        slide.active = false;
3✔
447
      }
3✔
448
    }
10✔
449

3✔
450
    this._activateSlide(this._slides[idx]);
3✔
451
  }
85✔
452

10✔
453
  //#endregion
10✔
454

10✔
455
  //#region Event listeners
10✔
456

10✔
457
  private _handleSlotChange(
10✔
458
    params: SlotChangeCallbackParameters<InferSlotNames<typeof Slots>>
104✔
459
  ): void {
104✔
460
    if (params.isDefault || params.isInitial) {
104✔
461
      this._slides = this._slots.getAssignedElements('[default]', {
95✔
462
        selector: IgcCarouselSlideComponent.tagName,
95✔
463
      });
95✔
464
    }
95✔
465

104✔
466
    if (params.slot === 'indicator') {
104✔
467
      this._projectedIndicators = this._slots.getAssignedElements('indicator', {
3✔
468
        selector: IgcCarouselIndicatorComponent.tagName,
3✔
469
      });
3✔
470
    }
3✔
471
  }
104✔
472

10✔
473
  private _handlePointerInteraction(event: PointerEvent): void {
10✔
474
    this._hasPointerInteraction = event.type === 'pointerenter';
10✔
475

10✔
476
    if (!this._hasInnerFocus) {
10✔
477
      this._handlePauseOnInteraction();
7✔
478
    }
7✔
479
  }
10✔
480

10✔
481
  private _handleFocusInteraction(event: FocusEvent): void {
10✔
482
    // focusin - element that lost focus
8✔
483
    // focusout - element that gained focus
8✔
484
    const node = event.relatedTarget as Node;
8✔
485

8✔
486
    if (this.contains(node)) {
8!
487
      return;
×
488
    }
×
489

8✔
490
    this._hasInnerFocus = event.type === 'focusin';
8✔
491

8✔
492
    if (!this._hasPointerInteraction) {
8✔
493
      this._handlePauseOnInteraction();
5✔
494
    }
5✔
495
  }
8✔
496

10✔
497
  private async _handleIndicatorClick(event: PointerEvent): Promise<void> {
10✔
498
    const indicator = findElementFromEventPath<IgcCarouselIndicatorComponent>(
4✔
499
      IgcCarouselIndicatorComponent.tagName,
4✔
500
      event
4✔
501
    )!;
4✔
502

4✔
503
    const index = this._hasProjectedIndicators
4!
504
      ? this._projectedIndicators.indexOf(indicator)
×
505
      : Array.from(this._defaultIndicators).indexOf(indicator);
4✔
506

4✔
507
    this._handleInteraction(() =>
4✔
508
      this.select(this._slides[index], index > this.current ? 'next' : 'prev')
4✔
509
    );
4✔
510
  }
4✔
511

10✔
512
  //#endregion
10✔
513

10✔
514
  //#region Keyboard event listeners
10✔
515

10✔
516
  private async _handleArrowLeft(): Promise<void> {
10✔
517
    this._hasKeyboardInteractionOnIndicators = true;
2✔
518
    this._handleInteraction(isLTR(this) ? this.prev : this.next);
2✔
519
  }
2✔
520

10✔
521
  private async _handleArrowRight(): Promise<void> {
10✔
522
    this._hasKeyboardInteractionOnIndicators = true;
2✔
523
    this._handleInteraction(isLTR(this) ? this.next : this.prev);
2✔
524
  }
2✔
525

10✔
526
  private async _handleHomeKey(): Promise<void> {
10✔
527
    this._hasKeyboardInteractionOnIndicators = true;
2✔
528
    this._handleInteraction(() =>
2✔
529
      this.select(isLTR(this) ? first(this._slides) : last(this._slides))
2✔
530
    );
2✔
531
  }
2✔
532

10✔
533
  private async _handleEndKey(): Promise<void> {
10✔
534
    this._hasKeyboardInteractionOnIndicators = true;
2✔
535
    this._handleInteraction(() =>
2✔
536
      this.select(isLTR(this) ? last(this._slides) : first(this._slides))
2✔
537
    );
2✔
538
  }
2✔
539

10✔
540
  //#endregion
10✔
541

10✔
542
  //#region Gestures event listeners
10✔
543

10✔
544
  private _handleVerticalSwipe({ data: { direction } }: SwipeEvent): void {
10✔
545
    if (this.vertical) {
7✔
546
      this._handleInteraction(direction === 'up' ? this.next : this.prev);
5✔
547
    }
5✔
548
  }
7✔
549

10✔
550
  private _handleHorizontalSwipe({ data: { direction } }: SwipeEvent): void {
10✔
551
    if (!this.vertical) {
9✔
552
      const callback = () => {
7✔
553
        if (isLTR(this)) {
7✔
554
          return direction === 'left' ? this.next : this.prev;
5✔
555
        }
5✔
556
        return direction === 'left' ? this.prev : this.next;
7✔
557
      };
7✔
558

7✔
559
      this._handleInteraction(callback());
7✔
560
    }
7✔
561
  }
9✔
562

10✔
563
  //#endregion
10✔
564

10✔
565
  //#region Internal API
10✔
566

10✔
567
  private _handleNavigationInteractionNext(): void {
10✔
568
    this._handleInteraction(this.next);
3✔
569
  }
3✔
570

10✔
571
  private _handleNavigationInteractionPrevious(): void {
10✔
572
    this._handleInteraction(this.prev);
3✔
573
  }
3✔
574

10✔
575
  private async _handleInteraction(
10✔
576
    callback: () => Promise<boolean>
30✔
577
  ): Promise<void> {
30✔
578
    if (this.interval) {
30!
579
      this._resetInterval();
×
580
    }
×
581

30✔
582
    if (await callback.call(this)) {
30✔
583
      this.emitEvent('igcSlideChanged', { detail: this.current });
25✔
584
    }
25✔
585

30✔
586
    if (this.interval) {
30!
587
      this._restartInterval();
×
588
    }
×
589
  }
30✔
590

10✔
591
  private _handlePauseOnInteraction(): void {
10✔
592
    if (!this.interval || this.disablePauseOnInteraction) return;
12✔
593

6✔
594
    if (this.isPlaying) {
12✔
595
      this.pause();
3✔
596
      this.emitEvent('igcPaused');
3✔
597
    } else {
3✔
598
      this.play();
3✔
599
      this.emitEvent('igcPlaying');
3✔
600
    }
3✔
601
  }
12✔
602

10✔
603
  private _activateSlide(slide: IgcCarouselSlideComponent): void {
10✔
604
    if (this._activeSlide) {
81✔
605
      this._activeSlide.active = false;
34✔
606
    }
34✔
607

81✔
608
    this._activeSlide = slide;
81✔
609
    this._activeSlide.active = true;
81✔
610

81✔
611
    if (this._hasKeyboardInteractionOnIndicators) {
81✔
612
      this._hasProjectedIndicators
8!
613
        ? this._projectedIndicators[this.current].focus()
×
614
        : this._defaultIndicators[this.current].focus();
8✔
615

8✔
616
      this._hasKeyboardInteractionOnIndicators = false;
8✔
617
    }
8✔
618
  }
81✔
619

10✔
620
  private _updateProjectedIndicators(): void {
10✔
621
    for (const [idx, slide] of this._slides.entries()) {
3✔
622
      const indicator = this._projectedIndicators[idx];
3✔
623
      indicator.active = slide.active;
3✔
624
      indicator.index = idx;
3✔
625
    }
3✔
626

3✔
627
    if (this._activeSlide) {
3✔
628
      this.setAttribute('aria-controls', this._activeSlide.id);
3✔
629
    }
3✔
630
  }
3✔
631

10✔
632
  private _resetInterval(): void {
10✔
633
    if (this._lastInterval) {
15✔
634
      clearInterval(this._lastInterval);
4✔
635
      this._lastInterval = null;
4✔
636
    }
4✔
637
  }
15✔
638

10✔
639
  private _restartInterval(): void {
10✔
640
    this._resetInterval();
10✔
641

10✔
642
    if (asNumber(this.interval) > 0) {
10✔
643
      this._lastInterval = setInterval(() => {
9✔
644
        if (
8✔
645
          this.isPlaying &&
8✔
646
          this.total &&
6✔
647
          !(this.disableLoop && this._nextIndex === 0)
6✔
648
        ) {
8✔
649
          this.select(this.slides[this._nextIndex], 'next');
5✔
650
          this.emitEvent('igcSlideChanged', { detail: this.current });
5✔
651
        } else {
8✔
652
          this.pause();
3✔
653
        }
3✔
654
      }, this.interval);
9✔
655
    }
9✔
656
  }
10✔
657

10✔
658
  private async _animateSlides(
10✔
659
    nextSlide: IgcCarouselSlideComponent,
31✔
660
    currentSlide: IgcCarouselSlideComponent,
31✔
661
    dir: 'next' | 'prev'
31✔
662
  ): Promise<void> {
31✔
663
    if (dir === 'next') {
31✔
664
      // Animate slides in next direction
18✔
665
      currentSlide.previous = true;
18✔
666
      currentSlide.toggleAnimation('out');
18✔
667
      this._activateSlide(nextSlide);
18✔
668
      await nextSlide.toggleAnimation('in');
18✔
669
      currentSlide.previous = false;
18✔
670
    } else {
31✔
671
      // Animate slides in previous direction
13✔
672
      currentSlide.previous = true;
13✔
673
      currentSlide.toggleAnimation('in', 'reverse');
13✔
674
      this._activateSlide(nextSlide);
13✔
675
      await nextSlide.toggleAnimation('out', 'reverse');
13✔
676
      currentSlide.previous = false;
13✔
677
    }
13✔
678
  }
31✔
679

10✔
680
  //#endregion
10✔
681

10✔
682
  //#region Public API
10✔
683

10✔
684
  /**
10✔
685
   * Resumes playing of the carousel slides.
10✔
686
   */
10✔
687
  public play(): void {
10✔
688
    if (!this.isPlaying) {
4✔
689
      this._paused = false;
4✔
690
      this._playing = true;
4✔
691
      this._restartInterval();
4✔
692
    }
4✔
693
  }
4✔
694

10✔
695
  /**
10✔
696
   * Pauses the carousel rotation of slides.
10✔
697
   */
10✔
698
  public pause(): void {
10✔
699
    if (this.isPlaying) {
9✔
700
      this._playing = false;
5✔
701
      this._paused = true;
5✔
702
      this._resetInterval();
5✔
703
    }
5✔
704
  }
9✔
705

10✔
706
  /**
10✔
707
   * Switches to the next slide, runs any animations, and returns if the operation was successful.
10✔
708
   */
10✔
709
  public async next(): Promise<boolean> {
10✔
710
    if (this.disableLoop && this._nextIndex === 0) {
10✔
711
      this.pause();
1✔
712
      return false;
1✔
713
    }
1✔
714

9✔
715
    return await this.select(this._slides[this._nextIndex], 'next');
9✔
716
  }
10✔
717

10✔
718
  /**
10✔
719
   * Switches to the previous slide, runs any animations, and returns if the operation was successful.
10✔
720
   */
10✔
721
  public async prev(): Promise<boolean> {
10✔
722
    if (this.disableLoop && this._previousIndex === this.total - 1) {
10✔
723
      this.pause();
1✔
724
      return false;
1✔
725
    }
1✔
726

9✔
727
    return await this.select(this._slides[this._previousIndex], 'prev');
9✔
728
  }
10✔
729

10✔
730
  /* blazorSuppress */
10✔
731
  /**
10✔
732
   * Switches to the passed-in slide, runs any animations, and returns if the operation was successful.
10✔
733
   */
10✔
734
  public async select(
10✔
735
    slide: IgcCarouselSlideComponent,
10✔
736
    animationDirection?: 'next' | 'prev'
10✔
737
  ): Promise<boolean>;
10✔
738
  /**
10✔
739
   * Switches to slide by index, runs any animations, and returns if the operation was successful.
10✔
740
   */
10✔
741
  public async select(
10✔
742
    index: number,
10✔
743
    animationDirection?: 'next' | 'prev'
10✔
744
  ): Promise<boolean>;
10✔
745
  public async select(
10✔
746
    slideOrIndex: IgcCarouselSlideComponent | number,
35✔
747
    animationDirection?: 'next' | 'prev'
35✔
748
  ): Promise<boolean> {
35✔
749
    let index: number;
35✔
750
    let slide: IgcCarouselSlideComponent | undefined;
35✔
751

35✔
752
    if (typeof slideOrIndex === 'number') {
35✔
753
      index = slideOrIndex;
3✔
754
      slide = this._slides.at(index);
3✔
755
    } else {
35✔
756
      slide = slideOrIndex;
32✔
757
      index = this._slides.indexOf(slide);
32✔
758
    }
32✔
759

35✔
760
    if (index === this.current || index === -1 || !slide) {
35✔
761
      return false;
4✔
762
    }
4✔
763

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

35✔
766
    await this._animateSlides(slide, this._activeSlide, dir);
35✔
767
    return true;
31✔
768
  }
35✔
769

10✔
770
  //#endregion
10✔
771

10✔
772
  //#region Template renderers
10✔
773

10✔
774
  private _renderNavigation() {
10✔
775
    return html`
155✔
776
      <igc-button
155✔
777
        ${ref(this._prevButtonRef)}
155✔
778
        type="button"
155✔
779
        part="navigation previous"
155✔
780
        aria-label=${this.resourceStrings.carousel_previous_slide ??
155!
781
        'previous slide'}
155✔
782
        aria-controls=${this._carouselId}
155✔
783
        ?disabled=${this.disableLoop && this.current === 0}
155✔
784
        @click=${this._handleNavigationInteractionPrevious}
155✔
785
      >
155✔
786
        <slot name="previous-button">
155✔
787
          <igc-icon
155✔
788
            name="carousel_prev"
155✔
789
            collection="default"
155✔
790
            aria-hidden="true"
155✔
791
          ></igc-icon>
155✔
792
        </slot>
155✔
793
      </igc-button>
155✔
794

155✔
795
      <igc-button
155✔
796
        ${ref(this._nextButtonRef)}
155✔
797
        type="button"
155✔
798
        part="navigation next"
155✔
799
        aria-label=${this.resourceStrings.carousel_next_slide ?? 'next slide'}
155!
800
        aria-controls=${this._carouselId}
155✔
801
        ?disabled=${this.disableLoop && this.current === this.total - 1}
155✔
802
        @click=${this._handleNavigationInteractionNext}
155✔
803
      >
155✔
804
        <slot name="next-button">
155✔
805
          <igc-icon
155✔
806
            name="carousel_next"
155✔
807
            collection="default"
155✔
808
            aria-hidden="true"
155✔
809
          ></igc-icon>
155✔
810
        </slot>
155✔
811
      </igc-button>
155✔
812
    `;
155✔
813
  }
155✔
814

10✔
815
  protected *_renderIndicators() {
10✔
816
    for (const [i, slide] of this._slides.entries()) {
148✔
817
      const forward = slide.active ? 'visible' : 'hidden';
298✔
818
      const backward = slide.active ? 'hidden' : 'visible';
298✔
819

298✔
820
      yield html`
298✔
821
        <igc-carousel-indicator
298✔
822
          exportparts="indicator, active, inactive"
298✔
823
          .active=${slide.active}
298✔
824
          .index=${i}
298✔
825
        >
298✔
826
          <div
298✔
827
            part="dot"
298✔
828
            style=${styleMap({ visibility: backward, zIndex: 1 })}
298✔
829
          ></div>
298✔
830
          <div
298✔
831
            part="dot active"
298✔
832
            slot="active"
298✔
833
            style=${styleMap({ visibility: forward })}
298✔
834
          ></div>
298✔
835
        </igc-carousel-indicator>
298✔
836
      `;
298✔
837
    }
298✔
838
  }
148✔
839

10✔
840
  private _renderIndicatorContainer() {
10✔
841
    const parts = {
151✔
842
      indicators: true,
151✔
843
      start: this.indicatorsOrientation === 'start',
151✔
844
    };
151✔
845

151✔
846
    return html`
151✔
847
      <igc-carousel-indicator-container>
151✔
848
        <div
151✔
849
          ${ref(this._indicatorsContainerRef)}
151✔
850
          role="tablist"
151✔
851
          part=${partMap(parts)}
151✔
852
        >
151✔
853
          <slot name="indicator" @click=${this._handleIndicatorClick}>
151✔
854
            ${cache(
151✔
855
              this._hasProjectedIndicators
151✔
856
                ? this._updateProjectedIndicators()
3✔
857
                : this._renderIndicators()
148✔
858
            )}
151✔
859
          </slot>
151✔
860
        </div>
151✔
861
      </igc-carousel-indicator-container>
151✔
862
    `;
151✔
863
  }
151✔
864

10✔
865
  private _renderLabel() {
10✔
866
    const parts = {
3✔
867
      label: true,
3✔
868
      indicators: true,
3✔
869
      start: this.indicatorsOrientation === 'start',
3✔
870
    };
3✔
871
    const value = formatString(
3✔
872
      this.slidesLabelFormat,
3✔
873
      this.current + 1,
3✔
874
      this.total
3✔
875
    );
3✔
876

3✔
877
    return html`
3✔
878
      <div part=${partMap(parts)}>
3✔
879
        <span>${value}</span>
3✔
880
      </div>
3✔
881
    `;
3✔
882
  }
3✔
883

10✔
884
  protected override render() {
10✔
885
    const hasNoIndicators = this.hideIndicators || this._showIndicatorsLabel;
156✔
886
    const hasLabel = !this.hideIndicators && this._showIndicatorsLabel;
156✔
887

156✔
888
    return html`
156✔
889
      <section>
156✔
890
        ${cache(this.hideNavigation ? nothing : this._renderNavigation())}
156✔
891
        ${hasNoIndicators ? nothing : this._renderIndicatorContainer()}
156✔
892
        ${hasLabel ? this._renderLabel() : nothing}
156✔
893
        <div
156✔
894
          ${ref(this._carouselSlidesContainerRef)}
156✔
895
          id=${this._carouselId}
156✔
896
          aria-live=${this.interval && this._playing ? 'off' : 'polite'}
156✔
897
        >
156✔
898
          <slot></slot>
156✔
899
        </div>
156✔
900
      </section>
156✔
901
    `;
156✔
902
  }
156✔
903
}
10✔
904

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

© 2025 Coveralls, Inc