• 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

85.87
/packages/overlay/src/OverlayStack.ts
1
/*
40✔
2
Copyright 2023 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
Unless required by applicable law or agreed to in writing, software distributed under
40✔
7
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
40✔
8
OF ANY KIND, either express or implied. See the License for the specific language
40✔
9
governing permissions and limitations under the License.
40✔
10
*/
40✔
11

40✔
12
import { Overlay } from './Overlay.js';
40✔
13

40✔
14
const supportsPopover = 'showPopover' in document.createElement('div');
40✔
15

40✔
16
class OverlayStack {
40✔
17
    private get document(): Document {
40✔
18
        return this.root.ownerDocument /* c8 ignore next */ || document;
40✔
19
    }
40✔
20

40✔
21
    private pointerdownPath?: EventTarget[];
40✔
22

40✔
23
    private lastOverlay?: Overlay;
40✔
24

40✔
25
    private root: HTMLElement = document.body;
40✔
26

40✔
27
    stack: Overlay[] = [];
40✔
28

40✔
29
    constructor() {
40✔
30
        this.bindEvents();
40✔
31
    }
40✔
32

40✔
33
    bindEvents(): void {
40✔
34
        this.document.addEventListener('pointerdown', this.handlePointerdown);
40✔
35
        this.document.addEventListener('pointerup', this.handlePointerup);
40✔
36
        this.document.addEventListener('keydown', this.handleKeydown);
40✔
37
    }
40✔
38

40✔
39
    private closeOverlay(overlay: Overlay): void {
40✔
40
        const overlayIndex = this.stack.indexOf(overlay);
359✔
41
        if (overlayIndex > -1) {
359✔
42
            this.stack.splice(overlayIndex, 1);
313✔
43
        }
313✔
44
        overlay.open = false;
359✔
45
    }
359✔
46

40✔
47
    /**
40✔
48
     * Cach the `pointerdownTarget` for later testing
40✔
49
     *
40✔
50
     * @param event {ClickEvent}
40✔
51
     */
40✔
52
    handlePointerdown = (event: Event): void => {
40✔
53
        this.pointerdownPath = event.composedPath();
160✔
54
        this.lastOverlay = this.stack[this.stack.length - 1];
160✔
55
    };
160✔
56

40✔
57
    /**
40✔
58
     * Close all overlays that are not ancestors of this click event
40✔
59
     *
40✔
60
     * @param event {ClickEvent}
40✔
61
     */
40✔
62
    handlePointerup = (): void => {
40✔
63
        // Test against the composed path in `pointerdown` in case the visitor moved their
482✔
64
        // pointer during the course of the interaction.
482✔
65
        // Ensure that this value is cleared even if the work in this method goes undone.
482✔
66
        const composedPath = this.pointerdownPath;
482✔
67
        this.pointerdownPath = undefined;
482✔
68
        if (!this.stack.length) return;
482✔
69
        if (!composedPath?.length) return;
482✔
70
        const lastOverlay = this.lastOverlay;
46✔
71
        this.lastOverlay = undefined;
46✔
72

46✔
73
        const lastIndex = this.stack.length - 1;
46✔
74
        const nonAncestorOverlays = this.stack.filter((overlay, i) => {
46✔
75
            const inStack = composedPath.find(
46✔
76
                (el) =>
46✔
77
                    // The Overlay is in the stack
647✔
78
                    el === overlay ||
647✔
79
                    // The Overlay trigger is in the stack and the Overlay is a "hint"
604✔
80
                    (el === overlay?.triggerElement &&
604!
81
                        'hint' === overlay?.type) ||
2!
82
                    // The last Overlay in the stack is not the last Overlay at `pointerdown` time and has a
602✔
83
                    // `triggerInteraction` of "longpress", meaning it was opened by this poitner interaction
602✔
84
                    (i === lastIndex &&
602✔
85
                        overlay !== lastOverlay &&
602!
UNCOV
86
                        overlay.triggerInteraction === 'longpress')
×
87
            );
46✔
88
            return (
46✔
89
                !inStack &&
46✔
90
                !overlay.shouldPreventClose() &&
1✔
91
                overlay.type !== 'manual' &&
1!
UNCOV
92
                // Don't close if this overlay is modal and not on top of the overlay stack.
×
UNCOV
93
                !(overlay.type === 'modal' && lastOverlay !== overlay)
×
94
            );
46✔
95
        }) as Overlay[];
46✔
96
        nonAncestorOverlays.reverse();
46✔
97
        nonAncestorOverlays.forEach((overlay) => {
46✔
UNCOV
98
            this.closeOverlay(overlay);
×
UNCOV
99
            let parentToClose = overlay.parentOverlayToForceClose;
×
UNCOV
100
            while (parentToClose) {
×
101
                this.closeOverlay(parentToClose);
×
102
                parentToClose = parentToClose.parentOverlayToForceClose;
×
103
            }
×
104
        });
46✔
105
    };
482✔
106

40✔
107
    handleBeforetoggle = (event: Event): void => {
40✔
108
        const { target, newState: open } = event as Event & {
313✔
109
            newState: string;
313✔
110
        };
313✔
111
        if (open === 'open') return;
313✔
112
        this.closeOverlay(target as Overlay);
20✔
113
    };
313✔
114

40✔
115
    private handleKeydown = (event: KeyboardEvent): void => {
40✔
116
        if (event.code !== 'Escape') return;
276✔
117
        if (!this.stack.length) return;
6✔
118
        const last = this.stack[this.stack.length - 1];
5✔
119
        if (last?.type === 'page') {
276!
120
            event.preventDefault();
1✔
121
            return;
1✔
122
        }
1✔
123
        if (supportsPopover) return;
4!
124
        if (last?.type === 'manual') {
276!
125
            // Manual Overlays should not close on "light dismiss".
×
126
            return;
×
127
        }
×
128

×
129
        if (!last) return;
×
130
        this.closeOverlay(last);
×
131
    };
276✔
132

40✔
133
    /**
40✔
134
     * Get an array of Overlays that all share the same trigger element.
40✔
135
     *
40✔
136
     * @param triggerElement {HTMLELement}
40✔
137
     * @returns {Overlay[]}
40✔
138
     */
40✔
139
    overlaysByTriggerElement(triggerElement: HTMLElement): Overlay[] {
40✔
140
        return this.stack.filter(
×
141
            (overlay) => overlay.triggerElement === triggerElement
×
142
        );
×
143
    }
×
144

40✔
145
    /**
40✔
146
     * When overlays are added manage the open state of exisiting overlays appropriately:
40✔
147
     * - 'modal': should close other non-'modal' and non-'manual' overlays
40✔
148
     * - 'page': should close other non-'modal' and non-'manual' overlays
40✔
149
     * - 'auto': should close other 'auto' overlays and other 'hint' overlays, but not 'manual' overlays
40✔
150
     * - 'manual': shouldn't close other overlays
40✔
151
     * - 'hint': shouldn't close other overlays and give way to all other overlays on a trigger
40✔
152
     */
40✔
153
    add(overlay: Overlay): void {
40✔
154
        if (this.stack.includes(overlay)) {
313!
UNCOV
155
            const overlayIndex = this.stack.indexOf(overlay);
×
UNCOV
156
            if (overlayIndex > -1) {
×
UNCOV
157
                this.stack.splice(overlayIndex, 1);
×
UNCOV
158
                this.stack.push(overlay);
×
UNCOV
159
            }
×
UNCOV
160
            return;
×
UNCOV
161
        }
×
162
        if (
313✔
163
            overlay.type === 'auto' ||
313✔
164
            overlay.type === 'modal' ||
61✔
165
            overlay.type === 'page'
50✔
166
        ) {
313✔
167
            // manage closing open overlays
272✔
168
            const queryPathEventName = 'sp-overlay-query-path';
272✔
169
            const queryPathEvent = new Event(queryPathEventName, {
272✔
170
                composed: true,
272✔
171
                bubbles: true,
272✔
172
            });
272✔
173
            overlay.addEventListener(
272✔
174
                queryPathEventName,
272✔
175
                (event: Event) => {
272✔
176
                    const path = event.composedPath();
272✔
177
                    this.stack.forEach((overlayEl) => {
272✔
178
                        const inPath = path.find((el) => el === overlayEl);
42✔
179
                        if (
42✔
180
                            !inPath &&
42✔
181
                            overlayEl.type !== 'manual' &&
27✔
182
                            overlayEl.type !== 'modal'
25✔
183
                        ) {
42✔
184
                            this.closeOverlay(overlayEl);
24✔
185
                        }
24✔
186
                    });
272✔
187
                },
272✔
188
                { once: true }
272✔
189
            );
272✔
190
            overlay.dispatchEvent(queryPathEvent);
272✔
191
        } else if (overlay.type === 'hint') {
313✔
192
            const hasPrevious = this.stack.some((overlayEl) => {
31✔
193
                return (
2✔
194
                    overlayEl.type !== 'manual' &&
2✔
195
                    overlayEl.triggerElement &&
2✔
UNCOV
196
                    overlayEl.triggerElement === overlay.triggerElement
×
197
                );
2✔
198
            });
31✔
199
            if (hasPrevious) {
31!
200
                overlay.open = false;
×
201
                return;
×
202
            }
×
203
            this.stack.forEach((overlayEl) => {
31✔
204
                if (overlayEl.type === 'hint') {
2✔
205
                    this.closeOverlay(overlayEl);
2✔
206
                }
2✔
207
            });
31✔
208
        }
31✔
209
        requestAnimationFrame(() => {
313✔
210
            this.stack.push(overlay);
313✔
211
            overlay.addEventListener('beforetoggle', this.handleBeforetoggle, {
313✔
212
                once: true,
313✔
213
            });
313✔
214
        });
313✔
215
    }
313✔
216

40✔
217
    remove(overlay: Overlay): void {
40✔
218
        this.closeOverlay(overlay);
313✔
219
    }
313✔
220
}
40✔
221

40✔
222
export const overlayStack = new OverlayStack();
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