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

IgniteUI / igniteui-webcomponents / 14702825363

28 Apr 2025 07:42AM UTC coverage: 98.263% (-0.02%) from 98.279%
14702825363

Pull #1352

github

web-flow
Merge 6e058e971 into 0f89d7575
Pull Request #1352: Refactor Tab component

4590 of 4823 branches covered (95.17%)

Branch coverage included in aggregate %.

582 of 592 new or added lines in 5 files covered. (98.31%)

1 existing line in 1 file now uncovered.

29414 of 29782 relevant lines covered (98.76%)

453.65 hits per line

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

99.07
/src/components/tabs/tabs.ts
1
import { LitElement, html, nothing } from 'lit';
10✔
2
import {
10✔
3
  eventOptions,
10✔
4
  property,
10✔
5
  queryAssignedElements,
10✔
6
  state,
10✔
7
} 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

10✔
11
import { styleMap } from 'lit/directives/style-map.js';
10✔
12
import { themes } from '../../theming/theming-decorator.js';
10✔
13
import IgcIconButtonComponent from '../button/icon-button.js';
10✔
14
import {
10✔
15
  addKeybindings,
10✔
16
  arrowLeft,
10✔
17
  arrowRight,
10✔
18
  endKey,
10✔
19
  homeKey,
10✔
20
} from '../common/controllers/key-bindings.js';
10✔
21
import {
10✔
22
  type MutationControllerParams,
10✔
23
  createMutationController,
10✔
24
} from '../common/controllers/mutation-observer.js';
10✔
25
import { createResizeController } from '../common/controllers/resize-observer.js';
10✔
26
import { watch } from '../common/decorators/watch.js';
10✔
27
import { registerComponent } from '../common/definitions/register.js';
10✔
28
import type { Constructor } from '../common/mixins/constructor.js';
10✔
29
import { EventEmitterMixin } from '../common/mixins/event-emitter.js';
10✔
30
import {
10✔
31
  findElementFromEventPath,
10✔
32
  first,
10✔
33
  getRoot,
10✔
34
  isEmpty,
10✔
35
  isLTR,
10✔
36
  isString,
10✔
37
  last,
10✔
38
  partNameMap,
10✔
39
  scrollIntoView,
10✔
40
  wrap,
10✔
41
} from '../common/util.js';
10✔
42
import type { TabsActivation, TabsAlignment } from '../types.js';
10✔
43
import { createTabHelpers, getTabHeader } from './tab-dom.js';
10✔
44
import IgcTabComponent from './tab.js';
10✔
45
import { styles as shared } from './themes/shared/tabs/tabs.common.css.js';
10✔
46
import { all } from './themes/tabs-themes.js';
10✔
47
import { styles } from './themes/tabs.base.css.js';
10✔
48

10✔
49
export interface IgcTabsComponentEventMap {
10✔
50
  igcChange: CustomEvent<IgcTabComponent>;
10✔
51
}
10✔
52

10✔
53
/* blazorAdditionalDependency: IgcTabComponent */
10✔
54
/**
10✔
55
 * Tabs organize and allow navigation between groups of content that are related and at the same level of hierarchy.
10✔
56
 *
10✔
57
 * The `<igc-tabs>` component allows the user to navigate between multiple `<igc-tab>` elements.
10✔
58
 * It supports keyboard navigation and provides API methods to control the selected tab.
10✔
59
 *
10✔
60
 * @element igc-tabs
10✔
61
 *
10✔
62
 * @fires igcChange - Emitted when the selected tab changes.
10✔
63
 *
10✔
64
 * @slot - Renders the `IgcTabComponents` inside default slot.
10✔
65
 *
10✔
66
 * @csspart start-scroll-button - The start scroll button displayed when the tabs overflow.
10✔
67
 * @csspart end-scroll-button - The end scroll button displayed when the tabs overflow.
10✔
68
 * @csspart selected-indicator - The indicator that shows which tab is selected.
10✔
69
 */
10✔
70
@themes(all)
10✔
71
export default class IgcTabsComponent extends EventEmitterMixin<
10✔
72
  IgcTabsComponentEventMap,
10✔
73
  Constructor<LitElement>
10✔
74
>(LitElement) {
10✔
75
  public static readonly tagName = 'igc-tabs';
10✔
76
  public static styles = [styles, shared];
10✔
77

10✔
78
  /* blazorSuppress */
10✔
79
  public static register(): void {
10✔
80
    registerComponent(
10✔
81
      IgcTabsComponent,
10✔
82
      IgcTabComponent,
10✔
83
      IgcIconButtonComponent
10✔
84
    );
10✔
85
  }
10✔
86

10✔
87
  //#region Private state & properties
10✔
88

10✔
89
  private readonly _resizeController = createResizeController(this, {
10✔
90
    callback: this._resizeCallback,
10✔
91
    options: { box: 'border-box' },
10✔
92
    target: null,
10✔
93
  });
10✔
94

10✔
95
  /** The tabs container reference holding the tab headers. */
10✔
96
  private readonly _headerRef = createRef<HTMLElement>();
10✔
97

10✔
98
  /** The selected tab indicator reference.  */
10✔
99
  private readonly _indicatorRef = createRef<HTMLElement>();
10✔
100

10✔
101
  private readonly _domHelpers = createTabHelpers(
10✔
102
    this,
10✔
103
    this._headerRef,
10✔
104
    this._indicatorRef
10✔
105
  );
10✔
106

10✔
107
  @queryAssignedElements({ selector: IgcTabComponent.tagName })
10✔
108
  private _tabs!: IgcTabComponent[];
10✔
109

10✔
110
  protected get _enabledTabs(): IgcTabComponent[] {
10✔
111
    return this._tabs.filter((tab) => !tab.disabled);
58✔
112
  }
58✔
113

10✔
114
  @state()
10✔
115
  private _activeTab?: IgcTabComponent;
10✔
116

10✔
117
  //#endregion
10✔
118

10✔
119
  //#region Public properties
10✔
120

10✔
121
  /**
10✔
122
   * Sets the alignment for the tab headers
10✔
123
   * @attr
10✔
124
   */
10✔
125
  @property({ reflect: true })
10✔
126
  public alignment: TabsAlignment = 'start';
10✔
127

10✔
128
  /**
10✔
129
   * Determines the tab activation. When set to auto,
10✔
130
   * the tab is instantly selected while navigating with the Left/Right Arrows, Home or End keys
10✔
131
   * and the corresponding panel is displayed.
10✔
132
   * When set to manual, the tab is only focused. The selection happens after pressing Space or Enter.
10✔
133
   * @attr
10✔
134
   */
10✔
135
  @property()
10✔
136
  public activation: TabsActivation = 'auto';
10✔
137

10✔
138
  /* blazorSuppress */
10✔
139
  /** Returns the direct `igc-tab` elements that are children of this element. */
10✔
140
  public get tabs(): IgcTabComponent[] {
10✔
141
    return this._tabs;
244✔
142
  }
244✔
143

10✔
144
  /** Returns the currently selected tab label or IDREF if no label property is set. */
10✔
145
  public get selected(): string {
10✔
146
    if (this._activeTab) {
2✔
147
      return this._activeTab.label || this._activeTab.id;
2!
148
    }
2!
UNCOV
149

×
NEW
150
    return '';
×
151
  }
2✔
152

10✔
153
  //#endregion
10✔
154

10✔
155
  @watch('alignment', { waitUntilFirstUpdate: true })
10✔
156
  protected _alignmentChanged(): void {
10✔
157
    this._domHelpers.setIndicator(this._activeTab);
5✔
158
  }
5✔
159

10✔
160
  //#region Life-cycle hooks
10✔
161

10✔
162
  constructor() {
10✔
163
    super();
38✔
164

38✔
165
    addKeybindings(this, {
38✔
166
      ref: this._headerRef,
38✔
167
      skip: this._skipKeyboard,
38✔
168
      bindingDefaults: { preventDefault: true },
38✔
169
    })
38✔
170
      .set(arrowLeft, () => this._handleArrowKeys(isLTR(this) ? -1 : 1))
38✔
171
      .set(arrowRight, () => this._handleArrowKeys(isLTR(this) ? 1 : -1))
38✔
172
      .set(homeKey, this._handleHomeKey)
38✔
173
      .set(endKey, this._handleEndKey)
38✔
174
      .setActivateHandler(this._handleActivationKeys, {
38✔
175
        preventDefault: false,
38✔
176
      });
38✔
177

38✔
178
    createMutationController(this, {
38✔
179
      callback: this._mutationCallback,
38✔
180
      config: {
38✔
181
        attributeFilter: ['selected'],
38✔
182
        childList: true,
38✔
183
        subtree: true,
38✔
184
      },
38✔
185
      filter: [IgcTabComponent.tagName],
38✔
186
    });
38✔
187
  }
38✔
188

10✔
189
  protected override async firstUpdated() {
10✔
190
    await this.updateComplete;
38✔
191

38✔
192
    const selectedTab =
38✔
193
      this._tabs.findLast((tab) => tab.selected) ?? first(this._enabledTabs);
38✔
194

38✔
195
    this._domHelpers.setStyleProperties();
38✔
196
    this._domHelpers.setScrollButtonState();
38✔
197
    this._setSelectedTab(selectedTab, false);
38✔
198

38✔
199
    this._resizeController.observe(this._headerRef.value!);
38✔
200
  }
38✔
201

10✔
202
  /** @internal */
10✔
203
  public override connectedCallback(): void {
10✔
204
    super.connectedCallback();
38✔
205
    this.role = 'tablist';
38✔
206
  }
38✔
207

10✔
208
  protected override updated(): void {
10✔
209
    if (this._domHelpers.isLeftToRightChanged) {
283✔
210
      this._domHelpers.setIndicator(this._activeTab);
43✔
211
    }
43✔
212
  }
283✔
213

10✔
214
  //#endregion
10✔
215

10✔
216
  //#region Observers callbacks
10✔
217

10✔
218
  private _resizeCallback(): void {
10✔
219
    this._domHelpers.setStyleProperties();
26✔
220
    this._domHelpers.setScrollButtonState();
26✔
221
    this._domHelpers.setIndicator(this._activeTab);
26✔
222
  }
26✔
223

10✔
224
  private _mutationCallback(
10✔
225
    parameters: MutationControllerParams<IgcTabComponent>
80✔
226
  ): void {
80✔
227
    this._selectedAttributeChanged(parameters);
80✔
228
    this._handleTabsRemoved(parameters);
80✔
229
    this._handleTabsAdded(parameters);
80✔
230

80✔
231
    this._domHelpers.setStyleProperties();
80✔
232
    this._domHelpers.setScrollButtonState();
80✔
233
    this._domHelpers.setIndicator(this._activeTab);
80✔
234
  }
80✔
235

10✔
236
  private _selectedAttributeChanged({
10✔
237
    changes,
80✔
238
  }: MutationControllerParams<IgcTabComponent>): void {
80✔
239
    const selected = changes.attributes.find(
80✔
240
      (tab) => this._tabs.includes(tab) && tab.selected
80✔
241
    );
80✔
242
    this._setSelectedTab(selected, false);
80✔
243
  }
80✔
244

10✔
245
  private _handleTabsAdded({
10✔
246
    changes,
80✔
247
  }: MutationControllerParams<IgcTabComponent>): void {
80✔
248
    if (!isEmpty(changes.added)) {
80✔
249
      const lastAdded = changes.added.findLast(
5✔
250
        (change) =>
5✔
251
          change.target.closest(this.tagName) === this && change.node.selected
5✔
252
      )?.node;
5✔
253

5✔
254
      this._setSelectedTab(lastAdded, false);
5✔
255
    }
5✔
256
  }
80✔
257

10✔
258
  private _handleTabsRemoved({
10✔
259
    changes,
80✔
260
  }: MutationControllerParams<IgcTabComponent>): void {
80✔
261
    if (!isEmpty(changes.removed)) {
80✔
262
      let nextSelectedTab: IgcTabComponent | null = null;
2✔
263

2✔
264
      const removed = changes.removed.filter(
2✔
265
        (change) => change.target.closest(this.tagName) === this
2✔
266
      );
2✔
267

2✔
268
      for (const each of removed) {
2✔
269
        if (each.node.selected && each.node === this._activeTab) {
3✔
270
          nextSelectedTab = first(this._enabledTabs);
1✔
271
          break;
1✔
272
        }
1✔
273
      }
3✔
274

2✔
275
      if (nextSelectedTab) {
2✔
276
        this._setSelectedTab(nextSelectedTab, false);
1✔
277
      }
1✔
278
    }
2✔
279
  }
80✔
280

10✔
281
  //#endregion
10✔
282

10✔
283
  //#region Private API
10✔
284

10✔
285
  private _getClosestActiveTabIndex(): number {
10✔
286
    const active = getRoot(this).activeElement;
9✔
287
    const tab = active ? active.closest(IgcTabComponent.tagName) : null;
9!
288
    return tab ? this._enabledTabs.indexOf(tab) : -1;
9✔
289
  }
9✔
290

10✔
291
  private _setSelectedTab(tab?: IgcTabComponent, shouldEmit = true): void {
10✔
292
    if (!tab || tab === this._activeTab) {
154✔
293
      return;
86✔
294
    }
86✔
295

68✔
296
    if (this._activeTab) {
154✔
297
      this._activeTab.selected = false;
31✔
298
    }
31✔
299

68✔
300
    tab.selected = true;
68✔
301
    this._activeTab = tab;
68✔
302

68✔
303
    scrollIntoView(getTabHeader(tab));
68✔
304
    this._domHelpers.setIndicator(this._activeTab);
68✔
305

68✔
306
    if (shouldEmit) {
154✔
307
      this.emitEvent('igcChange', { detail: this._activeTab });
13✔
308
    }
13✔
309
  }
154✔
310

10✔
311
  private _keyboardActivateTab(tab: IgcTabComponent) {
10✔
312
    const header = getTabHeader(tab);
14✔
313

14✔
314
    this._domHelpers.setScrollSnap();
14✔
315
    scrollIntoView(header);
14✔
316
    header.focus({ preventScroll: true });
14✔
317

14✔
318
    if (this.activation === 'auto') {
14✔
319
      this._setSelectedTab(tab);
9✔
320
    }
9✔
321
  }
14✔
322

10✔
323
  private _skipKeyboard(node: Element, event: KeyboardEvent): boolean {
10✔
324
    return !(
30✔
325
      this._isEventFromTabHeader(event) &&
30✔
326
      this._tabs.includes(node.closest(IgcTabComponent.tagName)!)
28✔
327
    );
30✔
328
  }
30✔
329

10✔
330
  private _isEventFromTabHeader(event: Event) {
10✔
331
    return findElementFromEventPath('[part~="tab-header"]', event);
38✔
332
  }
38✔
333

10✔
334
  //#endregion
10✔
335

10✔
336
  //#region Event handlers
10✔
337

10✔
338
  protected _handleArrowKeys(delta: -1 | 1): void {
10✔
339
    const tabs = this._enabledTabs;
7✔
340
    this._keyboardActivateTab(
7✔
341
      tabs[wrap(0, tabs.length - 1, this._getClosestActiveTabIndex() + delta)]
7✔
342
    );
7✔
343
  }
7✔
344

10✔
345
  protected _handleHomeKey(): void {
10✔
346
    this._keyboardActivateTab(first(this._enabledTabs));
2✔
347
  }
2✔
348

10✔
349
  protected _handleEndKey(): void {
10✔
350
    this._keyboardActivateTab(last(this._enabledTabs));
3✔
351
  }
3✔
352

10✔
353
  protected _handleActivationKeys(): void {
10✔
354
    const tabs = this._enabledTabs;
2✔
355
    const index = this._getClosestActiveTabIndex();
2✔
356

2✔
357
    if (index > -1) {
2✔
358
      this._setSelectedTab(tabs[index], false);
2✔
359
      this._keyboardActivateTab(tabs[index]);
2✔
360
    }
2✔
361
  }
2✔
362

10✔
363
  protected _handleClick(event: PointerEvent): void {
10✔
364
    if (!this._isEventFromTabHeader(event)) {
8✔
365
      return;
1✔
366
    }
1✔
367

7✔
368
    const tab = findElementFromEventPath<IgcTabComponent>(
7✔
369
      IgcTabComponent.tagName,
7✔
370
      event
7✔
371
    );
7✔
372

7✔
373
    if (!(tab && this._tabs.includes(tab)) || tab?.disabled) {
8✔
374
      return;
2✔
375
    }
2✔
376

5✔
377
    this._domHelpers.setScrollSnap();
5✔
378
    getTabHeader(tab).focus();
5✔
379
    this._setSelectedTab(tab);
5✔
380
  }
8✔
381

10✔
382
  @eventOptions({ passive: true })
10✔
383
  protected _handleScroll(): void {
10✔
384
    this._domHelpers.setScrollButtonState();
71✔
385
  }
71✔
386

10✔
387
  //#endregion
10✔
388

10✔
389
  //#region Public API methods
10✔
390

10✔
391
  /** Selects the specified tab and displays the corresponding panel.  */
10✔
392
  public select(ref: IgcTabComponent | string): void {
10✔
393
    const tab = isString(ref) ? this._tabs.find((t) => t.id === ref) : ref;
15✔
394

15✔
395
    if (tab) {
15✔
396
      this._setSelectedTab(tab, false);
14✔
397
    }
14✔
398
  }
15✔
399

10✔
400
  //#endregion
10✔
401

10✔
402
  //#region Render
10✔
403

10✔
404
  protected _renderScrollButton(direction: 'start' | 'end') {
10✔
405
    const isStart = direction === 'start';
566✔
406
    const { start, end } = this._domHelpers.scrollButtonsDisabled;
566✔
407

566✔
408
    return html`${cache(
566✔
409
      this._domHelpers.hasScrollButtons
566✔
410
        ? html`
214✔
411
            <igc-icon-button
214✔
412
              tabindex="-1"
214✔
413
              variant="flat"
214✔
414
              collection="default"
214✔
415
              part="${direction}-scroll-button"
214✔
416
              exportparts="icon"
214✔
417
              name=${isStart ? 'prev' : 'next'}
214✔
418
              ?disabled=${isStart ? start : end}
214✔
419
              @click=${() => this._domHelpers.scrollTabs(direction)}
214✔
420
            >
352✔
421
            </igc-icon-button>
352✔
422
          `
352✔
423
        : nothing
352✔
424
    )}`;
566✔
425
  }
566✔
426

10✔
427
  protected override render() {
10✔
428
    const part = partNameMap({
283✔
429
      inner: true,
283✔
430
      scrollable: this._domHelpers.hasScrollButtons,
283✔
431
    });
283✔
432

283✔
433
    return html`
283✔
434
      <div
283✔
435
        ${ref(this._headerRef)}
283✔
436
        part="tabs"
283✔
437
        style=${styleMap(this._domHelpers.styleProperties)}
283✔
438
        @scroll=${this._handleScroll}
283✔
439
      >
283✔
440
        <div part=${part}>
283✔
441
          ${this._renderScrollButton('start')}
283✔
442
          <slot @click=${this._handleClick}></slot>
283✔
443
          ${this._renderScrollButton('end')}
283✔
444
          <div part="selected-indicator">
283✔
445
            <span ${ref(this._indicatorRef)}></span>
283✔
446
          </div>
283✔
447
        </div>
283✔
448
      </div>
283✔
449
    `;
283✔
450
  }
283✔
451

10✔
452
  //#endregion
10✔
453
}
10✔
454

10✔
455
declare global {
10✔
456
  interface HTMLElementTagNameMap {
10✔
457
    'igc-tabs': IgcTabsComponent;
10✔
458
  }
10✔
459
}
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