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

adobe / spectrum-web-components / 12660154690

07 Jan 2025 10:10PM UTC coverage: 97.416% (-0.8%) from 98.209%
12660154690

Pull #4690

github

web-flow
Merge 168797a23 into 5bf31e817
Pull Request #4690: fix(OverlayTrigger): conditionally attach slotchange listener

4992 of 5291 branches covered (94.35%)

Branch coverage included in aggregate %.

20 of 20 new or added lines in 1 file covered. (100.0%)

278 existing lines in 8 files now uncovered.

32743 of 33445 relevant lines covered (97.9%)

373.37 hits per line

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

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

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

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

40✔
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

40✔
48
export function OverlayPopover<T extends Constructor<AbstractOverlay>>(
40✔
49
    constructor: T
40✔
50
): T & Constructor<SpectrumElement> {
40✔
51
    class OverlayWithPopover extends constructor {
40✔
52
        protected override async manageDelay(
40✔
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

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

40✔
100
        private shouldShowPopover(targetOpenState: boolean): void {
40✔
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

40✔
123
        protected override async ensureOnDOM(
40✔
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

40✔
134
        protected override async makeTransition(
40✔
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
273✔
146
                        : BeforetoggleClosedEvent;
290✔
147
                    this.dispatchEvent(new event());
563✔
148
                }
563✔
149
                if (!targetOpenState) {
563✔
150
                    return;
290✔
151
                }
290✔
152
                if (el.matches(userFocusableSelector)) {
563✔
153
                    focusEl = el;
×
154
                }
✔
155
                focusEl = focusEl || firstFocusableIn(el);
273✔
156
                if (focusEl) {
563✔
157
                    return;
162✔
158
                }
162✔
159
                const childSlots = el.querySelectorAll('slot');
111✔
160
                childSlots.forEach((slot) => {
111✔
161
                    if (!focusEl) {
82✔
162
                        focusEl = firstFocusableSlottedIn(slot);
82✔
163
                    }
82✔
164
                });
111✔
165
            };
563✔
166
            const finish =
563✔
167
                (el: OpenableElement, index: number) =>
563✔
168
                async (): Promise<void> => {
563✔
169
                    if (this.open !== targetOpenState) {
559✔
170
                        return;
2✔
171
                    }
2✔
172
                    const eventName = targetOpenState
557✔
173
                        ? 'sp-opened'
270✔
174
                        : 'sp-closed';
287✔
175
                    if (index > 0) {
559✔
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();
553✔
216
                        if (
550✔
217
                            targetOpenState === this.open &&
550✔
218
                            targetOpenState === false
335✔
219
                        ) {
557✔
220
                            this.requestSlottable();
253✔
221
                        }
253✔
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) {
559✔
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
                };
559✔
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
    }
40✔
250
    return OverlayWithPopover;
40✔
251
}
40✔
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