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

adobe / spectrum-web-components / 14108835255

27 Mar 2025 02:16PM UTC coverage: 97.999%. Remained the same
14108835255

Pull #5233

github

web-flow
Merge a4ea323ab into a4de4c76c
Pull Request #5233: chore: update dependency @spectrum-css/link to v7.1.0

5326 of 5623 branches covered (94.72%)

Branch coverage included in aggregate %.

33711 of 34211 relevant lines covered (98.54%)

644.49 hits per line

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

91.03
/packages/overlay/src/OverlayDialog.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 type { SpectrumElement } from '@spectrum-web-components/base';
44✔
13
import {
44✔
14
    firstFocusableIn,
44✔
15
    firstFocusableSlottedIn,
44✔
16
} from '@spectrum-web-components/shared/src/first-focusable-in.js';
44✔
17
import { VirtualTrigger } from './VirtualTrigger.js';
44✔
18
import { Constructor, OpenableElement } from './overlay-types.js';
44✔
19
import { guaranteedAllTransitionend, nextFrame } from './AbstractOverlay.js';
44✔
20
import {
44✔
21
    BeforetoggleClosedEvent,
44✔
22
    BeforetoggleOpenEvent,
44✔
23
    OverlayStateEvent,
44✔
24
} from './events.js';
44✔
25
import type { AbstractOverlay } from './AbstractOverlay.js';
44✔
26
import { userFocusableSelector } from '@spectrum-web-components/shared';
44✔
27

44✔
28
export function OverlayDialog<T extends Constructor<AbstractOverlay>>(
44✔
29
    constructor: T
44✔
30
): T & Constructor<SpectrumElement> {
44✔
31
    class OverlayWithDialog extends constructor {
44✔
32
        protected override async manageDialogOpen(): Promise<void> {
44✔
33
            const targetOpenState = this.open;
100✔
34
            await nextFrame();
100✔
35
            await this.managePosition();
99✔
36
            if (this.open !== targetOpenState) {
100✔
37
                return;
×
38
            }
✔
39
            const focusEl = await this.dialogMakeTransition(targetOpenState);
99✔
40
            if (this.open !== targetOpenState) {
100✔
41
                return;
×
42
            }
✔
43
            await this.dialogApplyFocus(targetOpenState, focusEl);
99✔
44
        }
100✔
45

44✔
46
        protected async dialogMakeTransition(
44✔
47
            targetOpenState: boolean
99✔
48
        ): Promise<HTMLElement | null> {
99✔
49
            let focusEl = null as HTMLElement | null;
99✔
50
            const start =
99✔
51
                (el: OpenableElement, index: number) =>
99✔
52
                async (): Promise<void> => {
99✔
53
                    el.open = targetOpenState;
99✔
54
                    if (!targetOpenState) {
99✔
55
                        const close = (): void => {
49✔
56
                            el.removeEventListener('close', close);
3✔
57
                            finish(el, index);
3✔
58
                        };
3✔
59
                        el.addEventListener('close', close);
49✔
60
                    }
49✔
61
                    if (index > 0) {
99✔
62
                        // Announce workflow on the first element _only_.
×
63
                        return;
×
64
                    }
×
65
                    const event = targetOpenState
99✔
66
                        ? BeforetoggleOpenEvent
50✔
67
                        : BeforetoggleClosedEvent;
49✔
68
                    this.dispatchEvent(new event());
99✔
69
                    if (!targetOpenState) {
99✔
70
                        // Show/focus workflow when opening _only_.
49✔
71
                        return;
49✔
72
                    }
49✔
73
                    if (el.matches(userFocusableSelector)) {
96✔
74
                        focusEl = el;
3✔
75
                    }
3✔
76
                    focusEl = focusEl || firstFocusableIn(el);
53✔
77
                    if (!focusEl) {
99✔
78
                        const childSlots = el.querySelectorAll('slot');
31✔
79
                        childSlots.forEach((slot) => {
31✔
80
                            if (!focusEl) {
4✔
81
                                focusEl = firstFocusableSlottedIn(slot);
4✔
82
                            }
4✔
83
                        });
31✔
84
                    }
31✔
85
                    if (!this.isConnected || this.dialogEl.open) {
99✔
86
                        // In both of these cases the browser will error.
×
87
                        // You can neither "reopen" a <dialog> or open one that is not on the DOM.
×
88
                        return;
×
89
                    }
✔
90
                    this.dialogEl.showModal();
50✔
91
                };
99✔
92
            const finish = (el: OpenableElement, index: number) => (): void => {
99✔
93
                if (this.open !== targetOpenState) {
100✔
94
                    return;
×
95
                }
×
96
                const eventName = targetOpenState ? 'sp-opened' : 'sp-closed';
100✔
97
                if (index > 0) {
100✔
98
                    el.dispatchEvent(
×
99
                        new OverlayStateEvent(eventName, this, {
×
100
                            interaction: this.type,
×
101
                            publish: false,
×
102
                        })
×
103
                    );
×
104
                    return;
×
105
                }
×
106
                if (!this.isConnected || targetOpenState !== this.open) {
100✔
107
                    // Don't lead into the `.close()` workflow if not connected to the DOM.
19✔
108
                    // The browser will error in this case.
19✔
109
                    return;
19✔
110
                }
19✔
111
                const reportChange = async (): Promise<void> => {
81✔
112
                    const hasVirtualTrigger =
81✔
113
                        this.triggerElement instanceof VirtualTrigger;
81✔
114
                    this.dispatchEvent(
81✔
115
                        new OverlayStateEvent(eventName, this, {
81✔
116
                            interaction: this.type,
81✔
117
                            publish: hasVirtualTrigger,
81✔
118
                        })
81✔
119
                    );
81✔
120
                    el.dispatchEvent(
81✔
121
                        new OverlayStateEvent(eventName, this, {
81✔
122
                            interaction: this.type,
81✔
123
                            publish: false,
81✔
124
                        })
81✔
125
                    );
81✔
126
                    if (this.triggerElement && !hasVirtualTrigger) {
81✔
127
                        (this.triggerElement as HTMLElement).dispatchEvent(
53✔
128
                            new OverlayStateEvent(eventName, this, {
53✔
129
                                interaction: this.type,
53✔
130
                                publish: true,
53✔
131
                            })
53✔
132
                        );
53✔
133
                    }
53✔
134
                    this.state = targetOpenState ? 'opened' : 'closed';
81✔
135
                    this.returnFocus();
81✔
136
                    // Ensure layout and paint are done and the Overlay is still closed before removing the slottable request.
81✔
137
                    await nextFrame();
81✔
138
                    await nextFrame();
81✔
139
                    if (
79✔
140
                        targetOpenState === this.open &&
79✔
141
                        targetOpenState === false
51✔
142
                    ) {
81✔
143
                        this.requestSlottable();
27✔
144
                    }
27✔
145
                };
81✔
146
                if (!targetOpenState && this.dialogEl.open) {
100✔
147
                    this.dialogEl.addEventListener(
24✔
148
                        'close',
24✔
149
                        () => {
24✔
150
                            reportChange();
24✔
151
                        },
24✔
152
                        { once: true }
24✔
153
                    );
24✔
154
                    this.dialogEl.close();
24✔
155
                } else {
92✔
156
                    reportChange();
57✔
157
                }
57✔
158
            };
100✔
159
            this.elements.forEach((el, index) => {
99✔
160
                guaranteedAllTransitionend(
99✔
161
                    el,
99✔
162
                    start(el, index),
99✔
163
                    finish(el, index)
99✔
164
                );
99✔
165
            });
99✔
166
            return focusEl;
99✔
167
        }
99✔
168

44✔
169
        protected async dialogApplyFocus(
44✔
170
            targetOpenState: boolean,
99✔
171
            focusEl: HTMLElement | null
99✔
172
        ): Promise<void> {
99✔
173
            /**
99✔
174
             * Focus should be handled natively in `<dialog>` elements when leveraging `.showModal()`, but it's NOT.
99✔
175
             * - webkit bug: https://bugs.webkit.org/show_bug.cgi?id=255507
99✔
176
             * - firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1828398
99✔
177
             **/
99✔
178
            this.applyFocus(targetOpenState, focusEl);
99✔
179
        }
99✔
180
    }
44✔
181
    return OverlayWithDialog;
44✔
182
}
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