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

adobe / spectrum-web-components / 13782907169

11 Mar 2025 07:50AM UTC coverage: 97.977%. Remained the same
13782907169

Pull #5181

github

web-flow
Merge 60ed4f689 into 0dadda413
Pull Request #5181: fix(lint): fixed the cem import statement

5298 of 5604 branches covered (94.54%)

Branch coverage included in aggregate %.

33650 of 34148 relevant lines covered (98.54%)

645.78 hits per line

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

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

44✔
7
Unless required by applicable law or agreed to in writing, software distributed under
44✔
8
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
44✔
9
OF ANY KIND, either express or implied. See the License for the specific language
44✔
10
governing permissions and limitations under the License.
44✔
11
*/
44✔
12
import {
44✔
13
    firstFocusableIn,
44✔
14
    firstFocusableSlottedIn,
44✔
15
} from '@spectrum-web-components/shared/src/first-focusable-in.js';
44✔
16
import type { SpectrumElement } from '@spectrum-web-components/base';
44✔
17
import { VirtualTrigger } from './VirtualTrigger.js';
44✔
18
import { Constructor, OpenableElement } from './overlay-types.js';
44✔
19
import {
44✔
20
    guaranteedAllTransitionend,
44✔
21
    nextFrame,
44✔
22
    overlayTimer,
44✔
23
} from './AbstractOverlay.js';
44✔
24
import {
44✔
25
    BeforetoggleClosedEvent,
44✔
26
    BeforetoggleOpenEvent,
44✔
27
    OverlayStateEvent,
44✔
28
} from './events.js';
44✔
29
import type { AbstractOverlay } from './AbstractOverlay.js';
44✔
30
import { userFocusableSelector } from '@spectrum-web-components/shared';
44✔
31

44✔
32
const supportsOverlayAuto = CSS.supports('(overlay: auto)');
44✔
33

44✔
34
function isOpen(el: HTMLElement): boolean {
762✔
35
    let popoverOpen = false;
762✔
36
    try {
762✔
37
        popoverOpen = el.matches(':popover-open');
762✔
38
        // eslint-disable-next-line no-empty
762✔
39
    } catch (error) {}
762!
40
    let open = false;
762✔
41
    try {
762✔
42
        open = el.matches(':open');
762✔
43
        // eslint-disable-next-line no-empty
762✔
44
    } catch (error) {}
762✔
45
    return popoverOpen || open;
762✔
46
}
762✔
47

44✔
48
export function OverlayPopover<T extends Constructor<AbstractOverlay>>(
44✔
49
    constructor: T
44✔
50
): T & Constructor<SpectrumElement> {
44✔
51
    class OverlayWithPopover extends constructor {
44✔
52
        protected override async manageDelay(
44✔
53
            targetOpenState: boolean
829✔
54
        ): Promise<void> {
829✔
55
            if (targetOpenState === false || targetOpenState !== this.open) {
829✔
56
                overlayTimer.close(this);
414✔
57
                return;
414✔
58
            }
414✔
59
            if (this.delayed) {
828✔
60
                const cancelled = await overlayTimer.openTimer(this);
8✔
61
                if (cancelled) {
8✔
62
                    this.open = !targetOpenState;
2✔
63
                }
2✔
64
            }
8✔
65
        }
829✔
66

44✔
67
        /**
44✔
68
         * A popover should be hidden _after_ it is no longer on top-layer because
44✔
69
         * the position metrics will have changed from when it was originally positioned.
44✔
70
         */
44✔
71
        private async shouldHidePopover(
44✔
72
            targetOpenState: boolean
×
73
        ): Promise<void> {
×
74
            if (targetOpenState && this.open !== targetOpenState) {
×
75
                return;
×
76
            }
×
77
            const update = async ({
×
78
                newState,
×
79
            }: { newState?: string } = {}): Promise<void> => {
×
80
                if (newState === 'open') {
×
81
                    return;
×
82
                }
×
83
                // When in a parent Overlay, this Overlay may need to position itself
×
84
                // while closing in due to the parent _also_ closing which means the
×
85
                // location can no longer rely on "top layer over transform" math.
×
86
                await this.placementController.resetOverlayPosition();
×
87
            };
×
88
            if (!isOpen(this.dialogEl)) {
×
89
                // The means the Overlay was closed from the outside, it is already off of top-layer
×
90
                // so we need to position it in regards to this new state.
×
91
                update();
×
92
                return;
×
93
            }
×
94
            // `toggle` is an async event, so it's possible for this handler to run a frame late
×
95
            this.dialogEl.addEventListener('toggle', update as EventListener, {
×
96
                once: true,
×
97
            });
×
98
        }
×
99

44✔
100
        private shouldShowPopover(targetOpenState: boolean): void {
44✔
101
            let popoverOpen = false;
824✔
102
            try {
824✔
103
                popoverOpen = this.dialogEl.matches(':popover-open');
824✔
104
                // eslint-disable-next-line no-empty
824✔
105
            } catch (error) {}
824✔
106
            let open = false;
824✔
107
            try {
824✔
108
                open = this.dialogEl.matches(':open');
824✔
109
                // eslint-disable-next-line no-empty
824✔
110
            } catch (error) {}
824✔
111
            if (
824✔
112
                targetOpenState &&
824✔
113
                this.open === targetOpenState &&
413✔
114
                !popoverOpen &&
380✔
115
                !open &&
378✔
116
                this.isConnected
378✔
117
            ) {
824✔
118
                this.dialogEl.showPopover();
378✔
119
                this.managePosition();
378✔
120
            }
378✔
121
        }
824✔
122

44✔
123
        protected override async ensureOnDOM(
44✔
124
            targetOpenState: boolean
827✔
125
        ): Promise<void> {
827✔
126
            await nextFrame();
827✔
127
            if (!supportsOverlayAuto) {
827✔
128
                await this.shouldHidePopover(targetOpenState);
×
129
            }
✔
130
            this.shouldShowPopover(targetOpenState);
824✔
131
            await nextFrame();
824✔
132
        }
827✔
133

44✔
134
        protected override async makeTransition(
44✔
135
            targetOpenState: boolean
778✔
136
        ): Promise<HTMLElement | null> {
778✔
137
            if (this.open !== targetOpenState) {
778✔
138
                return null;
×
139
            }
×
140
            let focusEl = null as HTMLElement | null;
778✔
141
            const start = (el: OpenableElement, index: number) => (): void => {
778✔
142
                el.open = targetOpenState;
778✔
143
                if (index === 0) {
778✔
144
                    const event = targetOpenState
778✔
145
                        ? BeforetoggleOpenEvent
376✔
146
                        : BeforetoggleClosedEvent;
402✔
147
                    this.dispatchEvent(new event());
778✔
148
                }
778✔
149
                if (!targetOpenState) {
778✔
150
                    return;
402✔
151
                }
402✔
152
                if (el.matches(userFocusableSelector)) {
778✔
153
                    focusEl = el;
×
154
                }
✔
155
                focusEl = focusEl || firstFocusableIn(el);
376✔
156
                if (focusEl) {
778✔
157
                    return;
111✔
158
                }
111✔
159
                const childSlots = el.querySelectorAll('slot');
265✔
160
                childSlots.forEach((slot) => {
265✔
161
                    if (!focusEl) {
193✔
162
                        focusEl = firstFocusableSlottedIn(slot);
193✔
163
                    }
193✔
164
                });
265✔
165
            };
778✔
166
            const finish =
778✔
167
                (el: OpenableElement, index: number) =>
778✔
168
                async (): Promise<void> => {
778✔
169
                    if (this.open !== targetOpenState) {
777✔
170
                        return;
15✔
171
                    }
15✔
172
                    const eventName = targetOpenState
762✔
173
                        ? 'sp-opened'
361✔
174
                        : 'sp-closed';
401✔
175
                    if (index > 0) {
777✔
176
                        el.dispatchEvent(
×
177
                            new OverlayStateEvent(eventName, this, {
×
178
                                interaction: this.type,
×
179
                                publish: false,
×
180
                            })
×
181
                        );
×
182
                        return;
×
183
                    }
✔
184
                    const reportChange = async (): Promise<void> => {
762✔
185
                        if (this.open !== targetOpenState) {
762✔
186
                            return;
×
187
                        }
×
188
                        await nextFrame();
762✔
189
                        const hasVirtualTrigger =
761✔
190
                            this.triggerElement instanceof VirtualTrigger;
761✔
191
                        this.dispatchEvent(
761✔
192
                            new OverlayStateEvent(eventName, this, {
761✔
193
                                interaction: this.type,
761✔
194
                                publish: hasVirtualTrigger,
761✔
195
                            })
761✔
196
                        );
761✔
197
                        el.dispatchEvent(
761✔
198
                            new OverlayStateEvent(eventName, this, {
761✔
199
                                interaction: this.type,
761✔
200
                                publish: false,
761✔
201
                            })
761✔
202
                        );
761✔
203
                        if (this.triggerElement && !hasVirtualTrigger) {
762✔
204
                            (this.triggerElement as HTMLElement).dispatchEvent(
695✔
205
                                new OverlayStateEvent(eventName, this, {
695✔
206
                                    interaction: this.type,
695✔
207
                                    publish: true,
695✔
208
                                })
695✔
209
                            );
695✔
210
                        }
695✔
211
                        this.state = targetOpenState ? 'opened' : 'closed';
762✔
212
                        this.returnFocus();
762✔
213
                        // Ensure layout and paint are done and the Overlay is still closed before removing the slottable request.
762✔
214
                        await nextFrame();
762✔
215
                        await nextFrame();
755✔
216
                        if (
742✔
217
                            targetOpenState === this.open &&
742✔
218
                            targetOpenState === false
460✔
219
                        ) {
762✔
220
                            this.requestSlottable();
351✔
221
                        }
351✔
222
                    };
762✔
223
                    if (this.open !== targetOpenState) {
776✔
224
                        return;
×
225
                    }
✔
226
                    const open = isOpen(this.dialogEl);
762✔
227
                    if (targetOpenState !== true && open && this.isConnected) {
777✔
228
                        this.dialogEl.addEventListener(
186✔
229
                            'beforetoggle',
186✔
230
                            () => {
186✔
231
                                reportChange();
186✔
232
                            },
186✔
233
                            { once: true }
186✔
234
                        );
186✔
235
                        this.dialogEl.hidePopover();
186✔
236
                    } else {
771✔
237
                        reportChange();
576✔
238
                    }
576✔
239
                };
777✔
240
            this.elements.forEach((el, index) => {
778✔
241
                guaranteedAllTransitionend(
778✔
242
                    el,
778✔
243
                    start(el, index),
778✔
244
                    finish(el, index)
778✔
245
                );
778✔
246
            });
778✔
247
            return focusEl;
778✔
248
        }
778✔
249
    }
44✔
250
    return OverlayWithPopover;
44✔
251
}
44✔
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