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

adobe / spectrum-web-components / 14094674923

26 Mar 2025 10:27PM CUT coverage: 86.218% (-11.8%) from 98.002%
14094674923

Pull #5221

github

web-flow
Merge 2a1ea92e7 into 3184c1e6a
Pull Request #5221: RFC | leverage css module imports in components

1737 of 2032 branches covered (85.48%)

Branch coverage included in aggregate %.

14184 of 16434 relevant lines covered (86.31%)

85.29 hits per line

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

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

2✔
7
Unless required by applicable law or agreed to in writing, software distributed under
2✔
8
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
2✔
9
OF ANY KIND, either express or implied. See the License for the specific language
2✔
10
governing permissions and limitations under the License.
2✔
11
*/
2✔
12
import { SpectrumElement } from '@spectrum-web-components/base';
2✔
13
import { reparentChildren } from '@spectrum-web-components/shared/src/reparent-children.js';
2✔
14

2✔
15
import type {
2✔
16
    OpenableElement,
2✔
17
    OverlayOptions,
2✔
18
    OverlayOptionsV1,
2✔
19
    OverlayState,
2✔
20
    OverlayTypes,
2✔
21
    Placement,
2✔
22
    TriggerInteractionsV1,
2✔
23
} from './overlay-types.js';
2✔
24
import type { Overlay } from './Overlay.js';
2✔
25
import type { VirtualTrigger } from './VirtualTrigger.js';
2✔
26
import { OverlayTimer } from './overlay-timer.js';
2✔
27
import type { PlacementController } from './PlacementController.js';
2✔
28
import type { ElementResolutionController } from '@spectrum-web-components/reactive-controllers/src/ElementResolution.js';
2✔
29

2✔
30
export const overlayTimer = new OverlayTimer();
2✔
31

2✔
32
export const noop = (): void => {
2✔
33
    return;
×
34
};
×
35

2✔
36
/**
2✔
37
 * Apply a "transitionend" listener to an element that may not transition but
2✔
38
 * guarantee the callback will be fired either way.
2✔
39
 *
2✔
40
 * @param el {HTMLElement} - Target of the "transition" listeners.
2✔
41
 * @param action {Function} - Method to trigger the "transition".
2✔
42
 * @param cb {Function} - Callback to trigger when the "transition" has ended.
2✔
43
 */
2✔
44
export const guaranteedAllTransitionend = (
2✔
45
    el: HTMLElement,
×
46
    action: () => void,
×
47
    cb: () => void
×
48
): void => {
×
49
    const abortController = new AbortController();
×
50
    const runningTransitions = new Map<string, number>();
×
51
    const cleanup = (): void => {
×
52
        abortController.abort();
×
53
        cb();
×
54
    };
×
55
    let guarantee2: number;
×
56
    let guarantee3: number;
×
57
    // WebKit fires `transitionrun` a little earlier, the multiple guarantees here
×
58
    // allow WebKit to be caught, but doesn't remove the animation listener until
×
59
    // after it would have fired in Chromium.
×
60
    const guarantee1 = requestAnimationFrame(() => {
×
61
        guarantee2 = requestAnimationFrame(() => {
×
62
            guarantee3 = requestAnimationFrame(() => {
×
63
                cleanup();
×
64
            });
×
65
        });
×
66
    });
×
67
    const handleTransitionend = (event: TransitionEvent): void => {
×
68
        if (event.target !== el) {
×
69
            return;
×
70
        }
×
71
        runningTransitions.set(
×
72
            event.propertyName,
×
73
            (runningTransitions.get(event.propertyName) as number) - 1
×
74
        );
×
75
        if (!runningTransitions.get(event.propertyName)) {
×
76
            runningTransitions.delete(event.propertyName);
×
77
        }
×
78
        if (runningTransitions.size === 0) {
×
79
            cleanup();
×
80
        }
×
81
    };
×
82
    const handleTransitionrun = (event: TransitionEvent): void => {
×
83
        if (event.target !== el) {
×
84
            return;
×
85
        }
×
86
        if (!runningTransitions.has(event.propertyName)) {
×
87
            runningTransitions.set(event.propertyName, 0);
×
88
        }
×
89
        runningTransitions.set(
×
90
            event.propertyName,
×
91
            (runningTransitions.get(event.propertyName) as number) + 1
×
92
        );
×
93
        cancelAnimationFrame(guarantee1);
×
94
        cancelAnimationFrame(guarantee2);
×
95
        cancelAnimationFrame(guarantee3);
×
96
    };
×
97
    el.addEventListener('transitionrun', handleTransitionrun, {
×
98
        signal: abortController.signal,
×
99
    });
×
100
    el.addEventListener('transitionend', handleTransitionend, {
×
101
        signal: abortController.signal,
×
102
    });
×
103
    el.addEventListener('transitioncancel', handleTransitionend, {
×
104
        signal: abortController.signal,
×
105
    });
×
106
    action();
×
107
};
×
108

2✔
109
export function nextFrame(): Promise<void> {
2✔
110
    return new Promise((res) => requestAnimationFrame(() => res()));
×
111
}
×
112

2✔
113
/**
2✔
114
 * Abstract Overlay base class so that property tyings and imperative API
2✔
115
 * interfaces can be held separate from the actual class definition.
2✔
116
 */
2✔
117
export class AbstractOverlay extends SpectrumElement {
2✔
118
    protected async applyFocus(
×
119
        _targetOpenState: boolean,
×
120
        _focusEl: HTMLElement | null
×
121
    ): Promise<void> {
×
122
        return;
×
123
    }
×
124
    /* c8 ignore next 6 */
2✔
125
    get delayed(): boolean {
2✔
126
        return false;
2✔
127
    }
2✔
128
    set delayed(_delayed: boolean) {
2✔
129
        return;
2✔
130
    }
2✔
131
    dialogEl!: HTMLDialogElement & {
×
132
        showPopover(): void;
×
133
        hidePopover(): void;
×
134
    };
×
135
    /* c8 ignore next 6 */
2✔
136
    get disabled(): boolean {
2✔
137
        return false;
2✔
138
    }
2✔
139
    set disabled(_disabled: boolean) {
2✔
140
        return;
2✔
141
    }
2✔
142
    dispose = noop;
×
143
    protected get elementResolver(): ElementResolutionController {
×
144
        return this._elementResolver;
×
145
    }
×
146
    protected set elementResolver(controller) {
×
147
        this._elementResolver = controller;
×
148
    }
×
149
    protected _elementResolver!: ElementResolutionController;
×
150
    /* c8 ignore next 3 */
2✔
151
    protected async ensureOnDOM(_targetOpenState: boolean): Promise<void> {
2✔
152
        return;
2✔
153
    }
2✔
154
    elements!: OpenableElement[];
×
155
    /* c8 ignore next 5 */
2✔
156
    protected async makeTransition(
2✔
157
        _targetOpenState: boolean
2✔
158
    ): Promise<HTMLElement | null> {
2✔
159
        return null;
2✔
160
    }
2✔
161
    protected async manageDelay(_targetOpenState: boolean): Promise<void> {
×
162
        return;
×
163
    }
×
164
    /* c8 ignore next 3 */
2✔
165
    protected async manageDialogOpen(): Promise<void> {
2✔
166
        return;
2✔
167
    }
2✔
168
    /* c8 ignore next 3 */
2✔
169
    protected async managePopoverOpen(): Promise<void> {
2✔
170
        return;
2✔
171
    }
2✔
172
    /* c8 ignore next 3 */
2✔
173
    protected managePosition(): void {
2✔
174
        return;
2✔
175
    }
2✔
176
    protected offset: number | [number, number] = 0;
×
177
    /* c8 ignore next 6 */
2✔
178
    get open(): boolean {
2✔
179
        return false;
2✔
180
    }
2✔
181
    set open(_open: boolean) {
2✔
182
        return;
2✔
183
    }
2✔
184
    placement?: Placement;
×
185
    protected get placementController(): PlacementController {
×
186
        return this._placementController;
×
187
    }
×
188
    protected set placementController(controller) {
×
189
        this._placementController = controller;
×
190
    }
×
191
    protected _placementController!: PlacementController;
×
192
    receivesFocus!: 'true' | 'false' | 'auto';
×
193
    protected requestSlottable(): void {}
×
194
    protected returnFocus(): void {
×
195
        return;
×
196
    }
×
197
    /* c8 ignore next 6 */
2✔
198
    get state(): OverlayState {
2✔
199
        return 'closed';
2✔
200
    }
2✔
201
    set state(_state: OverlayState) {
2✔
202
        return;
2✔
203
    }
2✔
204
    protected _state!: OverlayState;
×
205
    triggerElement!: HTMLElement | VirtualTrigger | null;
×
206
    type!: OverlayTypes;
×
207
    willPreventClose = false;
×
208
    /* c8 ignore next 3 */
2✔
209
    public manuallyKeepOpen(): void {
2✔
210
        return;
2✔
211
    }
2✔
212

2✔
213
    public static update(): void {
2✔
214
        const overlayUpdateEvent = new CustomEvent('sp-update-overlays', {
×
215
            bubbles: true,
×
216
            composed: true,
×
217
            cancelable: true,
×
218
        });
×
219
        document.dispatchEvent(overlayUpdateEvent);
×
220
    }
×
221

2✔
222
    /**
2✔
223
     * Overloaded imperative API entry point that allows for both the pre-0.37.0
2✔
224
     * argument signature as well as the post-0.37.0 signature. This allows for
2✔
225
     * consumers to continue to leverage it as they had been in previous releases
2✔
226
     * while also surfacing the more feature-rich API that has been made available.
2✔
227
     */
2✔
228
    public static async open(
2✔
229
        trigger: HTMLElement,
2✔
230
        interaction: TriggerInteractionsV1,
2✔
231
        content: HTMLElement,
2✔
232
        optionsV1: OverlayOptionsV1
2✔
233
    ): Promise<() => void>;
2✔
234
    public static async open(
2✔
235
        content: HTMLElement,
2✔
236
        options?: OverlayOptions
2✔
237
    ): Promise<Overlay>;
2✔
238
    public static async open(
2✔
239
        triggerOrContent: HTMLElement,
×
240
        interactionOrOptions:
×
241
            | TriggerInteractionsV1
×
242
            | OverlayOptions
×
243
            | undefined,
×
244
        content?: HTMLElement,
×
245
        optionsV1?: OverlayOptionsV1
×
246
    ): Promise<Overlay | (() => void)> {
×
247
        await import('@spectrum-web-components/overlay/sp-overlay.js');
×
248
        const v2 = arguments.length === 2;
×
249
        const overlayContent = content || triggerOrContent;
×
250
        // Use the `this` from the `static` method context rather than a
×
251
        // specific imported constructor to prevent opening a circular dependency.
×
252
        const overlay = new this() as Overlay;
×
253
        let restored = false;
×
254
        overlay.dispose = () => {
×
255
            overlay.addEventListener('sp-closed', () => {
×
256
                if (!restored) {
×
257
                    restoreContent();
×
258
                    restored = true;
×
259
                }
×
260
                requestAnimationFrame(() => {
×
261
                    overlay.remove();
×
262
                });
×
263
            });
×
264
            overlay.open = false;
×
265
            overlay.dispose = noop;
×
266
        };
×
267
        /**
×
268
         * Since content must exist in an <sp-overlay>, we need a way to get it there.
×
269
         * The best & most-direct way is to declaratively use an <sp-overlay> element,
×
270
         * but for imperative users, we'll reparent content into an overlay that we've created for them.
×
271
         **/
×
272
        const restoreContent = reparentChildren([overlayContent], overlay, {
×
273
            position: 'beforeend',
×
274
            prepareCallback: (el) => {
×
275
                // Ensure that content to be overlaid is no longer targetted to a specific `slot`.
×
276
                // This allow for it to be visible in the overlaid context.
×
277
                const slot = el.slot;
×
278
                el.removeAttribute('slot');
×
279
                return () => {
×
280
                    el.slot = slot;
×
281
                };
×
282
            },
×
283
        });
×
284

×
285
        const v1 = !v2 && overlayContent && optionsV1;
×
286
        if (v1) {
×
287
            if (window.__swc.DEBUG) {
×
288
                window.__swc.warn(
×
289
                    overlay,
×
290
                    `You are interacting with an ${overlay.localName} element via a deprecated imperative API. This API will be removed in a future version of the SWC library. Consider leveraging an ${overlay.localName} directly.`,
×
291
                    'https://opensource.adobe.com/spectrum-web-components/components/overlay/',
×
292
                    { level: 'deprecation' }
×
293
                );
×
294
            }
×
295
            const trigger = triggerOrContent;
×
296
            const interaction = interactionOrOptions;
×
297
            const options = optionsV1;
×
298
            AbstractOverlay.applyOptions(overlay, {
×
299
                ...options,
×
300
                delayed:
×
301
                    options.delayed || overlayContent.hasAttribute('delayed'),
×
302
                trigger: options.virtualTrigger || trigger,
×
303
                type:
×
304
                    interaction === 'modal'
×
305
                        ? 'modal'
×
306
                        : interaction === 'hover'
×
307
                          ? 'hint'
×
308
                          : 'auto',
×
309
            });
×
310
            trigger.insertAdjacentElement('afterend', overlay);
×
311
            await overlay.updateComplete;
×
312
            overlay.open = true;
×
313
            return overlay.dispose;
×
314
        }
×
315

×
316
        const options = interactionOrOptions as OverlayOptions;
×
317
        overlay.append(overlayContent);
×
318
        AbstractOverlay.applyOptions(overlay, {
×
319
            ...options,
×
320
            delayed: options.delayed || overlayContent.hasAttribute('delayed'),
×
321
        });
×
322
        overlay.updateComplete.then(() => {
×
323
            // Do we want to "open" this path, or leave that to the consumer?
×
324
            overlay.open = true;
×
325
        });
×
326
        return overlay;
×
327
    }
×
328

2✔
329
    static applyOptions(
2✔
330
        overlay: AbstractOverlay,
×
331
        options: OverlayOptions
×
332
    ): void {
×
333
        overlay.delayed = !!options.delayed;
×
334
        overlay.receivesFocus = options.receivesFocus ?? 'auto';
×
335
        overlay.triggerElement = options.trigger || null;
×
336
        overlay.type = options.type || 'modal';
×
337
        overlay.offset = options.offset ?? 0;
×
338
        overlay.placement = options.placement;
×
339
        overlay.willPreventClose = !!options.notImmediatelyClosable;
×
340
    }
×
341

2✔
342
    override disconnectedCallback(): void {
2✔
343
        super.disconnectedCallback();
×
344
    }
×
345
}
2✔
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