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

adobe / spectrum-web-components / 12282443963

11 Dec 2024 06:17PM UTC coverage: 97.436% (-0.8%) from 98.206%
12282443963

Pull #4989

github

web-flow
Merge 92e762395 into 5b674b468
Pull Request #4989: chore: testing something

4996 of 5296 branches covered (94.34%)

Branch coverage included in aggregate %.

7 of 19 new or added lines in 1 file covered. (36.84%)

257 existing lines in 8 files now uncovered.

32739 of 33432 relevant lines covered (97.93%)

380.42 hits per line

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

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

38✔
7
Unless required by applicable law or agreed to in writing, software distributed under
38✔
8
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
38✔
9
OF ANY KIND, either express or implied. See the License for the specific language
38✔
10
governing permissions and limitations under the License.
38✔
11
*/
38✔
12
import {
38✔
13
    html,
38✔
14
    PropertyValues,
38✔
15
    TemplateResult,
38✔
16
} from '@spectrum-web-components/base';
38✔
17
import {
38✔
18
    property,
38✔
19
    query,
38✔
20
    queryAssignedElements,
38✔
21
    state,
38✔
22
} from '@spectrum-web-components/base/src/decorators.js';
38✔
23
import {
38✔
24
    ElementResolutionController,
38✔
25
    elementResolverUpdatedSymbol,
38✔
26
} from '@spectrum-web-components/reactive-controllers/src/ElementResolution.js';
38✔
27
import {
38✔
28
    ifDefined,
38✔
29
    StyleInfo,
38✔
30
    styleMap,
38✔
31
} from '@spectrum-web-components/base/src/directives.js';
38✔
32
import { randomID } from '@spectrum-web-components/shared/src/random-id.js';
38✔
33
import type {
38✔
34
    OpenableElement,
38✔
35
    OverlayState,
38✔
36
    OverlayTypes,
38✔
37
    Placement,
38✔
38
    TriggerInteraction,
38✔
39
} from './overlay-types.js';
38✔
40
import { AbstractOverlay, nextFrame } from './AbstractOverlay.js';
38✔
41
import { OverlayDialog } from './OverlayDialog.js';
38✔
42
import { OverlayPopover } from './OverlayPopover.js';
38✔
43
import { OverlayNoPopover } from './OverlayNoPopover.js';
38✔
44
import { overlayStack } from './OverlayStack.js';
38✔
45
import { VirtualTrigger } from './VirtualTrigger.js';
38✔
46
import { PlacementController } from './PlacementController.js';
38✔
47
import type { ClickController } from './ClickController.js';
38✔
48
import type { HoverController } from './HoverController.js';
38✔
49
import type { LongpressController } from './LongpressController.js';
38✔
50
export { LONGPRESS_INSTRUCTIONS } from './LongpressController.js';
38✔
51
import { strategies } from './strategies.js';
38✔
52
import {
38✔
53
    removeSlottableRequest,
38✔
54
    SlottableRequestEvent,
38✔
55
} from './slottable-request-event.js';
38✔
56

38✔
57
import styles from './overlay.css.js';
38✔
58

38✔
59
const browserSupportsPopover = 'showPopover' in document.createElement('div');
38✔
60

38✔
61
// Start the base class and add the popover or no-popover functionality
38✔
62
let ComputedOverlayBase = OverlayDialog(AbstractOverlay);
38✔
63

38✔
64
if (browserSupportsPopover) {
38✔
65
    ComputedOverlayBase = OverlayPopover(ComputedOverlayBase);
38✔
66
} else {
×
67
    ComputedOverlayBase = OverlayNoPopover(ComputedOverlayBase);
×
68
}
×
69

38✔
70
/**
38✔
71
 * @element sp-overlay
38✔
72
 *
38✔
73
 * @fires sp-opened - announces that an overlay has completed any entry animations
38✔
74
 * @fires sp-closed - announce that an overlay has compelted any exit animations
38✔
75
 * @fires slottable-request - requests to add or remove slottable content
38✔
76
 */
38✔
77
export class Overlay extends ComputedOverlayBase {
38✔
78
    static override styles = [styles];
603✔
79

603✔
80
    /**
603✔
81
     * An Overlay that is `delayed` will wait until a warm-up period of 1000ms
603✔
82
     * has completed before opening. Once the warm-up period has completed, all
603✔
83
     * subsequent Overlays will open immediately. When no Overlays are opened,
603✔
84
     * a cool-down period of 1000ms will begin. Once the cool-down has completed,
603✔
85
     * the next Overlay to be opened will be subject to the warm-up period if
603✔
86
     * provided that option.
603✔
87
     *
603✔
88
     * This behavior helps to manage the performance and user experience by
603✔
89
     * preventing multiple overlays from opening simultaneously and ensuring
603✔
90
     * a smooth transition between opening and closing overlays.
603✔
91
     *
603✔
92
     * @type {boolean}
603✔
93
     * @default false
603✔
94
     */
603✔
95
    @property({ type: Boolean })
603✔
96
    override get delayed(): boolean {
603✔
97
        return this.elements.at(-1)?.hasAttribute('delayed') || this._delayed;
603✔
98
    }
603✔
99

603✔
100
    override set delayed(delayed: boolean) {
603✔
101
        this._delayed = delayed;
64✔
102
    }
64✔
103

603✔
104
    private _delayed = false;
603✔
105

603✔
106
    /**
603✔
107
     * A reference to the dialog element within the overlay.
603✔
108
     * This element is expected to have `showPopover` and `hidePopover` methods.
603✔
109
     */
603✔
110
    @query('.dialog')
603✔
111
    override dialogEl!: HTMLDialogElement & {
603✔
112
        showPopover(): void;
603✔
113
        hidePopover(): void;
603✔
114
    };
603✔
115

603✔
116
    /**
603✔
117
     * Indicates whether the overlay is currently functional or not.
603✔
118
     *
603✔
119
     * When set to `true`, the overlay is disabled, and any active strategy is aborted.
603✔
120
     * The overlay will also close if it is currently open. When set to `false`, the
603✔
121
     * overlay will re-bind events and re-open if it was previously open.
603✔
122
     *
603✔
123
     * @type {boolean}
603✔
124
     * @default false
603✔
125
     */
603✔
126
    @property({ type: Boolean })
603✔
127
    override get disabled(): boolean {
603✔
128
        return this._disabled;
1,534✔
129
    }
1,534✔
130

603✔
131
    override set disabled(disabled: boolean) {
603✔
132
        this._disabled = disabled;
10✔
133
        if (disabled) {
10✔
134
            // Abort any active strategy and close the overlay if it is currently open
5✔
135
            this.strategy?.abort();
5✔
136
            this.wasOpen = this.open;
5✔
137
            this.open = false;
5✔
138
        } else {
10✔
139
            // Re-bind events and re-open the overlay if it was previously open
5✔
140
            this.bindEvents();
5✔
141
            this.open = this.open || this.wasOpen;
5✔
142
            this.wasOpen = false;
5✔
143
        }
5✔
144
    }
10✔
145

603✔
146
    private _disabled = false;
603✔
147

603✔
148
    /**
603✔
149
     * A query to gather all elements slotted into the default slot, excluding elements
603✔
150
     * with the slot name "longpress-describedby-descriptor".
603✔
151
     */
603✔
152
    @queryAssignedElements({
603✔
153
        flatten: true,
603✔
154
        selector: ':not([slot="longpress-describedby-descriptor"], slot)',
603✔
155
    })
603✔
156
    override elements!: OpenableElement[];
603✔
157

603✔
158
    /**
603✔
159
     * A reference to the parent overlay that should be force-closed, if any.
603✔
160
     */
603✔
161
    public parentOverlayToForceClose?: Overlay;
603✔
162

603✔
163
    /**
603✔
164
     * Determines if the overlay has a non-virtual trigger element.
603✔
165
     *
603✔
166
     * @returns {boolean} `true` if the trigger element is not a virtual trigger, otherwise `false`.
603✔
167
     */
603✔
168
    private get hasNonVirtualTrigger(): boolean {
603✔
169
        return (
1,720✔
170
            !!this.triggerElement &&
1,720✔
171
            !(this.triggerElement instanceof VirtualTrigger)
1,244✔
172
        );
1,720✔
173
    }
1,720✔
174

603✔
175
    /**
603✔
176
     * The `offset` property accepts either a single number to define the offset of the
603✔
177
     * Overlay along the main axis from the trigger, or a 2-tuple to define the offset
603✔
178
     * along both the main axis and the cross axis. This option has no effect when there
603✔
179
     * is no trigger element.
603✔
180
     *
603✔
181
     * @type {number | [number, number]}
603✔
182
     * @default 0
603✔
183
     */
603✔
184
    @property({ type: Number })
603✔
185
    override offset: number | [number, number] = 0;
603✔
186

603✔
187
    /**
603✔
188
     * Provides an instance of the `PlacementController` for managing the positioning
603✔
189
     * of the overlay relative to its trigger element.
603✔
190
     *
603✔
191
     * If the `PlacementController` instance does not already exist, it is created and
603✔
192
     * assigned to the `_placementController` property.
603✔
193
     *
603✔
194
     * @protected
603✔
195
     * @returns {PlacementController} The `PlacementController` instance.
603✔
196
     */
603✔
197
    protected override get placementController(): PlacementController {
603✔
198
        if (!this._placementController) {
1,117✔
199
            this._placementController = new PlacementController(this);
589✔
200
        }
589✔
201
        return this._placementController;
1,117✔
202
    }
1,117✔
203

603✔
204
    /**
603✔
205
     * Indicates whether the Overlay is projected onto the "top layer" or not.
603✔
206
     *
603✔
207
     * When set to `true`, the overlay is open and visible. When set to `false`, the overlay is closed and hidden.
603✔
208
     *
603✔
209
     * @type {boolean}
603✔
210
     * @default false
603✔
211
     */
603✔
212
    @property({ type: Boolean, reflect: true })
603✔
213
    override get open(): boolean {
603✔
214
        return this._open;
57,404✔
215
    }
57,404✔
216

603✔
217
    override set open(open: boolean) {
603✔
218
        // Don't respond if the overlay is disabled.
11,465✔
219
        if (open && this.disabled) return;
11,465!
220

11,465✔
221
        // Don't respond if the state is not changing.
11,465✔
222
        if (open === this.open) return;
11,465✔
223

639✔
224
        // Don't respond if the overlay is in the shadow state during a longpress.
639✔
225
        // The shadow state occurs when the first "click" would normally close the popover.
639✔
226
        if (this.strategy?.activelyOpening && !open) return;
11,465!
227

639✔
228
        // Update the internal _open property.
639✔
229
        this._open = open;
639✔
230

639✔
231
        // Increment the open count if the overlay is opening.
639✔
232
        if (this.open) {
11,244✔
233
            Overlay.openCount += 1;
326✔
234
        }
326✔
235

639✔
236
        // Request an update to re-render the component if necessary.
639✔
237
        this.requestUpdate('open', !this.open);
639✔
238

639✔
239
        // Request slottable content if the overlay is opening.
639✔
240
        if (this.open) {
11,244✔
241
            this.requestSlottable();
326✔
242
        }
326✔
243
    }
11,465✔
244

603✔
245
    private _open = false;
603✔
246

603✔
247
    /**
603✔
248
     * Tracks the number of overlays that have been opened.
603✔
249
     *
603✔
250
     * This static property is used to manage the stacking context of multiple overlays.
603✔
251
     *
603✔
252
     * @type {number}
603✔
253
     * @default 1
603✔
254
     */
603✔
255
    static openCount = 1;
603✔
256

603✔
257
    /**
603✔
258
     * Instruct the Overlay where to place itself in relationship to the trigger element.
603✔
259
     *
603✔
260
     * @type {"top" | "top-start" | "top-end" | "right" | "right-start" | "right-end" | "bottom" | "bottom-start" | "bottom-end" | "left" | "left-start" | "left-end"}
603✔
261
     */
603✔
262
    @property()
603✔
263
    override placement?: Placement;
603✔
264

603✔
265
    /**
603✔
266
     * The state in which the last `request-slottable` event was dispatched.
603✔
267
     *
603✔
268
     * This property ensures that overlays do not dispatch the same state twice in a row.
603✔
269
     *
603✔
270
     * @type {boolean}
603✔
271
     * @default false
603✔
272
     */
603✔
273
    private lastRequestSlottableState = false;
603✔
274

603✔
275
    /**
603✔
276
     * Whether to pass focus to the overlay once opened, or
603✔
277
     * to the appropriate value based on the "type" of the overlay
603✔
278
     * when set to `"auto"`.
603✔
279
     *
603✔
280
     * @type {'true' | 'false' | 'auto'}
603✔
281
     * @default 'auto'
603✔
282
     */
603✔
283
    @property({ attribute: 'receives-focus' })
603✔
284
    override receivesFocus: 'true' | 'false' | 'auto' = 'auto';
603✔
285

603✔
286
    /**
603✔
287
     * A reference to the slot element within the overlay.
603✔
288
     *
603✔
289
     * This element is used to manage the content slotted into the overlay.
603✔
290
     *
603✔
291
     * @type {HTMLSlotElement}
603✔
292
     */
603✔
293
    @query('slot')
603✔
294
    slotEl!: HTMLSlotElement;
603✔
295

603✔
296
    /**
603✔
297
     * The current state of the overlay.
603✔
298
     *
603✔
299
     * This property reflects the current state of the overlay, such as 'opened' or 'closed'.
603✔
300
     * When the state changes, it triggers the appropriate actions and updates the component.
603✔
301
     *
603✔
302
     * @type {OverlayState}
603✔
303
     * @default 'closed'
603✔
304
     */
603✔
305
    @state()
603✔
306
    override get state(): OverlayState {
603✔
307
        return this._state;
13,933✔
308
    }
13,933✔
309

603✔
310
    override set state(state) {
603✔
311
        // Do not respond if the state is not changing.
1,214✔
312
        if (state === this.state) return;
1,214✔
313

1,211✔
314
        const oldState = this.state;
1,211✔
315

1,211✔
316
        this._state = state;
1,211✔
317

1,211✔
318
        // Complete the opening strategy if the state is 'opened' or 'closed'.
1,211✔
319
        if (this.state === 'opened' || this.state === 'closed') {
1,214✔
320
            this.strategy?.shouldCompleteOpen();
585✔
321
        }
585✔
322

1,211✔
323
        // Request an update to re-render the component if necessary.
1,211✔
324
        this.requestUpdate('state', oldState);
1,211✔
325
    }
1,214✔
326

603✔
327
    override _state: OverlayState = 'closed';
603✔
328

603✔
329
    /**
603✔
330
     * The interaction strategy for opening the overlay.
603✔
331
     * This can be a ClickController, HoverController, or LongpressController.
603✔
332
     */
603✔
333
    public strategy?: ClickController | HoverController | LongpressController;
603✔
334

603✔
335
    /**
603✔
336
     * The padding around the tip of the overlay.
603✔
337
     * This property defines the padding around the tip of the overlay, which can be used to adjust its positioning.
603✔
338
     *
603✔
339
     * @type {number}
603✔
340
     */
603✔
341
    @property({ type: Number, attribute: 'tip-padding' })
603✔
342
    tipPadding?: number;
603✔
343

603✔
344
    /**
603✔
345
     * An optional ID reference for the trigger element combined with the optional
603✔
346
     * interaction (click | hover | longpress) by which the overlay should open.
603✔
347
     * The format is `trigger@interaction`, e.g., `trigger@click` opens the overlay
603✔
348
     * when an element with the ID "trigger" is clicked.
603✔
349
     *
603✔
350
     * @type {string}
603✔
351
     */
603✔
352
    @property()
603✔
353
    trigger?: string;
603✔
354

603✔
355
    /**
603✔
356
     * An element reference for the trigger element that the overlay should relate to.
603✔
357
     * This property is not reflected as an attribute.
603✔
358
     *
603✔
359
     * @type {HTMLElement | VirtualTrigger | null}
603✔
360
     */
603✔
361
    @property({ attribute: false })
603✔
362
    override triggerElement: HTMLElement | VirtualTrigger | null = null;
603✔
363

603✔
364
    /**
603✔
365
     * The specific interaction to listen for on the `triggerElement` to open the overlay.
603✔
366
     * This property is not reflected as an attribute.
603✔
367
     *
603✔
368
     * @type {TriggerInteraction}
603✔
369
     */
603✔
370
    @property({ attribute: false })
603✔
371
    triggerInteraction?: TriggerInteraction;
603✔
372

603✔
373
    /**
603✔
374
     * Configures the open/close heuristics of the Overlay.
603✔
375
     *
603✔
376
     * @type {"auto" | "hint" | "manual" | "modal" | "page"}
603✔
377
     * @default "auto"
603✔
378
     */
603✔
379
    @property()
603✔
380
    override type: OverlayTypes = 'auto';
603✔
381

603✔
382
    /**
603✔
383
     * Tracks whether the overlay was previously open.
603✔
384
     * This is used to restore the open state when re-enabling the overlay.
603✔
385
     *
603✔
386
     * @type {boolean}
603✔
387
     * @default false
603✔
388
     */
603✔
389
    protected wasOpen = false;
603✔
390

603✔
391
    /**
603✔
392
     * Provides an instance of the `ElementResolutionController` for managing the element
603✔
393
     * that the overlay should be associated with. If the instance does not already exist,
603✔
394
     * it is created and assigned to the `_elementResolver` property.
603✔
395
     *
603✔
396
     * @protected
603✔
397
     * @returns {ElementResolutionController} The `ElementResolutionController` instance.
603✔
398
     */
603✔
399
    protected override get elementResolver(): ElementResolutionController {
603✔
400
        if (!this._elementResolver) {
42✔
401
            this._elementResolver = new ElementResolutionController(this);
14✔
402
        }
14✔
403

42✔
404
        return this._elementResolver;
42✔
405
    }
42✔
406

603✔
407
    /**
603✔
408
     * Determines if the overlay uses a dialog.
603✔
409
     * Returns `true` if the overlay type is "modal" or "page".
603✔
410
     *
603✔
411
     * @private
603✔
412
     * @returns {boolean} `true` if the overlay uses a dialog, otherwise `false`.
603✔
413
     */
603✔
414
    private get usesDialog(): boolean {
603✔
415
        return this.type === 'modal' || this.type === 'page';
626✔
416
    }
626✔
417

603✔
418
    /**
603✔
419
     * Determines the value for the popover attribute based on the overlay type.
603✔
420
     *
603✔
421
     * @private
603✔
422
     * @returns {'auto' | 'manual' | undefined} The popover value or undefined if not applicable.
603✔
423
     */
603✔
424
    private get popoverValue(): 'auto' | 'manual' | undefined {
603✔
425
        const hasPopoverAttribute = 'popover' in this;
2,051✔
426

2,051✔
427
        if (!hasPopoverAttribute) {
2,051!
428
            return undefined;
×
429
        }
×
430

2,051✔
431
        switch (this.type) {
2,051✔
432
            case 'modal':
2,051!
433
            case 'page':
2,051!
434
                return undefined;
×
435
            case 'hint':
2,051✔
436
                return 'manual';
602✔
437
            default:
2,051✔
438
                return this.type;
1,449✔
439
        }
2,051✔
440
    }
2,051✔
441

603✔
442
    /**
603✔
443
     * Determines if the overlay requires positioning based on its type and state.
603✔
444
     *
603✔
445
     * @protected
603✔
446
     * @returns {boolean} True if the overlay requires positioning, otherwise false.
603✔
447
     */
603✔
448
    protected get requiresPositioning(): boolean {
603✔
449
        // Do not position "page" overlays as they should block the entire UI.
2,463✔
450
        if (this.type === 'page' || !this.open) return false;
2,463✔
451

971✔
452
        // Do not position content without a trigger element, as there is nothing to position it relative to.
971✔
453
        // Do not automatically position content unless it is a "hint".
971✔
454
        if (!this.triggerElement || (!this.placement && this.type !== 'hint'))
1,515✔
455
            return false;
2,463✔
456

814✔
457
        return true;
814✔
458
    }
2,463✔
459

603✔
460
    /**
603✔
461
     * Manages the positioning of the overlay relative to its trigger element.
603✔
462
     *
603✔
463
     * This method calculates the necessary parameters for positioning the overlay,
603✔
464
     * such as offset, placement, and tip padding, and then delegates the actual
603✔
465
     * positioning to the `PlacementController`.
603✔
466
     *
603✔
467
     * @protected
603✔
468
     * @override
603✔
469
     */
603✔
470
    protected override managePosition(): void {
603✔
471
        // Do not proceed if positioning is not required or the overlay is not open.
315✔
472
        if (!this.requiresPositioning || !this.open) return;
315✔
473

237✔
474
        const offset = this.offset || 0;
308✔
475

315✔
476
        const trigger = this.triggerElement as HTMLElement;
315✔
477

315✔
478
        const placement = (this.placement as Placement) || 'right';
315✔
479

315✔
480
        const tipPadding = this.tipPadding;
315✔
481

315✔
482
        this.placementController.placeOverlay(this.dialogEl, {
315✔
483
            offset,
315✔
484
            placement,
315✔
485
            tipPadding,
315✔
486
            trigger,
315✔
487
            type: this.type,
315✔
488
        });
315✔
489
    }
315✔
490

603✔
491
    /**
603✔
492
     * Manages the process of opening the popover.
603✔
493
     *
603✔
494
     * This method handles the necessary steps to open the popover, including managing delays,
603✔
495
     * ensuring the popover is in the DOM, making transitions, and applying focus.
603✔
496
     *
603✔
497
     * @protected
603✔
498
     * @override
603✔
499
     * @returns {Promise<void>} A promise that resolves when the popover has been fully opened.
603✔
500
     */
603✔
501
    protected override async managePopoverOpen(): Promise<void> {
603✔
502
        // Call the base class method to handle any initial setup.
586✔
503
        super.managePopoverOpen();
586✔
504

586✔
505
        const targetOpenState = this.open;
586✔
506

586✔
507
        // Ensure the open state has not changed before proceeding.
586✔
508
        if (this.open !== targetOpenState) {
586✔
509
            return;
×
510
        }
×
511

586✔
512
        // Manage any delays before opening the popover.
586✔
513
        await this.manageDelay(targetOpenState);
586✔
514

586✔
515
        if (this.open !== targetOpenState) {
586✔
UNCOV
516
            return;
×
UNCOV
517
        }
×
518

586✔
519
        // Ensure the popover is in the DOM before proceeding.
586✔
520
        await this.ensureOnDOM(targetOpenState);
586✔
521

581✔
522
        if (this.open !== targetOpenState) {
586✔
523
            return;
18✔
524
        }
18✔
525

563✔
526
        // Make any necessary transitions for opening the popover.
563✔
527
        const focusEl = await this.makeTransition(targetOpenState);
563✔
528

563✔
529
        if (this.open !== targetOpenState) {
586✔
530
            return;
×
531
        }
✔
532

563✔
533
        // Apply focus to the appropriate element after opening the popover.
563✔
534
        await this.applyFocus(targetOpenState, focusEl);
563✔
535
    }
586✔
536

603✔
537
    /**
603✔
538
     * Applies focus to the appropriate element after the popover has been opened.
603✔
539
     *
603✔
540
     * This method handles the focus management for the overlay, ensuring that the correct
603✔
541
     * element receives focus based on the overlay's type and state.
603✔
542
     *
603✔
543
     * @protected
603✔
544
     * @override
603✔
545
     * @param {boolean} targetOpenState - The target open state of the overlay.
603✔
546
     * @param {HTMLElement | null} focusEl - The element to focus after opening the popover.
603✔
547
     * @returns {Promise<void>} A promise that resolves when the focus has been applied.
603✔
548
     */
603✔
549
    protected override async applyFocus(
603✔
550
        targetOpenState: boolean,
603✔
551
        focusEl: HTMLElement | null
603✔
552
    ): Promise<void> {
603✔
553
        // Do not move focus when explicitly told not to or when the overlay is a "hint".
603✔
554
        if (this.receivesFocus === 'false' || this.type === 'hint') {
603✔
555
            return;
123✔
556
        }
123✔
557

480✔
558
        // Wait for the next two animation frames to ensure the DOM is updated.
480✔
559
        await nextFrame();
480✔
560
        await nextFrame();
480✔
561

480✔
562
        // If the open state has changed during the delay, do not proceed.
480✔
563
        if (targetOpenState === this.open && !this.open) {
603✔
564
            // If the overlay is closing and the trigger element is still focused, return focus to the trigger element.
240✔
565
            if (
240✔
566
                this.hasNonVirtualTrigger &&
240✔
567
                this.contains((this.getRootNode() as Document).activeElement)
197✔
568
            ) {
240✔
569
                (this.triggerElement as HTMLElement).focus();
54✔
570
            }
54✔
571
            return;
240✔
572
        }
240✔
573

240✔
574
        // Apply focus to the specified focus element.
240✔
575
        focusEl?.focus();
603✔
576
    }
603✔
577

603✔
578
    /**
603✔
579
     * Returns focus to the trigger element if the overlay is closed.
603✔
580
     *
603✔
581
     * This method ensures that focus is returned to the trigger element when the overlay is closed,
603✔
582
     * unless the overlay is of type "hint" or the focus is already outside the overlay.
603✔
583
     *
603✔
584
     * @protected
603✔
585
     * @override
603✔
586
     */
603✔
587
    protected override returnFocus(): void {
603✔
588
        // Do not proceed if the overlay is open or if the overlay type is "hint".
588✔
589
        if (this.open || this.type === 'hint') return;
588✔
590

270✔
591
        /**
270✔
592
         * Retrieves the ancestors of the currently focused element.
270✔
593
         *
270✔
594
         * @returns {HTMLElement[]} An array of ancestor elements.
270✔
595
         */
270✔
596
        const getAncestors = (): HTMLElement[] => {
270✔
597
            const ancestors: HTMLElement[] = [];
195✔
598

195✔
599
            // eslint-disable-next-line @spectrum-web-components/document-active-element
195✔
600
            let currentNode = document.activeElement;
195✔
601

195✔
602
            // Traverse the shadow DOM to find the active element.
195✔
603
            while (currentNode?.shadowRoot?.activeElement) {
195✔
604
                currentNode = currentNode.shadowRoot.activeElement;
78✔
605
            }
78✔
606

195✔
607
            // Traverse the DOM tree to collect ancestor elements.
195✔
608
            while (currentNode) {
195✔
609
                const ancestor =
1,315✔
610
                    currentNode.assignedSlot ||
1,315✔
611
                    currentNode.parentElement ||
1,038✔
612
                    (currentNode.getRootNode() as ShadowRoot)?.host;
550✔
613
                if (ancestor) {
1,315✔
614
                    ancestors.push(ancestor as HTMLElement);
1,120✔
615
                }
1,120✔
616
                currentNode = ancestor;
1,315✔
617
            }
1,315✔
618
            return ancestors;
195✔
619
        };
195✔
620

270✔
621
        // Check if focus should be returned to the trigger element.
270✔
622
        if (
270✔
623
            this.receivesFocus !== 'false' &&
270✔
624
            !!(this.triggerElement as HTMLElement)?.focus &&
232✔
625
            (this.contains((this.getRootNode() as Document).activeElement) ||
195✔
626
                getAncestors().includes(this) ||
195✔
627
                // eslint-disable-next-line @spectrum-web-components/document-active-element
173✔
628
                document.activeElement === document.body)
173✔
629
        ) {
588✔
630
            // Return focus to the trigger element.
103✔
631
            (this.triggerElement as HTMLElement).focus();
103✔
632
        }
103✔
633
    }
588✔
634

603✔
635
    /**
603✔
636
     * Handles the focus out event to close the overlay if the focus moves outside of it.
603✔
637
     *
603✔
638
     * This method ensures that the overlay is closed when the focus moves to an element
603✔
639
     * outside of the overlay, unless the focus is moved to a related element.
603✔
640
     *
603✔
641
     * @private
603✔
642
     * @param {FocusEvent} event - The focus out event.
603✔
643
     */
603✔
644
    private closeOnFocusOut = (event: FocusEvent): void => {
603✔
645
        // If the related target (newly focused element) is not known, do nothing.
1,395✔
646
        if (!event.relatedTarget) {
1,395✔
647
            return;
589✔
648
        }
589✔
649

806✔
650
        // Create a custom event to query the relationship of the newly focused element.
806✔
651
        const relationEvent = new Event('overlay-relation-query', {
806✔
652
            bubbles: true,
806✔
653
            composed: true,
806✔
654
        });
806✔
655

806✔
656
        // Add an event listener to the related target to handle the custom event.
806✔
657
        event.relatedTarget.addEventListener(
806✔
658
            relationEvent.type,
806✔
659
            (event: Event) => {
806✔
660
                // If the newly focused element is not within the overlay, close the overlay.
10,193✔
661
                if (!event.composedPath().includes(this)) {
10,193✔
662
                    this.open = false;
9,737✔
663
                }
9,737✔
664
            }
10,193✔
665
        );
806✔
666

806✔
667
        // Dispatch the custom event to the related target.
806✔
668
        event.relatedTarget.dispatchEvent(relationEvent);
806✔
669
    };
1,395✔
670

38✔
671
    /**
38✔
672
     * Manages the process of opening or closing the overlay.
38✔
673
     *
38✔
674
     * This method handles the necessary steps to open or close the overlay, including updating the state,
38✔
675
     * managing the overlay stack, and handling focus events.
38✔
676
     *
38✔
677
     * @protected
38✔
678
     * @param {boolean} oldOpen - The previous open state of the overlay.
38✔
679
     * @returns {Promise<void>} A promise that resolves when the overlay has been fully managed.
38✔
680
     */
38✔
681
    protected async manageOpen(oldOpen: boolean): Promise<void> {
38✔
682
        // Prevent entering the manage workflow if the overlay is not connected to the DOM.
626✔
683
        // The `.showPopover()` and `.showModal()` events will error on content that is not connected to the DOM.
626✔
684
        if (!this.isConnected && this.open) return;
626!
685

626✔
686
        // Wait for the component to finish updating if it has not already done so.
626✔
687
        if (!this.hasUpdated) {
626✔
688
            await this.updateComplete;
105✔
689
        }
105✔
690

626✔
691
        if (this.open) {
626✔
692
            // Add the overlay to the overlay stack.
313✔
693
            overlayStack.add(this);
313✔
694

313✔
695
            if (this.willPreventClose) {
313✔
696
                // Add an event listener to handle the pointerup event and toggle the 'not-immediately-closable' class.
21✔
697
                document.addEventListener(
21✔
698
                    'pointerup',
21✔
699
                    () => {
21✔
700
                        this.dialogEl.classList.toggle(
21✔
701
                            'not-immediately-closable',
21✔
702
                            false
21✔
703
                        );
21✔
704
                        this.willPreventClose = false;
21✔
705
                    },
21✔
706
                    { once: true }
21✔
707
                );
21✔
708
                this.dialogEl.classList.toggle(
21✔
709
                    'not-immediately-closable',
21✔
710
                    true
21✔
711
                );
21✔
712
            }
21✔
713
        } else {
626✔
714
            if (oldOpen) {
313✔
715
                // Dispose of the overlay if it was previously open.
313✔
716
                this.dispose();
313✔
717
            }
313✔
718

313✔
719
            // Remove the overlay from the overlay stack.
313✔
720
            overlayStack.remove(this);
313✔
721
        }
313✔
722

626✔
723
        // Update the state of the overlay based on the open property.
626✔
724
        if (this.open && this.state !== 'opened') {
626✔
725
            this.state = 'opening';
313✔
726
        } else if (!this.open && this.state !== 'closed') {
313✔
727
            this.state = 'closing';
313✔
728
        }
313✔
729

626✔
730
        // Manage the dialog or popover based on the overlay type.
626✔
731
        if (this.usesDialog) {
626✔
732
            this.manageDialogOpen();
40✔
733
        } else {
624✔
734
            this.managePopoverOpen();
586✔
735
        }
586✔
736

626✔
737
        // Handle focus events for auto type overlays.
626✔
738
        if (this.type === 'auto') {
626✔
739
            const listenerRoot = this.getRootNode() as Document;
504✔
740
            if (this.open) {
504✔
741
                listenerRoot.addEventListener(
252✔
742
                    'focusout',
252✔
743
                    this.closeOnFocusOut,
252✔
744
                    { capture: true }
252✔
745
                );
252✔
746
            } else {
252✔
747
                listenerRoot.removeEventListener(
252✔
748
                    'focusout',
252✔
749
                    this.closeOnFocusOut,
252✔
750
                    { capture: true }
252✔
751
                );
252✔
752
            }
252✔
753
        }
504✔
754
    }
626✔
755

38✔
756
    /**
38✔
757
     * Binds event handling strategies to the overlay based on the specified trigger interaction.
38✔
758
     *
38✔
759
     * This method sets up the appropriate event handling strategy for the overlay, ensuring that
38✔
760
     * it responds correctly to user interactions such as clicks, hovers, or long presses.
38✔
761
     *
38✔
762
     * @protected
38✔
763
     */
38✔
764
    protected bindEvents(): void {
38✔
765
        // Abort any existing strategy to ensure a clean setup.
887✔
766
        this.strategy?.abort();
887✔
767
        this.strategy = undefined;
887✔
768

887✔
769
        // Return early if there is no non-virtual trigger element.
887✔
770
        if (!this.hasNonVirtualTrigger) return;
887✔
771

515✔
772
        // Return early if no trigger interaction is specified.
515✔
773
        if (!this.triggerInteraction) return;
515✔
774

260✔
775
        // Set up a new event handling strategy based on the specified trigger interaction.
260✔
776
        this.strategy = new strategies[this.triggerInteraction](
260✔
777
            this.triggerElement as HTMLElement,
260✔
778
            {
260✔
779
                overlay: this,
260✔
780
            }
260✔
781
        );
260✔
782
    }
887✔
783

38✔
784
    /**
38✔
785
     * Handles the `beforetoggle` event to manage the overlay's state.
38✔
786
     *
38✔
787
     * This method checks the new state of the event and calls `handleBrowserClose`
38✔
788
     * if the new state is not 'open'.
38✔
789
     *
38✔
790
     * @protected
38✔
791
     * @param {Event & { newState: string }} event - The `beforetoggle` event with the new state.
38✔
792
     */
38✔
793
    protected handleBeforetoggle(event: Event & { newState: string }): void {
38✔
794
        if (event.newState !== 'open') {
442✔
795
            this.handleBrowserClose(event);
167✔
796
        }
167✔
797
    }
442✔
798

38✔
799
    /**
38✔
800
     * Handles the browser's close event to manage the overlay's state.
38✔
801
     *
38✔
802
     * This method stops the propagation of the event and closes the overlay if it is not
38✔
803
     * actively opening. If the overlay is actively opening, it calls `manuallyKeepOpen`.
38✔
804
     *
38✔
805
     * @protected
38✔
806
     * @param {Event} event - The browser's close event.
38✔
807
     */
38✔
808
    protected handleBrowserClose(event: Event): void {
38✔
809
        event.stopPropagation();
186✔
810
        if (!this.strategy?.activelyOpening) {
186✔
811
            this.open = false;
186✔
812
            return;
186✔
813
        }
186!
UNCOV
814
        this.manuallyKeepOpen();
×
815
    }
186✔
816

38✔
817
    /**
38✔
818
     * Manually keeps the overlay open.
38✔
819
     *
38✔
820
     * This method sets the overlay to open, allows placement updates, and manages the open state.
38✔
821
     *
38✔
822
     * @public
38✔
823
     * @override
38✔
824
     */
38✔
825
    public override manuallyKeepOpen(): void {
38✔
UNCOV
826
        this.open = true;
×
UNCOV
827
        this.placementController.allowPlacementUpdate = true;
×
UNCOV
828
        this.manageOpen(false);
×
UNCOV
829
    }
×
830

38✔
831
    /**
38✔
832
     * Handles the `slotchange` event to manage the overlay's state.
38✔
833
     *
38✔
834
     * This method checks if there are any elements in the slot. If there are no elements,
38✔
835
     * it releases the description from the strategy. If there are elements and the trigger
38✔
836
     * is non-virtual, it prepares the description for the trigger element.
38✔
837
     *
38✔
838
     * @protected
38✔
839
     */
38✔
840
    protected handleSlotchange(): void {
38✔
841
        if (!this.elements.length) {
654✔
842
            // Release the description if there are no elements in the slot.
61✔
843
            this.strategy?.releaseDescription();
61!
844
        } else if (this.hasNonVirtualTrigger) {
654✔
845
            // Prepare the description for the trigger element if it is non-virtual.
511✔
846
            this.strategy?.prepareDescription(
511✔
847
                this.triggerElement as HTMLElement
37✔
848
            );
511✔
849
        }
511✔
850
    }
654✔
851

38✔
852
    /**
38✔
853
     * Determines whether the overlay should prevent closing.
38✔
854
     *
38✔
855
     * This method checks the `willPreventClose` flag and resets it to `false`.
38✔
856
     * It returns the value of the `willPreventClose` flag.
38✔
857
     *
38✔
858
     * @public
38✔
859
     * @returns {boolean} `true` if the overlay should prevent closing, otherwise `false`.
38✔
860
     */
38✔
861
    public shouldPreventClose(): boolean {
38✔
862
        const shouldPreventClose = this.willPreventClose;
2✔
863
        this.willPreventClose = false;
2✔
864
        return shouldPreventClose;
2✔
865
    }
2✔
866

38✔
867
    /**
38✔
868
     * Requests slottable content for the overlay.
38✔
869
     *
38✔
870
     * This method dispatches a `SlottableRequestEvent` to request or remove slottable content
38✔
871
     * based on the current open state of the overlay. It ensures that the same state is not
38✔
872
     * dispatched twice in a row.
38✔
873
     *
38✔
874
     * @protected
38✔
875
     * @override
38✔
876
     */
38✔
877
    protected override requestSlottable(): void {
38✔
878
        // Do not dispatch the same state twice in a row.
585✔
879
        if (this.lastRequestSlottableState === this.open) {
585✔
880
            return;
37✔
881
        }
37✔
882

548✔
883
        // Force a reflow if the overlay is closing.
548✔
884
        if (!this.open) {
585✔
885
            document.body.offsetHeight;
256✔
886
        }
256✔
887

548✔
888
        /**
548✔
889
         * @ignore
548✔
890
         */
548✔
891
        // Dispatch a custom event to request or remove slottable content based on the open state.
548✔
892
        this.dispatchEvent(
548✔
893
            new SlottableRequestEvent(
548✔
894
                'overlay-content',
548✔
895
                this.open ? {} : removeSlottableRequest
585✔
896
            )
585✔
897
        );
585✔
898

585✔
899
        // Update the last request slottable state.
585✔
900
        this.lastRequestSlottableState = this.open;
585✔
901
    }
585✔
902

38✔
903
    /**
38✔
904
     * Lifecycle method called before the component updates.
38✔
905
     *
38✔
906
     * This method handles various tasks before the component updates, such as setting an ID,
38✔
907
     * managing the open state, resolving the trigger element, and binding events.
38✔
908
     *
38✔
909
     * @override
38✔
910
     * @param {PropertyValues} changes - The properties that have changed.
38✔
911
     */
38✔
912
    override willUpdate(changes: PropertyValues): void {
38✔
913
        // Ensure the component has an ID attribute.
2,148✔
914
        if (!this.hasAttribute('id')) {
2,148✔
915
            this.setAttribute(
587✔
916
                'id',
587✔
917
                `${this.tagName.toLowerCase()}-${randomID()}`
587✔
918
            );
587✔
919
        }
587✔
920

2,148✔
921
        // Manage the open state if the 'open' property has changed.
2,148✔
922
        if (changes.has('open') && (this.hasUpdated || this.open)) {
2,148✔
923
            this.manageOpen(changes.get('open'));
626✔
924
        }
626✔
925

2,148✔
926
        // Resolve the trigger element if the 'trigger' property has changed.
2,148✔
927
        if (changes.has('trigger')) {
2,148✔
928
            const [id, interaction] = this.trigger?.split('@') || [];
14!
929
            this.elementResolver.selector = id ? `#${id}` : '';
14!
930
            this.triggerInteraction = interaction as
14✔
931
                | 'click'
14✔
932
                | 'longpress'
14✔
933
                | 'hover'
14✔
934
                | undefined;
14✔
935
        }
14✔
936

2,148✔
937
        // Initialize oldTrigger to track the previous trigger element.
2,148✔
938
        let oldTrigger: HTMLElement | false | undefined = false;
2,148✔
939

2,148✔
940
        // Check if the element resolver has been updated.
2,148✔
941
        if (changes.has(elementResolverUpdatedSymbol)) {
2,148✔
942
            // Store the current trigger element.
28✔
943
            oldTrigger = this.triggerElement as HTMLElement;
28✔
944
            // Update the trigger element from the element resolver.
28✔
945
            this.triggerElement = this.elementResolver.element;
28✔
946
        }
28✔
947

2,148✔
948
        // Check if the 'triggerElement' property has changed.
2,148✔
949
        if (changes.has('triggerElement')) {
2,148✔
950
            // Store the old trigger element.
880✔
951
            oldTrigger = changes.get('triggerElement');
880✔
952
        }
880✔
953

2,148✔
954
        // If the trigger element has changed, bind the new events.
2,148✔
955
        if (oldTrigger !== false) {
2,148✔
956
            this.bindEvents();
880✔
957
        }
880✔
958
    }
2,148✔
959

38✔
960
    /**
38✔
961
     * Lifecycle method called after the component updates.
38✔
962
     *
38✔
963
     * This method handles various tasks after the component updates, such as updating the placement
38✔
964
     * attribute, resetting the overlay position, and clearing the overlay position based on the state.
38✔
965
     *
38✔
966
     * @override
38✔
967
     * @param {PropertyValues} changes - The properties that have changed.
38✔
968
     */
38✔
969
    protected override updated(changes: PropertyValues): void {
38✔
970
        // Call the base class method to handle any initial setup.
2,148✔
971
        super.updated(changes);
2,148✔
972

2,148✔
973
        // Check if the 'placement' property has changed.
2,148✔
974
        if (changes.has('placement')) {
2,148✔
975
            if (this.placement) {
508✔
976
                // Set the 'actual-placement' attribute on the dialog element.
508✔
977
                this.dialogEl.setAttribute('actual-placement', this.placement);
508✔
978
            } else {
508!
979
                // Remove the 'actual-placement' attribute from the dialog element.
×
980
                this.dialogEl.removeAttribute('actual-placement');
×
981
            }
×
982

508✔
983
            // If the overlay is open and the 'placement' property has changed, reset the overlay position.
508✔
984
            if (this.open && typeof changes.get('placement') !== 'undefined') {
508✔
985
                this.placementController.resetOverlayPosition();
2✔
986
            }
2✔
987
        }
508✔
988

2,148✔
989
        // Check if the 'state' property has changed and the overlay is closed.
2,148✔
990
        if (
2,148✔
991
            changes.has('state') &&
2,148✔
992
            this.state === 'closed' &&
1,786✔
993
            typeof changes.get('state') !== 'undefined'
878✔
994
        ) {
2,148✔
995
            // Clear the overlay position.
878✔
996
            this.placementController.clearOverlayPosition();
878✔
997
        }
878✔
998
    }
2,148✔
999

38✔
1000
    /**
38✔
1001
     * Renders the content of the overlay.
38✔
1002
     *
38✔
1003
     * This method returns a template result containing a slot element. The slot element
38✔
1004
     * listens for the `slotchange` event to manage the overlay's state.
38✔
1005
     *
38✔
1006
     * @protected
38✔
1007
     * @returns {TemplateResult} The template result containing the slot element.
38✔
1008
     */
38✔
1009
    protected renderContent(): TemplateResult {
38✔
1010
        return html`
2,148✔
1011
            <slot @slotchange=${this.handleSlotchange}></slot>
2,148✔
1012
        `;
2,148✔
1013
    }
2,148✔
1014

38✔
1015
    /**
38✔
1016
     * Generates a style map for the dialog element.
38✔
1017
     *
38✔
1018
     * This method returns an object containing CSS custom properties for the dialog element.
38✔
1019
     * The `--swc-overlay-open-count` custom property is set to the current open count of overlays.
38✔
1020
     *
38✔
1021
     * @private
38✔
1022
     * @returns {StyleInfo} The style map for the dialog element.
38✔
1023
     */
38✔
1024
    private get dialogStyleMap(): StyleInfo {
38✔
1025
        return {
2,148✔
1026
            '--swc-overlay-open-count': Overlay.openCount.toString(),
2,148✔
1027
        };
2,148✔
1028
    }
2,148✔
1029

38✔
1030
    /**
38✔
1031
     * Renders the dialog element for the overlay.
38✔
1032
     *
38✔
1033
     * This method returns a template result containing a dialog element. The dialog element
38✔
1034
     * includes various attributes and event listeners to manage the overlay's state and behavior.
38✔
1035
     *
38✔
1036
     * @protected
38✔
1037
     * @returns {TemplateResult} The template result containing the dialog element.
38✔
1038
     */
38✔
1039
    protected renderDialog(): TemplateResult {
38✔
1040
        /**
97✔
1041
         * The `--swc-overlay-open-count` custom property is applied to mimic the single stack
97✔
1042
         * nature of the top layer in browsers that do not yet support it.
97✔
1043
         *
97✔
1044
         * The value should always represent the total number of overlays that have ever been opened.
97✔
1045
         * This value will be added to the `--swc-overlay-z-index-base` custom property, which can be
97✔
1046
         * provided by a consuming developer. By default, `--swc-overlay-z-index-base` is set to 1000
97✔
1047
         * to ensure that the overlay stacks above most other elements during fallback delivery.
97✔
1048
         */
97✔
1049
        return html`
97✔
1050
            <dialog
97✔
1051
                class="dialog"
97✔
1052
                part="dialog"
97✔
1053
                placement=${ifDefined(
97✔
1054
                    this.requiresPositioning
97!
UNCOV
1055
                        ? this.placement || 'right'
×
1056
                        : undefined
97✔
1057
                )}
97✔
1058
                style=${styleMap(this.dialogStyleMap)}
97✔
1059
                @close=${this.handleBrowserClose}
97✔
1060
                @cancel=${this.handleBrowserClose}
97✔
1061
                @beforetoggle=${this.handleBeforetoggle}
97✔
1062
                ?is-visible=${this.state !== 'closed'}
97✔
1063
            >
97✔
1064
                ${this.renderContent()}
97✔
1065
            </dialog>
97✔
1066
        `;
97✔
1067
    }
97✔
1068

38✔
1069
    /**
38✔
1070
     * Renders the popover element for the overlay.
38✔
1071
     *
38✔
1072
     * This method returns a template result containing a div element styled as a popover.
38✔
1073
     * The popover element includes various attributes and event listeners to manage the overlay's state and behavior.
38✔
1074
     *
38✔
1075
     * @protected
38✔
1076
     * @returns {TemplateResult} The template result containing the popover element.
38✔
1077
     */
38✔
1078
    protected renderPopover(): TemplateResult {
38✔
1079
        /**
2,051✔
1080
         * The `--swc-overlay-open-count` custom property is applied to mimic the single stack
2,051✔
1081
         * nature of the top layer in browsers that do not yet support it.
2,051✔
1082
         *
2,051✔
1083
         * The value should always represent the total number of overlays that have ever been opened.
2,051✔
1084
         * This value will be added to the `--swc-overlay-z-index-base` custom property, which can be
2,051✔
1085
         * provided by a consuming developer. By default, `--swc-overlay-z-index-base` is set to 1000
2,051✔
1086
         * to ensure that the overlay stacks above most other elements during fallback delivery.
2,051✔
1087
         */
2,051✔
1088
        return html`
2,051✔
1089
            <div
2,051✔
1090
                class="dialog"
2,051✔
1091
                part="dialog"
2,051✔
1092
                placement=${ifDefined(
2,051✔
1093
                    this.requiresPositioning
2,051✔
1094
                        ? this.placement || 'right'
577✔
1095
                        : undefined
1,474✔
1096
                )}
2,051✔
1097
                popover=${ifDefined(this.popoverValue)}
2,051✔
1098
                style=${styleMap(this.dialogStyleMap)}
2,051✔
1099
                @beforetoggle=${this.handleBeforetoggle}
2,051✔
1100
                @close=${this.handleBrowserClose}
2,051✔
1101
                ?is-visible=${this.state !== 'closed'}
2,051✔
1102
            >
2,051✔
1103
                ${this.renderContent()}
2,051✔
1104
            </div>
2,051✔
1105
        `;
2,051✔
1106
    }
2,051✔
1107

38✔
1108
    /**
38✔
1109
     * Renders the overlay component.
38✔
1110
     *
38✔
1111
     * This method returns a template result containing either a dialog or popover element
38✔
1112
     * based on the overlay type. It also includes a slot for longpress descriptors.
38✔
1113
     *
38✔
1114
     * @override
38✔
1115
     * @returns {TemplateResult} The template result containing the overlay content.
38✔
1116
     */
38✔
1117
    public override render(): TemplateResult {
38✔
1118
        const isDialog = this.type === 'modal' || this.type === 'page';
2,148✔
1119
        return html`
2,148✔
1120
            ${isDialog ? this.renderDialog() : this.renderPopover()}
2,148✔
1121
            <slot name="longpress-describedby-descriptor"></slot>
2,148✔
1122
        `;
2,148✔
1123
    }
2,148✔
1124

38✔
1125
    /**
38✔
1126
     * Lifecycle method called when the component is added to the DOM.
38✔
1127
     *
38✔
1128
     * This method sets up event listeners and binds events if the component has already updated.
38✔
1129
     *
38✔
1130
     * @override
38✔
1131
     */
38✔
1132
    override connectedCallback(): void {
38✔
1133
        super.connectedCallback();
591✔
1134

591✔
1135
        // Add an event listener to handle the 'close' event and update the 'open' property.
591✔
1136
        this.addEventListener('close', () => {
591✔
1137
            this.open = false;
×
1138
        });
591✔
1139

591✔
1140
        // Bind events if the component has already updated.
591✔
1141
        if (this.hasUpdated) {
591✔
1142
            this.bindEvents();
2✔
1143
        }
2✔
1144
    }
591✔
1145

38✔
1146
    /**
38✔
1147
     * Lifecycle method called when the component is removed from the DOM.
38✔
1148
     *
38✔
1149
     * This method releases the description from the strategy and updates the 'open' property.
38✔
1150
     *
38✔
1151
     * @override
38✔
1152
     */
38✔
1153
    override disconnectedCallback(): void {
38✔
1154
        // Release the description from the strategy.
589✔
1155
        this.strategy?.releaseDescription();
589✔
1156
        // Update the 'open' property to false.
589✔
1157
        this.open = false;
589✔
1158
        super.disconnectedCallback();
589✔
1159
    }
589✔
1160
}
38✔
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