• 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

84.91
/packages/overlay/src/OverlayPopover.ts
1
/*
38✔
2
Copyright 2020 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
    firstFocusableIn,
38✔
14
    firstFocusableSlottedIn,
38✔
15
} from '@spectrum-web-components/shared/src/first-focusable-in.js';
38✔
16
import type { SpectrumElement } from '@spectrum-web-components/base';
38✔
17
import { VirtualTrigger } from './VirtualTrigger.js';
38✔
18
import { Constructor, OpenableElement } from './overlay-types.js';
38✔
19
import {
38✔
20
    guaranteedAllTransitionend,
38✔
21
    nextFrame,
38✔
22
    overlayTimer,
38✔
23
} from './AbstractOverlay.js';
38✔
24
import {
38✔
25
    BeforetoggleClosedEvent,
38✔
26
    BeforetoggleOpenEvent,
38✔
27
    OverlayStateEvent,
38✔
28
} from './events.js';
38✔
29
import type { AbstractOverlay } from './AbstractOverlay.js';
38✔
30
import { userFocusableSelector } from '@spectrum-web-components/shared';
38✔
31

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

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

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

38✔
67
        /**
38✔
68
         * A popover should be hidden _after_ it is no longer on top-layer because
38✔
69
         * the position metrics will have changed from when it was originally positioned.
38✔
70
         */
38✔
71
        private async shouldHidePopover(
38✔
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

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

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

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