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

adobe / spectrum-web-components / 18572166522

16 Oct 2025 07:14PM UTC coverage: 97.719% (-0.2%) from 97.957%
18572166522

Pull #5809

github

web-flow
Merge 287fe7bcb into 7f9549f8c
Pull Request #5809: 1042-form-field-mixin

5417 of 5723 branches covered (94.65%)

Branch coverage included in aggregate %.

348 of 396 new or added lines in 4 files covered. (87.88%)

45 existing lines in 2 files now uncovered.

34417 of 35041 relevant lines covered (98.22%)

623.94 hits per line

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

96.1
/packages/combobox/src/Combobox.ts
1
/**
4✔
2
 * Copyright 2025 Adobe. All rights reserved.
4✔
3
 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4✔
4
 * you may not use this file except in compliance with the License. You may obtain a copy
4✔
5
 * of the License at http://www.apache.org/licenses/LICENSE-2.0
4✔
6
 *
4✔
7
 * Unless required by applicable law or agreed to in writing, software distributed under
4✔
8
 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
4✔
9
 * OF ANY KIND, either express or implied. See the License for the specific language
4✔
10
 * governing permissions and limitations under the License.
4✔
11
 */
4✔
12

4✔
13
import {
4✔
14
    CSSResultArray,
4✔
15
    html,
4✔
16
    nothing,
4✔
17
    PropertyValues,
4✔
18
    type SpectrumElement,
4✔
19
    TemplateResult,
4✔
20
} from '@spectrum-web-components/base';
4✔
21
import {
4✔
22
    property,
4✔
23
    query,
4✔
24
    state,
4✔
25
} from '@spectrum-web-components/base/src/decorators.js';
4✔
26
import {
4✔
27
    ifDefined,
4✔
28
    live,
4✔
29
    repeat,
4✔
30
} from '@spectrum-web-components/base/src/directives.js';
4✔
31
import '@spectrum-web-components/icons-ui/icons/sp-icon-chevron100.js';
4✔
32
import '@spectrum-web-components/menu/sp-menu-item.js';
4✔
33
import '@spectrum-web-components/menu/sp-menu.js';
4✔
34
import '@spectrum-web-components/overlay/sp-overlay.js';
4✔
35
import '@spectrum-web-components/picker-button/sp-picker-button.js';
4✔
36
import '@spectrum-web-components/progress-circle/sp-progress-circle.js';
4✔
37
import '@spectrum-web-components/popover/sp-popover.js';
4✔
38
import { Textfield } from '@spectrum-web-components/textfield';
4✔
39
import { FieldLabelMixin } from '@spectrum-web-components/field-label/src/FieldLabelMixin.js';
4✔
40
import type { Tooltip } from '@spectrum-web-components/tooltip';
4✔
41

4✔
42
import chevronStyles from '@spectrum-web-components/icon/src/spectrum-icon-chevron.css.js';
4✔
43
import { Menu, MenuItem } from '@spectrum-web-components/menu';
4✔
44
import styles from './combobox.css.js';
4✔
45

4✔
46
export type ComboboxOption = {
4✔
47
    value: string;
4✔
48
    itemText: string;
4✔
49
    disabled?: boolean;
4✔
50
};
4✔
51

4✔
52
/**
4✔
53
 * @element sp-combobox
4✔
54
 * @slot - Supply Menu Item elements to the default slot in order to populate the available options
4✔
55
 * @slot tooltip - Tooltip to to be applied to the the Picker Button
4✔
56
 */
4✔
57
export class Combobox extends FieldLabelMixin(Textfield, 'field-label') {
4✔
58
    public static override get styles(): CSSResultArray {
68✔
59
        return [...super.styles, styles, chevronStyles];
68✔
60
    }
68✔
61

68✔
62
    /**
68✔
63
     * The currently active ComboboxItem descendant, when available.
68✔
64
     */
68✔
65
    @state()
68✔
66
    private activeDescendant?: ComboboxOption | MenuItem;
68✔
67

68✔
68
    @property({ type: String })
68✔
69
    public override autocomplete: 'list' | 'none' = 'none';
68✔
70

68✔
71
    @state()
68✔
72
    private availableOptions: (ComboboxOption | MenuItem)[] = [];
68✔
73

68✔
74
    /**
68✔
75
     * Whether the listbox is visible.
68✔
76
     **/
68✔
77
    @property({ type: Boolean, reflect: true })
68✔
78
    public open = false;
68✔
79

68✔
80
    /** Whether the items are currently loading. */
68✔
81
    @property({ type: Boolean, reflect: true })
68✔
82
    public pending = false;
68✔
83

68✔
84
    /** Defines a string value that labels the Combobox while it is in pending state. */
68✔
85
    @property({ type: String, attribute: 'pending-label' })
68✔
86
    public pendingLabel = 'Pending';
68✔
87

68✔
88
    @query('slot:not([name])')
68✔
89
    private optionSlot!: HTMLSlotElement;
68✔
90

68✔
91
    @state()
68✔
92
    overlayOpen = false;
68✔
93

68✔
94
    @query('#input')
68✔
95
    private input!: HTMLInputElement;
68✔
96

68✔
97
    private itemValue = '';
68✔
98

68✔
99
    /**
68✔
100
     * An array of options to present in the Menu provided while typing into the input
68✔
101
     */
68✔
102
    @property({ type: Array })
68✔
103
    public options?: (ComboboxOption | MenuItem)[];
68✔
104

68✔
105
    /**
68✔
106
     * The array of the children of the combobox, ie ComboboxItems.
68✔
107
     **/
68✔
108
    @state()
68✔
109
    protected optionEls: MenuItem[] = [];
68✔
110

68✔
111
    private tooltipEl?: Tooltip;
68✔
112

68✔
113
    private resizeObserver?: ResizeObserver | undefined;
68✔
114

68✔
115
    @state()
68✔
116
    private fieldWidth = 0;
68✔
117

68✔
118
    public override focus(): void {
68✔
119
        this.focusElement.focus();
69✔
120
    }
69✔
121

68✔
122
    public override click(): void {
68✔
123
        this.focus();
12✔
124
        this.focusElement.click();
12✔
125
    }
12✔
126

68✔
127
    private scrollToActiveDescendant(): void {
68✔
128
        if (!this.activeDescendant) {
33✔
129
            return;
×
130
        }
×
131
        const activeEl = this.shadowRoot.getElementById(
33✔
132
            this.activeDescendant.value
33✔
133
        );
33✔
134
        if (activeEl) {
33✔
135
            activeEl.scrollIntoView({ block: 'nearest' });
20✔
136
        }
20✔
137
    }
33✔
138

68✔
139
    public handleComboboxKeydown(event: KeyboardEvent): void {
68✔
140
        if (this.readonly || this.pending) {
56✔
141
            return;
2✔
142
        }
2✔
143
        if (event.altKey && event.code === 'ArrowDown') {
56✔
144
            this.open = true;
1✔
145
        } else if (event.code === 'ArrowDown') {
56✔
146
            event.preventDefault();
24✔
147
            this.open = true;
24✔
148
            this.activateNextDescendant();
24✔
149
            this.scrollToActiveDescendant();
24✔
150
        } else if (event.code === 'ArrowUp') {
52✔
151
            event.preventDefault();
9✔
152
            this.open = true;
9✔
153
            this.activatePreviousDescendant();
9✔
154
            this.scrollToActiveDescendant();
9✔
155
        } else if (event.code === 'Escape') {
29✔
156
            if (!this.open) {
2✔
157
                this.value = '';
1✔
158
            }
1✔
159
            this.open = false;
2✔
160
        } else if (event.code === 'Enter') {
20✔
161
            this.selectDescendant();
3✔
162
            this.open = false;
3✔
163
        } else if (event.code === 'Home') {
18✔
164
            this.focusElement.setSelectionRange(0, 0);
1✔
165
            this.activeDescendant = undefined;
1✔
166
        } else if (event.code === 'End') {
15✔
167
            const { length } = this.value;
1✔
168
            this.focusElement.setSelectionRange(length, length);
1✔
169
            this.activeDescendant = undefined;
1✔
170
        } else if (event.code === 'ArrowLeft') {
14✔
171
            this.activeDescendant = undefined;
3✔
172
        } else if (event.code === 'ArrowRight') {
13✔
173
            this.activeDescendant = undefined;
1✔
174
        }
1✔
175
    }
56✔
176

68✔
177
    /**
68✔
178
     * Convert the flattened array of assigned elements of `slot[name='option']` to
68✔
179
     * an array of `ComboboxOptions` for use in rendering options in the shadow DOM.s
68✔
180
     **/
68✔
181
    public handleSlotchange(): void {
68✔
182
        this.setOptionsFromSlottedItems();
70✔
183
        this.itemObserver.disconnect();
70✔
184
        this.optionEls.map((item) => {
70✔
185
            this.itemObserver.observe(item, {
620✔
186
                attributes: true,
620✔
187
                attributeFilter: ['id'],
620✔
188
                childList: true,
620✔
189
            });
620✔
190
        });
70✔
191
    }
70✔
192

68✔
193
    protected handleTooltipSlotchange(
68✔
194
        event: Event & { target: HTMLSlotElement }
2✔
195
    ): void {
2✔
196
        this.tooltipEl = event.target.assignedElements()[0] as
2✔
197
            | Tooltip
2✔
198
            | undefined;
2✔
199
    }
2✔
200

68✔
201
    public setOptionsFromSlottedItems(): void {
68✔
202
        const elements = this.optionSlot.assignedElements({
75✔
203
            flatten: true,
75✔
204
        }) as MenuItem[];
75✔
205
        // Element data
75✔
206
        this.optionEls = elements;
75✔
207
    }
75✔
208

68✔
209
    public activateNextDescendant(): void {
68✔
210
        const activeIndex = !this.activeDescendant
24✔
211
            ? -1
16✔
212
            : this.availableOptions.indexOf(this.activeDescendant);
8✔
213
        let nextActiveIndex = activeIndex;
24✔
214
        do {
24✔
215
            nextActiveIndex =
25✔
216
                (this.availableOptions.length + nextActiveIndex + 1) %
25✔
217
                this.availableOptions.length;
25✔
218
            // Break if we've checked all options to avoid infinite loop
25✔
219
            if (nextActiveIndex === activeIndex) break;
25✔
220
        } while (this.availableOptions[nextActiveIndex].disabled);
24✔
221

24✔
222
        if (!this.availableOptions[nextActiveIndex].disabled) {
24✔
223
            this.activeDescendant = this.availableOptions[nextActiveIndex];
24✔
224
        }
24✔
225
        this.optionEls.forEach((el) =>
24✔
226
            el.setAttribute(
249✔
227
                'aria-selected',
249✔
228
                el.value === this.activeDescendant?.value ? 'true' : 'false'
249✔
229
            )
249✔
230
        );
24✔
231
    }
24✔
232

68✔
233
    public activatePreviousDescendant(): void {
68✔
234
        const activeIndex = !this.activeDescendant
9✔
235
            ? 0
4✔
236
            : this.availableOptions.indexOf(this.activeDescendant);
5✔
237
        let previousActiveIndex = activeIndex;
9✔
238
        do {
9✔
239
            previousActiveIndex =
9✔
240
                (this.availableOptions.length + previousActiveIndex - 1) %
9✔
241
                this.availableOptions.length;
9✔
242
            // Break if we've checked all options to avoid infinite loop
9✔
243
            if (previousActiveIndex === activeIndex) break;
9✔
244
        } while (this.availableOptions[previousActiveIndex].disabled);
9✔
245

9✔
246
        if (!this.availableOptions[previousActiveIndex].disabled) {
9✔
247
            this.activeDescendant = this.availableOptions[previousActiveIndex];
9✔
248
        }
9✔
249
        this.optionEls.forEach((el) =>
9✔
250
            el.setAttribute(
×
251
                'aria-selected',
×
252
                el.value === this.activeDescendant?.value ? 'true' : 'false'
×
253
            )
×
254
        );
9✔
255
    }
9✔
256

68✔
257
    public selectDescendant(): void {
68✔
258
        if (!this.activeDescendant) {
3✔
259
            return;
1✔
260
        }
1✔
261

2✔
262
        const activeEl = this.shadowRoot.getElementById(
2✔
263
            this.activeDescendant.value
2✔
264
        );
2✔
265
        if (activeEl) {
2✔
266
            activeEl.click();
2✔
267
        }
2✔
268
    }
3✔
269

68✔
270
    public filterAvailableOptions(): void {
68✔
271
        if (this.autocomplete === 'none' || this.pending) {
94✔
272
            return;
21✔
273
        }
21✔
274
        const valueLowerCase = this.value.toLowerCase();
73✔
275
        this.availableOptions = (this.options || this.optionEls).filter(
94!
276
            (descendant) => {
94✔
277
                const itemTextLowerCase = descendant.itemText.toLowerCase();
2,036✔
278
                return itemTextLowerCase.startsWith(valueLowerCase);
2,036✔
279
            }
2,036✔
280
        );
94✔
281
    }
94✔
282

68✔
283
    public override handleInput(event: Event): void {
68✔
284
        super.handleInput(event);
8✔
285
        if (!this.pending) {
8✔
286
            this.activeDescendant = undefined;
7✔
287
            this.open = true;
7✔
288
        }
7✔
289
    }
8✔
290

68✔
291
    protected handleMenuChange(event: PointerEvent & { target: Menu }): void {
68✔
292
        const { target } = event;
5✔
293
        const selected = (this.options || this.optionEls).find(
5✔
294
            (item) => item.value === target?.value
5✔
295
        );
5✔
296
        this.value = selected?.itemText || '';
5✔
297
        event.preventDefault();
5✔
298
        this.open = false;
5✔
299
        this._returnItems();
5✔
300
        this.focus();
5✔
301
    }
5✔
302

68✔
303
    public handleClosed(): void {
68✔
304
        this.open = false;
40✔
305
        this.overlayOpen = false;
40✔
306
    }
40✔
307

68✔
308
    public handleOpened(): void {
68✔
309
        // Do stuff here?
26✔
310
    }
26✔
311

68✔
312
    public toggleOpen(): void {
68✔
313
        if (this.readonly || this.pending) {
22✔
314
            this.open = false;
1✔
315
            return;
1✔
316
        }
1✔
317
        this.open = !this.open;
21✔
318
        this.inputElement.focus();
21✔
319
    }
22✔
320

68✔
321
    protected override shouldUpdate(
68✔
322
        changed: PropertyValues<this & { optionEls: MenuItem[] }>
503✔
323
    ): boolean {
503✔
324
        if (changed.has('open')) {
503✔
325
            if (!this.open) {
155✔
326
                this.activeDescendant = undefined;
112✔
327
            } else {
155✔
328
                this.overlayOpen = true;
43✔
329
            }
43✔
330
        }
155✔
331
        if (changed.has('value')) {
503✔
332
            this.filterAvailableOptions();
94✔
333
            this.itemValue =
94✔
334
                this.availableOptions.find(
94✔
335
                    (option) => option.itemText === this.value
94✔
336
                )?.value ?? '';
80✔
337
        }
94✔
338
        return super.shouldUpdate(changed);
503✔
339
    }
503✔
340

68✔
341
    protected override onBlur(event: FocusEvent): void {
68✔
342
        if (
53✔
343
            event.relatedTarget &&
53✔
344
            (this.contains(event.relatedTarget as HTMLElement) ||
6✔
345
                this.shadowRoot.contains(event.relatedTarget as HTMLElement))
6✔
346
        ) {
53✔
347
            return;
4✔
348
        }
4✔
349
        super.onBlur(event);
49✔
350
    }
53✔
351

68✔
352
    /**
68✔
353
     * gets the hidden label for the combobox:
68✔
354
     * appliedLabel corresponds to `<label for="...">`, which is overriden
68✔
355
     * if user adds the `label` attribute manually to `<sp-combobox>`.
68✔
356
     **/
68✔
357
    protected get visuallyHiddenLabel(): string | undefined {
68✔
NEW
358
        //TODO Deprecate applied label
×
NEW
359
        const label = this.label || this.appliedLabel;
×
NEW
360
        return label && label.trim().length > 0 ? label : undefined;
×
NEW
361
    }
×
362

68✔
363
    protected renderVisuallyHiddenLabels(): TemplateResult {
68✔
NEW
364
        //TODO Deprecate applied label
×
UNCOV
365
        return html`
×
UNCOV
366
            ${this.pending
×
UNCOV
367
                ? html`
×
UNCOV
368
                      ${this.renderLoader()}
×
UNCOV
369
                  `
×
UNCOV
370
                : nothing}
×
UNCOV
371
        `;
×
UNCOV
372
    }
×
373

68✔
374
    protected renderLoader(): TemplateResult {
68✔
375
        import(
17✔
376
            '@spectrum-web-components/progress-circle/sp-progress-circle.js'
17✔
377
        );
17✔
378
        return html`
17✔
379
            <sp-progress-circle
17✔
380
                size="s"
17✔
381
                indeterminate
17✔
382
                role="presentation"
17✔
383
                class="progress-circle"
17✔
384
            ></sp-progress-circle>
17✔
385
        `;
17✔
386
    }
17✔
387

68✔
388
    protected override get _ariaLabel(): string | undefined {
68✔
389
        const pending = this.pending ? this.pendingLabel : undefined;
1,006✔
390
        if (this.appliedLabel && this.appliedLabel.trim().length > 0) {
1,006!
NEW
391
            //TODO Deprecate applied label
×
NEW
392
            return `${this.appliedLabel}${this.pending ? ` ${this.pendingLabel}` : ''}`;
×
393
        } else if (this.label && this.label.trim().length > 0) {
1,006✔
394
            return `${this.label}${this.pending ? ` ${this.pendingLabel}` : ''}`;
858✔
395
        } else if (this.slotHasContent) {
1,006✔
396
            return pending;
82✔
397
        } else {
148✔
398
            window.__swc.warn(
66✔
399
                this,
66✔
400
                `<${this.localName}> needs a label:`,
66✔
401
                'https://opensource.adobe.com/spectrum-web-components/components/textfield/#accessibility',
66✔
402
                {
66✔
403
                    type: 'accessibility',
66✔
404
                    issues: [
66✔
405
                        'value supplied to the default slot, which will be displayed visually as part of the element, or',
66✔
406
                        'value supplied to the "label" attribute, which will read by assistive technologies',
66✔
407
                    ],
66✔
408
                }
66✔
409
            );
66✔
410
            return pending;
66✔
411
        }
66✔
412
    }
1,006✔
413

68✔
414
    protected override renderField(): TemplateResult {
68✔
415
        return html`
503✔
416
            ${this.renderStateIcons()}
503✔
417
            <input
503✔
418
                aria-activedescendant=${ifDefined(
503✔
419
                    this.activeDescendant
503✔
420
                        ? `${this.activeDescendant.value}`
45✔
421
                        : undefined
458✔
422
                )}
503✔
423
                aria-autocomplete=${ifDefined(
503✔
424
                    this.autocomplete as 'list' | 'none'
503✔
425
                )}
503✔
426
                aria-controls=${ifDefined(
503✔
427
                    this.open ? 'listbox-menu' : undefined
503✔
428
                )}
503✔
429
                aria-label=${ifDefined(this._ariaLabel)}
503✔
430
                aria-labelledby=${ifDefined(
503✔
431
                    this.slotHasContent ? 'field-label-slot' : undefined
503✔
432
                )}
503✔
433
                aria-describedby="${this.helpTextId} tooltip"
503✔
434
                aria-expanded="${this.open ? 'true' : 'false'}"
503✔
435
                aria-invalid=${ifDefined(this.invalid || undefined)}
503✔
436
                autocomplete="off"
503✔
437
                @click=${this.toggleOpen}
503✔
438
                @keydown=${this.handleComboboxKeydown}
503✔
439
                id="input"
503✔
440
                class="input"
503✔
441
                role="combobox"
503✔
442
                type="text"
503✔
443
                .value=${live(this.displayValue)}
503✔
444
                tabindex="0"
503✔
445
                @sp-closed=${this.handleClosed}
503✔
446
                @sp-opened=${this.handleOpened}
503✔
447
                maxlength=${ifDefined(
503✔
448
                    this.maxlength > -1 ? this.maxlength : undefined
503!
449
                )}
503✔
450
                minlength=${ifDefined(
503✔
451
                    this.minlength > -1 ? this.minlength : undefined
503!
452
                )}
503✔
453
                pattern=${ifDefined(this.pattern)}
503✔
454
                placeholder=${ifDefined(
503✔
455
                    this.placeholder?.length > 0 && !this.pending
503!
456
                        ? this.placeholder
5✔
457
                        : undefined
498✔
458
                )}
503✔
459
                @change=${this.handleChange}
503✔
460
                @input=${this.handleInput}
503✔
461
                @focus=${this.onFocus}
503✔
462
                @blur=${this.onBlur}
503✔
463
                ?disabled=${this.disabled}
503✔
464
                ?required=${this.required}
503✔
465
                ?readonly=${this.readonly}
503✔
466
            />
503✔
467
        `;
503✔
468
    }
503✔
469

68✔
470
    protected override render(): TemplateResult {
68✔
471
        if (this.tooltipEl) {
503✔
472
            this.tooltipEl.disabled = this.open;
13✔
473
        }
13✔
474

503✔
475
        return html`
503✔
476
            ${this.renderFieldLabel('input')}
503✔
477
            <div id="textfield">${this.renderField()}</div>
503✔
478
            ${this.renderHelpText(this.invalid)}
503✔
479
            <sp-picker-button
503✔
480
                aria-controls="listbox-menu"
503✔
481
                aria-describedby="${this.helpTextId} tooltip"
503✔
482
                aria-expanded=${this.open ? 'true' : 'false'}
503✔
483
                aria-label=${ifDefined(this._ariaLabel)}
503✔
484
                aria-labelledby=${ifDefined(
503✔
485
                    this.slotHasContent ? 'field-label-slot' : undefined
503✔
486
                )}
503✔
487
                @click=${this.toggleOpen}
503✔
488
                tabindex="-1"
503✔
489
                class="button ${this.focused
503✔
490
                    ? 'focus-visible is-keyboardFocused'
154✔
491
                    : ''}"
503✔
492
                ?disabled=${this.disabled}
503✔
493
                ?focused=${this.focused}
503✔
494
                ?quiet=${this.quiet}
503✔
495
                size=${this.size}
503✔
496
            ></sp-picker-button>
503✔
497
            <sp-overlay
503✔
498
                ?open=${this.open}
503✔
499
                .triggerElement=${this.input}
503✔
500
                offset="0"
503✔
501
                placement="bottom-start"
503✔
502
                .receivesFocus=${'false'}
503✔
503
                role="presentation"
503✔
504
            >
503✔
505
                <sp-popover
503✔
506
                    id="listbox"
503✔
507
                    ?open=${this.open}
503✔
508
                    role="presentation"
503✔
509
                    ?hidden=${this.availableOptions.length === 0}
503✔
510
                >
503✔
511
                    <sp-menu
503✔
512
                        @change=${this.handleMenuChange}
503✔
513
                        tabindex="-1"
503✔
514
                        aria-labelledby="label visually-hidden-label"
503✔
515
                        aria-label=${ifDefined(this.label || this.appliedLabel)}
503✔
516
                        id="listbox-menu"
503✔
517
                        role="listbox"
503✔
518
                        selects=${ifDefined(
503✔
519
                            this.autocomplete === 'none' ? 'single' : undefined
503✔
520
                        )}
503✔
521
                        .selected=${this.autocomplete === 'none' &&
503✔
522
                        this.itemValue
101✔
523
                            ? [this.itemValue]
6✔
524
                            : []}
503✔
525
                        style="min-width: ${this.fieldWidth}px;"
503✔
526
                        size=${this.size}
503✔
527
                    >
503✔
528
                        ${this.overlayOpen
503✔
529
                            ? repeat(
164✔
530
                                  this.availableOptions,
164✔
531
                                  (option) => option.value,
164✔
532
                                  (option) => {
164✔
533
                                      return html`
7,192✔
534
                                          <sp-menu-item
7,192✔
535
                                              id="${option.value}"
7,192✔
536
                                              ?focused=${this.activeDescendant
7,192✔
537
                                                  ?.value === option.value}
7,192✔
538
                                              aria-selected=${this
7,192✔
539
                                                  .activeDescendant?.value ===
1,594✔
540
                                              option.value
7,192✔
541
                                                  ? 'true'
45✔
542
                                                  : 'false'}
7,192✔
543
                                              .value=${option.value}
7,192✔
544
                                              .selected=${option.value ===
7,192✔
545
                                              this.itemValue}
7,192✔
546
                                              ?disabled=${option.disabled}
7,192✔
547
                                          >
7,192✔
548
                                              ${option.itemText}
7,192✔
549
                                          </sp-menu-item>
7,192✔
550
                                      `;
7,192✔
551
                                  }
7,192✔
552
                              )
164✔
553
                            : html``}
503✔
554
                        <slot
503✔
555
                            hidden
503✔
556
                            @slotchange=${this.handleSlotchange}
503✔
557
                        ></slot>
503✔
558
                    </sp-menu>
503✔
559
                </sp-popover>
503✔
560
            </sp-overlay>
503✔
561
            ${this.pending
503✔
562
                ? html`
17✔
563
                      ${this.renderLoader()}
17✔
564
                  `
486✔
565
                : nothing}
503✔
566
            <slot
503✔
567
                aria-hidden="true"
503✔
568
                name="tooltip"
503✔
569
                id="tooltip"
503✔
570
                @slotchange=${this.handleTooltipSlotchange}
503✔
571
            ></slot>
503✔
572
        `;
503✔
573
    }
503✔
574

68✔
575
    applyFocusElementLabel = (value?: string): void => {
68✔
UNCOV
576
        this.appliedLabel = value;
×
UNCOV
577
    };
×
578

68✔
579
    protected override firstUpdated(
68✔
580
        changed: PropertyValues<this & { optionEls: MenuItem[] }>
68✔
581
    ): void {
68✔
582
        super.firstUpdated(changed);
68✔
583
        this.addEventListener('focusout', (event: FocusEvent) => {
68✔
584
            const isMenuItem =
49✔
585
                event.relatedTarget &&
49✔
586
                this.contains(event.relatedTarget as Node);
2✔
587
            if (event.target === this && !isMenuItem) {
49✔
588
                this.focused = false;
49✔
589
            }
49✔
590
        });
68✔
591
        this.resizeObserver = new ResizeObserver((entries) => {
68✔
592
            this.fieldWidth = entries[0].borderBoxSize[0].inlineSize;
68✔
593
        });
68✔
594

68✔
595
        this.resizeObserver.observe(this);
68✔
596
    }
68✔
597

68✔
598
    private _returnItems = (): void => {
68✔
599
        return;
5✔
600
    };
5✔
601

4✔
602
    protected async manageListOverlay(): Promise<void> {
4✔
603
        if (this.open) {
155✔
604
            this.focused = true;
43✔
605
            this.focus();
43✔
606
        }
43✔
607
    }
155✔
608

4✔
609
    protected override updated(
4✔
610
        changed: PropertyValues<
503✔
611
            this & { optionEls: MenuItem[]; activeDescendant: MenuItem }
503✔
612
        >
503✔
613
    ): void {
503✔
614
        if (changed.has('open') && !this.pending) {
503✔
615
            this.manageListOverlay();
155✔
616
        }
155✔
617
        if (!this.focused && this.open) {
503✔
618
            this.open = false;
1✔
619
        }
1✔
620
        if (changed.has('pending') && this.pending) {
503✔
621
            this.open = false;
5✔
622
        }
5✔
623
        if (changed.has('activeDescendant')) {
503✔
624
            const previouslyActiveDescendant = changed.get(
52✔
625
                'activeDescendant'
52✔
626
            ) as unknown as MenuItem;
52✔
627
            if (previouslyActiveDescendant) {
52✔
628
                previouslyActiveDescendant.focused = false;
32✔
629
            }
32✔
630
            if (
52✔
631
                this.activeDescendant &&
52✔
632
                typeof (this.activeDescendant as MenuItem).focused !==
32✔
633
                    'undefined'
32✔
634
            ) {
52✔
635
                (this.activeDescendant as MenuItem).focused = true;
18✔
636
            }
18✔
637
        }
52✔
638
        if (changed.has('options') || changed.has('optionEls')) {
503✔
639
            // if all options are disabled, set combobox to disabled
144✔
640
            if (this.options?.every((option) => option.disabled)) {
144!
641
                this.disabled = true;
×
642
            }
×
643

144✔
644
            this.availableOptions = this.options || this.optionEls;
144✔
645
        }
144✔
646
    }
503✔
647

4✔
648
    protected override async getUpdateComplete(): Promise<boolean> {
4✔
649
        const complete = await super.getUpdateComplete();
434✔
650
        const list = this.shadowRoot.querySelector(
434✔
651
            '#listbox'
434✔
652
        ) as HTMLUListElement;
434✔
653
        if (list) {
434✔
654
            const descendants = [...list.children] as SpectrumElement[];
434✔
655
            await Promise.all(
434✔
656
                descendants.map((descendant) => descendant.updateComplete)
434✔
657
            );
434✔
658
        }
434✔
659
        return complete;
434✔
660
    }
434✔
661

4✔
662
    public override connectedCallback(): void {
4✔
663
        super.connectedCallback();
68✔
664
        if (!this.itemObserver) {
68✔
665
            this.itemObserver = new MutationObserver(
68✔
666
                this.setOptionsFromSlottedItems.bind(this)
68✔
667
            );
68✔
668
        }
68✔
669
    }
68✔
670

4✔
671
    public override disconnectedCallback(): void {
4✔
672
        this.itemObserver.disconnect();
68✔
673
        this.open = false;
68✔
674
        this.resizeObserver?.disconnect();
68!
675
        this.resizeObserver = undefined;
68✔
676
        super.disconnectedCallback();
68✔
677
    }
68✔
678

4✔
679
    private itemObserver!: MutationObserver;
4✔
680
}
4✔
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