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

adobe / spectrum-web-components / 13553164764

26 Feb 2025 08:53PM UTC coverage: 97.966% (-0.2%) from 98.185%
13553164764

Pull #5031

github

web-flow
Merge b7398ad1e into 191a15bd9
Pull Request #5031: fix(action menu): keyboard accessibility omnibus

5295 of 5602 branches covered (94.52%)

Branch coverage included in aggregate %.

627 of 678 new or added lines in 11 files covered. (92.48%)

27 existing lines in 8 files now uncovered.

33662 of 34164 relevant lines covered (98.53%)

644.21 hits per line

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

98.34
/packages/tabs/src/Tabs.ts
1
/*
6✔
2
Copyright 2020 Adobe. All rights reserved.
6✔
3
This file is licensed to you under the Apache License, Version 2.0 (the "License");
6✔
4
you may not use this file except in compliance with the License. You may obtain a copy
6✔
5
of the License at http://www.apache.org/licenses/LICENSE-2.0
6✔
6

6✔
7
Unless required by applicable law or agreed to in writing, software distributed under
6✔
8
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
6✔
9
OF ANY KIND, either express or implied. See the License for the specific language
6✔
10
governing permissions and limitations under the License.
6✔
11
*/
6✔
12

6✔
13
import {
6✔
14
    css,
6✔
15
    CSSResult,
6✔
16
    CSSResultArray,
6✔
17
    html,
6✔
18
    PropertyValueMap,
6✔
19
    PropertyValues,
6✔
20
    SizedMixin,
6✔
21
    TemplateResult,
6✔
22
} from '@spectrum-web-components/base';
6✔
23
import {
6✔
24
    property,
6✔
25
    query,
6✔
26
} from '@spectrum-web-components/base/src/decorators.js';
6✔
27
import {
6✔
28
    classMap,
6✔
29
    ifDefined,
6✔
30
} from '@spectrum-web-components/base/src/directives.js';
6✔
31
import { IntersectionController } from '@lit-labs/observers/intersection-controller.js';
6✔
32
import { ResizeController } from '@lit-labs/observers/resize-controller.js';
6✔
33
import { Tab } from './Tab.js';
6✔
34
import { Focusable } from '@spectrum-web-components/shared';
6✔
35
import { RovingTabindexController } from '@spectrum-web-components/reactive-controllers/src/RovingTabindex.js';
6✔
36

6✔
37
import tabStyles from './tabs.css.js';
6✔
38
import tabSizes from './tabs-sizes.css.js';
6✔
39
import { TabPanel } from './TabPanel.js';
6✔
40

6✔
41
// Encapsulated for use both here and in TopNav
6✔
42
export const ScaledIndicator = {
6✔
43
    baseSize: 100 as const,
6✔
44
    noSelectionStyle: 'transform: translateX(0px) scaleX(0) scaleY(0)',
6✔
45

6✔
46
    transformX(left: number, width: number): string {
6✔
47
        const scale = width / this.baseSize;
158✔
48
        return `transform: translateX(${left}px) scaleX(${scale});`;
158✔
49
    },
158✔
50

6✔
51
    transformY(top: number, height: number): string {
6✔
52
        const scale = height / this.baseSize;
8✔
53
        return `transform: translateY(${top}px) scaleY(${scale});`;
8✔
54
    },
8✔
55

6✔
56
    baseStyles(): CSSResult {
6✔
57
        return css`
5✔
58
            :host([direction='vertical-right']) #selection-indicator,
5✔
59
            :host([direction='vertical']) #selection-indicator {
5✔
60
                height: ${this.baseSize}px;
5✔
61
            }
5✔
62
            :host([dir][direction='horizontal']) #selection-indicator {
5✔
63
                width: ${this.baseSize}px;
5✔
64
            }
5✔
65
        `;
5✔
66
    },
5✔
67
};
6✔
68

6✔
69
/**
6✔
70
 * Given that the scroll needs to be on the right side of the viewport.
6✔
71
 * Returns the coordonate x it needs to scroll so that the tab with given index is visible.
6✔
72
 */
6✔
73
export function calculateScrollTargetForRightSide(
6✔
74
    index: number,
3✔
75
    direction: 'rtl' | 'ltr',
3✔
76
    tabs: Tab[],
3✔
77
    container: HTMLDivElement
3✔
78
): number {
3✔
79
    const nextIndex = index + (direction === 'rtl' ? -1 : 1);
3✔
80
    const nextTab = tabs[nextIndex];
3✔
81
    const viewportEnd = container.scrollLeft + container.offsetWidth;
3✔
82
    return nextTab ? nextTab.offsetLeft - container.offsetWidth : viewportEnd;
3✔
83
}
3✔
84

6✔
85
/**
6✔
86
 * Given that the scroll needs to be on the left side of the viewport.
6✔
87
 * Returns the coordonate x it needs to scroll so that the tab with given index is visible.
6✔
88
 */
6✔
89
export function calculateScrollTargetForLeftSide(
6✔
90
    index: number,
5✔
91
    direction: 'rtl' | 'ltr',
5✔
92
    tabs: Tab[],
5✔
93
    container: HTMLDivElement
5✔
94
): number {
5✔
95
    const prevIndex = index + (direction === 'rtl' ? 1 : -1);
5✔
96
    const prevTab = tabs[prevIndex];
5✔
97
    const leftmostElement = direction === 'rtl' ? -container.offsetWidth : 0;
5✔
98
    return prevTab ? prevTab.offsetLeft + prevTab.offsetWidth : leftmostElement;
5✔
99
}
5✔
100

6✔
101
/**
6✔
102
 * @element sp-tabs
6✔
103
 *
6✔
104
 * @slot - Tab elements to manage as a group
6✔
105
 * @slot tab-panel - Tab Panel elements related to the listed Tab elements
6✔
106
 * @csspart tablist - Container element for the slotted sp-tab elements
6✔
107
 *
6✔
108
 * @fires change - The selected Tab child has changed.
6✔
109
 */
6✔
110
export class Tabs extends SizedMixin(Focusable, { noDefaultSize: true }) {
6✔
111
    public static override get styles(): CSSResultArray {
6✔
112
        return [tabSizes, tabStyles, ScaledIndicator.baseStyles()];
6✔
113
    }
6✔
114

6✔
115
    /**
6✔
116
     * Whether to activate a tab on keyboard focus or not.
6✔
117
     *
6✔
118
     * By default a tab is activated via a "click" interaction. This is specifically intended for when
6✔
119
     * tab content cannot be displayed instantly, e.g. not all of the DOM content is available, etc.
6✔
120
     * To learn more about "Deciding When to Make Selection Automatically Follow Focus", visit:
6✔
121
     * https://w3c.github.io/aria-practices/#kbd_selection_follows_focus
6✔
122
     */
6✔
123
    @property({ type: Boolean })
6✔
124
    public auto = false;
6✔
125

6✔
126
    /**
6✔
127
     * The tab items are displayed closer together.
6✔
128
     */
6✔
129
    @property({ type: Boolean, reflect: true })
6✔
130
    public compact = false;
6✔
131

6✔
132
    @property({ reflect: true })
6✔
133
    public override dir!: 'ltr' | 'rtl';
6✔
134

6✔
135
    @property({ reflect: true })
6✔
136
    public direction: 'vertical' | 'vertical-right' | 'horizontal' =
6✔
137
        'horizontal';
6✔
138

6✔
139
    @property({ type: Boolean, reflect: true })
6✔
140
    public emphasized = false;
6✔
141

6✔
142
    @property()
6✔
143
    public label = '';
6✔
144

6✔
145
    @property({ type: Boolean })
6✔
146
    public enableTabsScroll = false;
6✔
147

6✔
148
    /**
6✔
149
     * The tab list is displayed without a border.
6✔
150
     */
6✔
151
    @property({ type: Boolean, reflect: true })
6✔
152
    public quiet = false;
6✔
153

6✔
154
    @property({ attribute: false })
6✔
155
    public selectionIndicatorStyle = ScaledIndicator.noSelectionStyle;
6✔
156

6✔
157
    @property({ attribute: false })
6✔
158
    public shouldAnimate = false;
6✔
159

6✔
160
    @query('slot')
6✔
161
    private slotEl!: HTMLSlotElement;
6✔
162

6✔
163
    @query('#list')
6✔
164
    private tabList!: HTMLDivElement;
6✔
165

6✔
166
    @property({ reflect: true })
6✔
167
    selected = '';
6✔
168

6✔
169
    private set tabs(tabs: Tab[]) {
6✔
170
        if (tabs === this.tabs) return;
34!
171
        this._tabs.forEach((tab) => {
34✔
172
            this.resizeController.unobserve(tab);
×
173
        });
34✔
174
        tabs.forEach((tab) => {
34✔
175
            this.resizeController.observe(tab);
199✔
176
        });
34✔
177
        this._tabs = tabs;
34✔
178
        this.rovingTabindexController.clearElementCache();
34✔
179
    }
34✔
180

6✔
181
    private get tabs(): Tab[] {
6✔
182
        return this._tabs;
565✔
183
    }
565✔
184

6✔
185
    private _tabs: Tab[] = [];
6✔
186

6✔
187
    constructor() {
6✔
188
        super();
34✔
189
        new IntersectionController(this, {
34✔
190
            config: {
34✔
191
                root: null,
34✔
192
                rootMargin: '0px',
34✔
193
                threshold: [0, 1],
34✔
194
            },
34✔
195
            callback: () => {
34✔
196
                this.updateSelectionIndicator();
22✔
197
            },
22✔
198
        });
34✔
199
    }
34✔
200

6✔
201
    protected resizeController = new ResizeController(this, {
6✔
202
        callback: () => {
6✔
203
            this.updateSelectionIndicator();
95✔
204
        },
95✔
205
    });
6✔
206

6✔
207
    rovingTabindexController = new RovingTabindexController<Tab>(this, {
6✔
208
        focusInIndex: (elements) => {
6✔
209
            let focusInIndex = 0;
569✔
210
            const firstFocusableElement = elements.find((el, index) => {
569✔
211
                const focusInElement = this.selected
984✔
212
                    ? !el.disabled && el.value === this.selected
972✔
213
                    : !el.disabled;
14✔
214
                focusInIndex = index;
984✔
215
                return focusInElement;
984✔
216
            });
569✔
217
            return firstFocusableElement ? focusInIndex : -1;
569✔
218
        },
569✔
219
        direction: () => 'both',
6✔
220
        elementEnterAction: (el) => {
6✔
221
            if (!this.auto) return;
16✔
222

5✔
223
            this.shouldAnimate = true;
5✔
224
            this.selectTarget(el);
5✔
225
        },
16✔
226
        elements: () => this.tabs,
6✔
227
        isFocusableElement: (el) => !el.disabled,
6✔
228
        listenerScope: () => this.tabList,
6✔
229
    });
6✔
230

6✔
231
    /**
6✔
232
     * @private
6✔
233
     */
6✔
234
    public override get focusElement(): Tab | this {
6✔
235
        return this.rovingTabindexController.focusInElement || this;
84✔
236
    }
84✔
237

6✔
238
    private limitDeltaToInterval(min: number, max: number) {
6✔
239
        return (delta: number): number => {
24✔
240
            if (delta < min) return min;
24✔
241
            if (delta > max) return max;
24✔
242
            return delta;
19✔
243
        };
24✔
244
    }
24✔
245

6✔
246
    /**
6✔
247
     * Scrolls through the tabs component, on the X-axis, by a given ammount of pixels/ delta. The given delta is limited to the scrollable area of the tabs component.
6✔
248
     * @param {number} delta - The ammount of pixels to scroll by. If the value is positive, the tabs will scroll to the right. If the value is negative, the tabs will scroll to the left.
6✔
249
     * @param {ScrollBehavior} behavior - The scroll behavior to use. Defaults to 'smooth'.
6✔
250
     */
6✔
251
    public scrollTabs(
6✔
252
        delta: number,
24✔
253
        behavior: ScrollBehavior = 'smooth'
24✔
254
    ): void {
24✔
255
        if (delta === 0) return;
24!
256

24✔
257
        const { scrollLeft, clientWidth, scrollWidth } = this.tabList;
24✔
258
        const dirLimit = scrollWidth - clientWidth - Math.abs(scrollLeft);
24✔
259

24✔
260
        const limitDelta =
24✔
261
            this.dir === 'ltr'
24✔
262
                ? this.limitDeltaToInterval(-scrollLeft, dirLimit)
14✔
263
                : this.limitDeltaToInterval(-dirLimit, Math.abs(scrollLeft));
10✔
264

24✔
265
        this.tabList?.scrollBy({
24!
266
            left: limitDelta(delta),
24✔
267
            top: 0,
24✔
268
            behavior,
24✔
269
        });
24✔
270
    }
24✔
271

6✔
272
    public get scrollState(): Record<string, boolean> {
6✔
273
        if (this.tabList) {
257✔
274
            const { scrollLeft, clientWidth, scrollWidth } = this.tabList;
239✔
275
            const canScrollLeft = Math.abs(scrollLeft) > 0;
239✔
276
            const canScrollRight =
239✔
277
                Math.ceil(Math.abs(scrollLeft)) < scrollWidth - clientWidth;
239✔
278
            return {
239✔
279
                canScrollLeft:
239✔
280
                    this.dir === 'ltr' ? canScrollLeft : canScrollRight,
239✔
281
                canScrollRight:
239✔
282
                    this.dir === 'ltr' ? canScrollRight : canScrollLeft,
239✔
283
            };
239✔
284
        }
239✔
285
        return {};
18✔
286
    }
257✔
287

6✔
288
    override async getUpdateComplete(): Promise<boolean> {
6✔
289
        const complete = await super.getUpdateComplete();
98✔
290

98✔
291
        const tabs = [...this.children] as Tab[];
98✔
292
        const tabUpdateCompletes = tabs.map((tab) => {
98✔
293
            if (typeof tab.updateComplete !== 'undefined') {
838✔
294
                return tab.updateComplete;
835✔
295
            }
835✔
296
            return Promise.resolve(true);
3✔
297
        });
98✔
298

98✔
299
        await Promise.all(tabUpdateCompletes);
98✔
300
        return complete;
98✔
301
    }
98✔
302

6✔
303
    private getNecessaryAutoScroll(index: number): number {
6✔
304
        const selectedTab = this.tabs[index];
10✔
305
        const selectionEnd = selectedTab.offsetLeft + selectedTab.offsetWidth;
10✔
306
        const viewportEnd = this.tabList.scrollLeft + this.tabList.offsetWidth;
10✔
307
        const selectionStart = selectedTab.offsetLeft;
10✔
308
        const viewportStart = this.tabList.scrollLeft;
10✔
309

10✔
310
        if (selectionEnd > viewportEnd) {
10✔
311
            // Selection is on the right side, not visible.
1✔
312
            return calculateScrollTargetForRightSide(
1✔
313
                index,
1✔
314
                this.dir,
1✔
315
                this.tabs,
1✔
316
                this.tabList
1✔
317
            );
1✔
318
        } else if (selectionStart < viewportStart) {
10✔
319
            // Selection is on the left side, not visible.
1✔
320
            return calculateScrollTargetForLeftSide(
1✔
321
                index,
1✔
322
                this.dir,
1✔
323
                this.tabs,
1✔
324
                this.tabList
1✔
325
            );
1✔
326
        }
1✔
327

8✔
328
        return -1;
8✔
329
    }
10✔
330

6✔
331
    public async scrollToSelection(): Promise<void> {
6✔
332
        if (!this.enableTabsScroll || !this.selected) {
59✔
333
            return;
49✔
334
        }
49✔
335

10✔
336
        await this.updateComplete;
10✔
337

10✔
338
        const selectedIndex = this.tabs.findIndex(
10✔
339
            (tab) => tab.value === this.selected
10✔
340
        );
10✔
341

10✔
342
        if (selectedIndex !== -1 && this.tabList) {
59✔
343
            // We have a selection, calculate the scroll needed to bring it into view
10✔
344
            const scrollTarget = this.getNecessaryAutoScroll(selectedIndex);
10✔
345

10✔
346
            // scrollTarget = -1 means it is already into view.
10✔
347
            if (scrollTarget !== -1) {
10✔
348
                this.tabList.scrollTo({ left: scrollTarget });
2✔
349
            }
2✔
350
        }
10✔
351
    }
59✔
352

6✔
353
    protected override updated(
6✔
354
        changedProperties: PropertyValueMap<this>
253✔
355
    ): void {
253✔
356
        super.updated(changedProperties);
253✔
357

253✔
358
        if (changedProperties.has('selected')) {
253✔
359
            this.scrollToSelection();
59✔
360
        }
59✔
361
    }
253✔
362

6✔
363
    protected managePanels({
6✔
364
        target,
18✔
365
    }: Event & { target: HTMLSlotElement }): void {
18✔
366
        const panels = target.assignedElements() as TabPanel[];
18✔
367
        panels.map((panel) => {
18✔
368
            const { value, id } = panel;
148✔
369
            const tab = this.querySelector(`[role="tab"][value="${value}"]`);
148✔
370
            if (tab) {
148✔
371
                tab.setAttribute('aria-controls', id);
148✔
372
                panel.setAttribute('aria-labelledby', tab.id);
148✔
373
            }
148✔
374
            panel.selected = value === this.selected;
148✔
375
        });
18✔
376
    }
18✔
377

6✔
378
    protected override render(): TemplateResult {
6✔
379
        return html`
253✔
380
            <div
253✔
381
                class=${classMap({ scroll: this.enableTabsScroll })}
253✔
382
                aria-label=${ifDefined(this.label ? this.label : undefined)}
253!
383
                @click=${this.onClick}
253✔
384
                @keydown=${this.onKeyDown}
253✔
385
                @scroll=${this.onTabsScroll}
253✔
386
                id="list"
253✔
387
                role="tablist"
253✔
388
                part="tablist"
253✔
389
            >
253✔
390
                <slot @slotchange=${this.onSlotChange}></slot>
253✔
391
                <div
253✔
392
                    id="selection-indicator"
253✔
393
                    class=${ifDefined(
253✔
394
                        this.shouldAnimate ? undefined : 'first-position'
253✔
395
                    )}
253✔
396
                    style=${this.selectionIndicatorStyle}
253✔
397
                    role="presentation"
253✔
398
                ></div>
253✔
399
            </div>
253✔
400
            <slot name="tab-panel" @slotchange=${this.managePanels}></slot>
253✔
401
        `;
253✔
402
    }
253✔
403

6✔
404
    protected override willUpdate(changes: PropertyValues): void {
6✔
405
        if (!this.hasUpdated) {
253✔
406
            const selectedChild = this.querySelector(
34✔
407
                ':scope > [selected]'
34✔
408
            ) as Tab;
34✔
409
            if (selectedChild) {
34!
UNCOV
410
                this.selectTarget(selectedChild);
×
UNCOV
411
            }
×
412
        }
34✔
413

253✔
414
        super.willUpdate(changes);
253✔
415
        if (changes.has('selected')) {
253✔
416
            if (this.tabs.length) {
59✔
417
                this.updateCheckedState();
25✔
418
            }
25✔
419
            if (changes.get('selected')) {
59✔
420
                const previous = this.querySelector(
21✔
421
                    `[role="tabpanel"][value="${changes.get('selected')}"]`
21✔
422
                ) as TabPanel;
21✔
423
                if (previous) previous.selected = false;
21✔
424
            }
21✔
425
            const next = this.querySelector(
59✔
426
                `[role="tabpanel"][value="${this.selected}"]`
59✔
427
            ) as TabPanel;
59✔
428
            if (next) next.selected = true;
59✔
429
        }
59✔
430
        if (changes.has('direction')) {
253✔
431
            if (this.direction === 'horizontal') {
34✔
432
                this.removeAttribute('aria-orientation');
32✔
433
            } else {
34✔
434
                this.setAttribute('aria-orientation', 'vertical');
2✔
435
            }
2✔
436
        }
34✔
437
        if (changes.has('dir')) {
253✔
438
            this.updateSelectionIndicator();
68✔
439
        }
68✔
440
        if (changes.has('disabled')) {
253✔
441
            if (this.disabled) {
35✔
442
                this.setAttribute('aria-disabled', 'true');
1✔
443
            } else {
35✔
444
                this.removeAttribute('aria-disabled');
34✔
445
            }
34✔
446
        }
35✔
447
        if (
253✔
448
            !this.shouldAnimate &&
253✔
449
            typeof changes.get('shouldAnimate') !== 'undefined'
209✔
450
        ) {
253!
451
            this.shouldAnimate = true;
×
452
        }
×
453
    }
253✔
454

6✔
455
    private onTabsScroll = (): void => {
6✔
456
        this.dispatchEvent(
205✔
457
            new Event('sp-tabs-scroll', {
205✔
458
                bubbles: true,
205✔
459
                composed: true,
205✔
460
            })
205✔
461
        );
205✔
462
    };
205✔
463

6✔
464
    private onClick = (event: Event): void => {
6✔
465
        if (this.disabled) {
13✔
466
            return;
3✔
467
        }
3✔
468
        const target = event
12✔
469
            .composedPath()
12✔
470
            .find((el) => (el as Tab).parentElement === this) as Tab;
12✔
471
        if (!target || target.disabled) {
13✔
472
            return;
4✔
473
        }
4✔
474
        this.shouldAnimate = true;
10✔
475
        this.selectTarget(target);
10✔
476
    };
13✔
477

6✔
478
    private onKeyDown = (event: KeyboardEvent): void => {
6✔
479
        if (event.code === 'Enter' || event.code === 'Space') {
23✔
480
            event.preventDefault();
9✔
481
            const target = event.target as HTMLElement;
9✔
482
            if (target) {
9✔
483
                this.selectTarget(target);
9✔
484
            }
9✔
485
        }
9✔
486
    };
23✔
487

6✔
488
    private selectTarget(target: HTMLElement): void {
6✔
489
        const value = target.getAttribute('value');
18✔
490
        if (value) {
18✔
491
            const selected = this.selected;
17✔
492
            this.selected = value;
17✔
493
            const applyDefault = this.dispatchEvent(
17✔
494
                new Event('change', {
17✔
495
                    cancelable: true,
17✔
496
                })
17✔
497
            );
17✔
498
            if (!applyDefault) {
17✔
499
                this.selected = selected;
1✔
500
            }
1✔
501
        }
17✔
502
    }
18✔
503

6✔
504
    private onSlotChange(): void {
6✔
505
        this.tabs = this.slotEl
34✔
506
            .assignedElements()
34✔
507
            .filter((el) => el.getAttribute('role') === 'tab') as Tab[];
34✔
508
        this.updateCheckedState();
34✔
509
    }
34✔
510

6✔
511
    private updateCheckedState = (): void => {
6✔
512
        this.tabs.forEach((element) => {
61✔
513
            element.removeAttribute('selected');
282✔
514
        });
61✔
515

61✔
516
        if (this.selected) {
61✔
517
            const currentChecked = this.tabs.find(
54✔
518
                (el) => el.value === this.selected
54✔
519
            );
54✔
520

54✔
521
            if (currentChecked) {
54✔
522
                currentChecked.selected = true;
51✔
523
            } else {
54✔
524
                this.selected = '';
5✔
525
            }
5✔
526
        } else {
61✔
527
            const firstTab = this.tabs[0];
9✔
528
            if (firstTab) {
9✔
529
                firstTab.setAttribute('tabindex', '0');
9✔
530
            }
9✔
531
        }
9✔
532

61✔
533
        this.updateSelectionIndicator();
61✔
534
    };
61✔
535

6✔
536
    private updateSelectionIndicator = async (): Promise<void> => {
6✔
537
        const selectedElement = this.tabs.find((el) => el.selected);
245✔
538
        if (!selectedElement) {
245✔
539
            this.selectionIndicatorStyle = ScaledIndicator.noSelectionStyle;
96✔
540
            return;
96✔
541
        }
96✔
542
        await Promise.all([
151✔
543
            selectedElement.updateComplete,
151✔
544
            document.fonts ? document.fonts.ready : Promise.resolve(),
245!
545
        ]);
245✔
546
        const { width, height } = selectedElement.getBoundingClientRect();
151✔
547

151✔
548
        this.selectionIndicatorStyle =
151✔
549
            this.direction === 'horizontal'
151✔
550
                ? ScaledIndicator.transformX(selectedElement.offsetLeft, width)
143✔
551
                : ScaledIndicator.transformY(selectedElement.offsetTop, height);
10✔
552
    };
245✔
553

6✔
554
    public override connectedCallback(): void {
6✔
555
        super.connectedCallback();
34✔
556
        window.addEventListener('resize', this.updateSelectionIndicator);
34✔
557
        if ('fonts' in document) {
34✔
558
            (
34✔
559
                document as unknown as {
34✔
560
                    fonts: {
34✔
561
                        addEventListener: (
34✔
562
                            name: string,
34✔
563
                            callback: () => void
34✔
564
                        ) => void;
34✔
565
                    };
34✔
566
                }
34✔
567
            ).fonts.addEventListener(
34✔
568
                'loadingdone',
34✔
569
                this.updateSelectionIndicator
34✔
570
            );
34✔
571
        }
34✔
572
    }
34✔
573

6✔
574
    public override disconnectedCallback(): void {
6✔
575
        window.removeEventListener('resize', this.updateSelectionIndicator);
34✔
576
        if ('fonts' in document) {
34✔
577
            (
34✔
578
                document as unknown as {
34✔
579
                    fonts: {
34✔
580
                        removeEventListener: (
34✔
581
                            name: string,
34✔
582
                            callback: () => void
34✔
583
                        ) => void;
34✔
584
                    };
34✔
585
                }
34✔
586
            ).fonts.removeEventListener(
34✔
587
                'loadingdone',
34✔
588
                this.updateSelectionIndicator
34✔
589
            );
34✔
590
        }
34✔
591
        super.disconnectedCallback();
34✔
592
    }
34✔
593
}
6✔
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