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

adobe / spectrum-web-components / 12802187570

16 Jan 2025 04:36AM UTC coverage: 98.206% (-0.003%) from 98.209%
12802187570

Pull #5019

github

web-flow
Merge 012b0d7d4 into cee6892e5
Pull Request #5019: chore: fix update-spectrum-css script

5151 of 5424 branches covered (94.97%)

Branch coverage included in aggregate %.

33006 of 33430 relevant lines covered (98.73%)

380.51 hits per line

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

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

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

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

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