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

IgniteUI / igniteui-webcomponents / 15300667911

28 May 2025 12:54PM UTC coverage: 98.255% (-0.01%) from 98.266%
15300667911

Pull #1684

github

web-flow
Merge 7c672357a into 8e858b455
Pull Request #1684: refactor: Keyboard focus ring controller

4598 of 4832 branches covered (95.16%)

Branch coverage included in aggregate %.

179 of 179 new or added lines in 10 files covered. (100.0%)

2 existing lines in 1 file now uncovered.

29470 of 29841 relevant lines covered (98.76%)

449.16 hits per line

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

97.98
/src/components/carousel/carousel.ts
1
import { ContextProvider } from '@lit/context';
10✔
2
import { LitElement, html, 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 { themes } from '../../theming/theming-decorator.js';
10✔
13
import IgcButtonComponent from '../button/button.js';
10✔
14
import { carouselContext } from '../common/context.js';
10✔
15
import { addKeyboardFocusRing } from '../common/controllers/focus-ring.js';
10✔
16
import {
10✔
17
  type SwipeEvent,
10✔
18
  addGesturesController,
10✔
19
} from '../common/controllers/gestures.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
  type MutationControllerParams,
10✔
29
  createMutationController,
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 {
10✔
36
  asNumber,
10✔
37
  createCounter,
10✔
38
  findElementFromEventPath,
10✔
39
  first,
10✔
40
  formatString,
10✔
41
  isLTR,
10✔
42
  last,
10✔
43
  partNameMap,
10✔
44
  wrap,
10✔
45
} from '../common/util.js';
10✔
46
import IgcIconComponent from '../icon/icon.js';
10✔
47
import type {
10✔
48
  CarouselIndicatorsOrientation,
10✔
49
  HorizontalTransitionAnimation,
10✔
50
} from '../types.js';
10✔
51
import IgcCarouselIndicatorContainerComponent from './carousel-indicator-container.js';
10✔
52
import IgcCarouselIndicatorComponent from './carousel-indicator.js';
10✔
53
import IgcCarouselSlideComponent from './carousel-slide.js';
10✔
54
import { styles } from './themes/carousel.base.css.js';
10✔
55
import { all } from './themes/container.js';
10✔
56
import { styles as shared } from './themes/shared/carousel.common.css.js';
10✔
57

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

10✔
64
/**
10✔
65
 * The `igc-carousel` presents a set of `igc-carousel-slide`s by sequentially displaying a subset of one or more slides.
10✔
66
 *
10✔
67
 * @element igc-carousel
10✔
68
 *
10✔
69
 * @slot Default slot for the carousel. Any projected `igc-carousel-slide` components should be projected here.
10✔
70
 * @slot previous-button - Renders content inside the previous button.
10✔
71
 * @slot next-button - Renders content inside the next button.
10✔
72
 *
10✔
73
 * @fires igcSlideChanged - Emitted when the current active slide is changed either by user interaction or by the interval callback.
10✔
74
 * @fires igcPlaying - Emitted when the carousel enters playing state by a user interaction.
10✔
75
 * @fires igcPaused - Emitted when the carousel enters paused state by a user interaction.
10✔
76
 *
10✔
77
 * @csspart navigation - The wrapper container of each carousel navigation button.
10✔
78
 * @csspart previous - The wrapper container of the carousel previous navigation button.
10✔
79
 * @csspart next - The wrapper container of the carousel next navigation button.
10✔
80
 * @csspart dot - The carousel dot indicator container.
10✔
81
 * @csspart active - The carousel active dot indicator container.
10✔
82
 * @csspart label - The label container of the carousel indicators.
10✔
83
 * @csspart start - The wrapping container of all carousel indicators when indicators-orientation is set to start.
10✔
84
 */
10✔
85

10✔
86
@themes(all)
10✔
87
export default class IgcCarouselComponent extends EventEmitterMixin<
10✔
88
  IgcCarouselComponentEventMap,
10✔
89
  Constructor<LitElement>
10✔
90
>(LitElement) {
10✔
91
  public static styles = [styles, shared];
10✔
92
  public static readonly tagName = 'igc-carousel';
10✔
93

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

10✔
106
  private static readonly increment = createCounter();
10✔
107
  private readonly _carouselId = `igc-carousel-${IgcCarouselComponent.increment()}`;
10✔
108
  private readonly _focusRingManager = addKeyboardFocusRing(this);
10✔
109

10✔
110
  private readonly _internals: ElementInternals;
10✔
111
  private _lastInterval!: ReturnType<typeof setInterval> | null;
10✔
112
  private _hasKeyboardInteractionOnIndicators = false;
10✔
113
  private _hasMouseStop = false;
10✔
114

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3✔
177
    this.activateSlide(this.slides[idx]);
3✔
178
  }
74✔
179

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

4✔
319
    this.restartInterval();
4✔
320
  }
4✔
321

10✔
322
  constructor() {
10✔
323
    super();
44✔
324
    this._internals = this.attachInternals();
44✔
325

44✔
326
    this._internals.role = 'region';
44✔
327
    this._internals.ariaRoleDescription = 'carousel';
44✔
328

44✔
329
    this.addEventListener('pointerenter', this.handlePointerEnter);
44✔
330
    this.addEventListener('pointerleave', this.handlePointerLeave);
44✔
331

44✔
332
    addGesturesController(this, {
44✔
333
      ref: this._carouselSlidesContainerRef,
44✔
334
      touchOnly: true,
44✔
335
    })
44✔
336
      .set('swipe-left', this.handleHorizontalSwipe)
44✔
337
      .set('swipe-right', this.handleHorizontalSwipe)
44✔
338
      .set('swipe-up', this.handleVerticalSwipe)
44✔
339
      .set('swipe-down', this.handleVerticalSwipe);
44✔
340

44✔
341
    addKeybindings(this, {
44✔
342
      ref: this._indicatorsContainerRef,
44✔
343
      bindingDefaults: { preventDefault: true },
44✔
344
    })
44✔
345
      .set(arrowLeft, this.handleArrowLeft)
44✔
346
      .set(arrowRight, this.handleArrowRight)
44✔
347
      .set(homeKey, this.handleHomeKey)
44✔
348
      .set(endKey, this.handleEndKey);
44✔
349

44✔
350
    addKeybindings(this, {
44✔
351
      ref: this._prevButtonRef,
44✔
352
      bindingDefaults: { preventDefault: true },
44✔
353
    }).setActivateHandler(this.handleNavigationInteractionPrevious);
44✔
354

44✔
355
    addKeybindings(this, {
44✔
356
      ref: this._nextButtonRef,
44✔
357
      bindingDefaults: { preventDefault: true },
44✔
358
    }).setActivateHandler(this.handleNavigationInteractionNext);
44✔
359

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

10✔
371
  private handleSlotChange(): void {
10✔
372
    if (this.total) {
43✔
373
      this.activateSlide(
43✔
374
        this.slides.findLast((slide) => slide.active) ?? first(this.slides)
43✔
375
      );
43✔
376
    }
43✔
377
  }
43✔
378

10✔
379
  private handleIndicatorSlotChange(): void {
10✔
380
    this.requestUpdate();
3✔
381
  }
3✔
382

10✔
383
  private handlePointerEnter(): void {
10✔
384
    this._hasMouseStop = true;
3✔
385
    if (this._focusRingManager.focused) {
3!
386
      return;
×
387
    }
×
388
    this.handlePauseOnInteraction();
3✔
389
  }
3✔
390

10✔
391
  private handlePointerLeave(): void {
10✔
392
    this._hasMouseStop = false;
4✔
393
    if (this._focusRingManager.focused) {
4✔
394
      return;
1✔
395
    }
1✔
396
    this.handlePauseOnInteraction();
3✔
397
  }
4✔
398

10✔
399
  private handleFocusIn(): void {
10✔
400
    if (this._focusRingManager.focused || this._hasMouseStop) {
8✔
401
      return;
6✔
402
    }
6✔
403
    this.handlePauseOnInteraction();
2✔
404
  }
8✔
405

10✔
406
  private handleFocusOut(event: FocusEvent): void {
10✔
407
    const node = event.relatedTarget as Node;
2✔
408

2✔
409
    if (this.contains(node) || this.renderRoot.contains(node)) {
2!
UNCOV
410
      return;
×
UNCOV
411
    }
×
412

2✔
413
    if (this._focusRingManager.focused && !this._hasMouseStop) {
2✔
414
      this.handlePauseOnInteraction();
2✔
415
    }
2✔
416
  }
2✔
417

10✔
418
  private handlePauseOnInteraction(): void {
10✔
419
    if (!this.interval || this.disablePauseOnInteraction) return;
10✔
420

4✔
421
    if (this.isPlaying) {
10✔
422
      this.pause();
2✔
423
      this.emitEvent('igcPaused');
2✔
424
    } else {
2✔
425
      this.play();
2✔
426
      this.emitEvent('igcPlaying');
2✔
427
    }
2✔
428
  }
10✔
429

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

10✔
435
  private async handleArrowRight(): Promise<void> {
10✔
436
    this._hasKeyboardInteractionOnIndicators = true;
2✔
437
    this.handleInteraction(isLTR(this) ? this.next : this.prev);
2✔
438
  }
2✔
439

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

10✔
447
  private async handleEndKey(): Promise<void> {
10✔
448
    this._hasKeyboardInteractionOnIndicators = true;
2✔
449
    this.handleInteraction(() =>
2✔
450
      this.select(isLTR(this) ? last(this.slides) : first(this.slides))
2✔
451
    );
2✔
452
  }
2✔
453

10✔
454
  private handleVerticalSwipe({ data: { direction } }: SwipeEvent) {
10✔
455
    if (this.vertical) {
4✔
456
      this.handleInteraction(direction === 'up' ? this.next : this.prev);
2✔
457
    }
2✔
458
  }
4✔
459

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

10✔
472
  private async handleIndicatorClick(event: PointerEvent): Promise<void> {
10✔
473
    const indicator = findElementFromEventPath<IgcCarouselIndicatorComponent>(
2✔
474
      IgcCarouselIndicatorComponent.tagName,
2✔
475
      event
2✔
476
    )!;
2✔
477

2✔
478
    const index = this.hasProjectedIndicators
2!
479
      ? this._projectedIndicators.indexOf(indicator)
×
480
      : Array.from(this._defaultIndicators).indexOf(indicator);
2✔
481

2✔
482
    this.handleInteraction(() =>
2✔
483
      this.select(this.slides[index], index > this.current ? 'next' : 'prev')
2✔
484
    );
2✔
485
  }
2✔
486

10✔
487
  private handleNavigationInteractionNext() {
10✔
488
    this.handleInteraction(this.next);
3✔
489
  }
3✔
490

10✔
491
  private handleNavigationInteractionPrevious() {
10✔
492
    this.handleInteraction(this.prev);
3✔
493
  }
3✔
494

10✔
495
  private async handleInteraction(
10✔
496
    callback: () => Promise<unknown>
22✔
497
  ): Promise<void> {
22✔
498
    if (this.interval) {
22!
499
      this.resetInterval();
×
500
    }
×
501

22✔
502
    await callback.call(this);
22✔
503
    this.emitEvent('igcSlideChanged', { detail: this.current });
20✔
504

20✔
505
    if (this.interval) {
22!
506
      this.restartInterval();
×
507
    }
×
508
  }
22✔
509

10✔
510
  private activateSlide(slide: IgcCarouselSlideComponent): void {
10✔
511
    if (this._activeSlide) {
73✔
512
      this._activeSlide.active = false;
31✔
513
    }
31✔
514

73✔
515
    this._activeSlide = slide;
73✔
516
    this._activeSlide.active = true;
73✔
517

73✔
518
    if (this._hasKeyboardInteractionOnIndicators) {
73✔
519
      this.hasProjectedIndicators
8!
520
        ? this._projectedIndicators[this.current].focus()
×
521
        : this._defaultIndicators[this.current].focus();
8✔
522

8✔
523
      this._hasKeyboardInteractionOnIndicators = false;
8✔
524
    }
8✔
525
  }
73✔
526

10✔
527
  private updateProjectedIndicators(): void {
10✔
528
    for (const [idx, slide] of this.slides.entries()) {
3✔
529
      const indicator = this._projectedIndicators[idx];
3✔
530
      indicator.active = slide.active;
3✔
531
      indicator.index = idx;
3✔
532

3✔
533
      this.setAttribute('aria-controls', slide.id);
3✔
534
    }
3✔
535
  }
3✔
536

10✔
537
  private resetInterval(): void {
10✔
538
    if (this._lastInterval) {
10✔
539
      clearInterval(this._lastInterval);
2✔
540
      this._lastInterval = null;
2✔
541
    }
2✔
542
  }
10✔
543

10✔
544
  private restartInterval(): void {
10✔
545
    this.resetInterval();
7✔
546

7✔
547
    if (asNumber(this.interval) > 0) {
7✔
548
      this._lastInterval = setInterval(() => {
6✔
549
        if (this.isPlaying && this.total) {
1✔
550
          this.next();
1✔
551
          this.emitEvent('igcSlideChanged', { detail: this.current });
1✔
552
        } else {
1!
553
          this.pause();
×
554
        }
×
555
      }, this.interval);
6✔
556
    }
6✔
557
  }
7✔
558

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

10✔
581
  /**
10✔
582
   * Resumes playing of the carousel slides.
10✔
583
   */
10✔
584
  public play(): void {
10✔
585
    if (!this.isPlaying) {
3✔
586
      this._paused = false;
3✔
587
      this._playing = true;
3✔
588
      this.restartInterval();
3✔
589
    }
3✔
590
  }
3✔
591

10✔
592
  /**
10✔
593
   * Pauses the carousel rotation of slides.
10✔
594
   */
10✔
595
  public pause(): void {
10✔
596
    if (this.isPlaying) {
5✔
597
      this._playing = false;
3✔
598
      this._paused = true;
3✔
599
      this.resetInterval();
3✔
600
    }
3✔
601
  }
5✔
602

10✔
603
  /**
10✔
604
   * Switches to the next slide, runs any animations, and returns if the operation was successful.
10✔
605
   */
10✔
606
  public async next(): Promise<boolean> {
10✔
607
    if (this.disableLoop && this.nextIndex === 0) {
11✔
608
      this.pause();
1✔
609
      return false;
1✔
610
    }
1✔
611

10✔
612
    return await this.select(this.slides[this.nextIndex], 'next');
10✔
613
  }
11✔
614

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

9✔
624
    return await this.select(this.slides[this.prevIndex], 'prev');
9✔
625
  }
10✔
626

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

31✔
649
    if (typeof slideOrIndex === 'number') {
31✔
650
      index = slideOrIndex;
3✔
651
      slide = this.slides.at(index);
3✔
652
    } else {
31✔
653
      slide = slideOrIndex;
28✔
654
      index = this.slides.indexOf(slide);
28✔
655
    }
28✔
656

31✔
657
    if (index === this.current || index === -1 || !slide) {
31✔
658
      return false;
4✔
659
    }
4✔
660

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

31✔
663
    await this.animateSlides(slide, this._activeSlide, dir);
31✔
664
    return true;
25✔
665
  }
31✔
666

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

10✔
706
  protected *renderIndicators() {
10✔
707
    for (const [i, slide] of this.slides.entries()) {
138✔
708
      const forward = slide.active ? 'visible' : 'hidden';
285✔
709
      const backward = slide.active ? 'hidden' : 'visible';
285✔
710

285✔
711
      yield html`
285✔
712
        <igc-carousel-indicator
285✔
713
          exportparts="indicator, active, inactive"
285✔
714
          .active=${slide.active}
285✔
715
          .index=${i}
285✔
716
        >
285✔
717
          <div
285✔
718
            part="dot"
285✔
719
            style=${styleMap({ visibility: backward, zIndex: 1 })}
285✔
720
          ></div>
285✔
721
          <div
285✔
722
            part="dot active"
285✔
723
            slot="active"
285✔
724
            style=${styleMap({ visibility: forward })}
285✔
725
          ></div>
285✔
726
        </igc-carousel-indicator>
285✔
727
      `;
285✔
728
    }
285✔
729
  }
138✔
730

10✔
731
  private indicatorTemplate() {
10✔
732
    const parts = partNameMap({
141✔
733
      indicators: true,
141✔
734
      start: this.indicatorsOrientation === 'start',
141✔
735
    });
141✔
736

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

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

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

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

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