• 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

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

20✔
7
Unless required by applicable law or agreed to in writing, software distributed under
20✔
8
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
20✔
9
OF ANY KIND, either express or implied. See the License for the specific language
20✔
10
governing permissions and limitations under the License.
20✔
11
*/
20✔
12

20✔
13
import {
20✔
14
    CSSResultArray,
20✔
15
    html,
20✔
16
    PropertyValues,
20✔
17
    SpectrumElement,
20✔
18
    TemplateResult,
20✔
19
} from '@spectrum-web-components/base';
20✔
20
import {
20✔
21
    property,
20✔
22
    query,
20✔
23
    state,
20✔
24
} from '@spectrum-web-components/base/src/decorators.js';
20✔
25
import type { Placement } from '@floating-ui/dom';
20✔
26

20✔
27
import type { BeforetoggleOpenEvent } from './events.js';
20✔
28
import type { Overlay } from './Overlay.js';
20✔
29
import type { OverlayTriggerInteractions } from './overlay-types';
20✔
30

20✔
31
import overlayTriggerStyles from './overlay-trigger.css.js';
20✔
32

20✔
33
export type OverlayContentTypes = 'click' | 'hover' | 'longpress';
20✔
34

20✔
35
/**
20✔
36
 * @element overlay-trigger
20✔
37
 *
20✔
38
 * @slot trigger - The content that will trigger the various overlays
20✔
39
 * @slot hover-content - The content that will be displayed on hover
20✔
40
 * @slot click-content - The content that will be displayed on click
20✔
41
 * @slot longpress-content - The content that will be displayed on click
20✔
42
 *
20✔
43
 * @fires sp-opened - Announces that the overlay has been opened
20✔
44
 * @fires sp-closed - Announces that the overlay has been closed
20✔
45
 */
20✔
46
export class OverlayTrigger extends SpectrumElement {
20✔
47
    public static override get styles(): CSSResultArray {
188✔
48
        return [overlayTriggerStyles];
188✔
49
    }
188✔
50

188✔
51
    @property()
188✔
52
    content = 'click hover longpress';
188✔
53

188✔
54
    /**
188✔
55
     * @type {"top" | "top-start" | "top-end" | "right" | "right-start" | "right-end" | "bottom" | "bottom-start" | "bottom-end" | "left" | "left-start" | "left-end"}
188✔
56
     * @attr
188✔
57
     */
188✔
58
    @property({ reflect: true })
188✔
59
    public placement?: Placement;
188✔
60

188✔
61
    @property()
188✔
62
    public type?: OverlayTriggerInteractions;
188✔
63

188✔
64
    @property({ type: Number })
188✔
65
    public offset = 6;
188✔
66

188✔
67
    @property({ reflect: true })
188✔
68
    public open?: OverlayContentTypes;
188✔
69

188✔
70
    @property({ type: Boolean, reflect: true })
188✔
71
    public disabled = false;
188✔
72

188✔
73
    @property({ attribute: 'receives-focus' })
188✔
74
    public receivesFocus: 'true' | 'false' | 'auto' = 'auto';
188✔
75

188✔
76
    @state()
188✔
77
    private clickContent: HTMLElement[] = [];
188✔
78

188✔
79
    private clickPlacement?: Placement;
188✔
80

188✔
81
    @state()
188✔
82
    private longpressContent: HTMLElement[] = [];
188✔
83

188✔
84
    private longpressPlacement?: Placement;
188✔
85

188✔
86
    @state()
188✔
87
    private hoverContent: HTMLElement[] = [];
188✔
88

188✔
89
    private hoverPlacement?: Placement;
188✔
90

188✔
91
    @state()
188✔
92
    private targetContent: HTMLElement[] = [];
188✔
93

20✔
94
    @query('#click-overlay', true)
20✔
95
    clickOverlayElement!: Overlay;
20✔
96

20✔
97
    @query('#longpress-overlay', true)
20✔
98
    longpressOverlayElement!: Overlay;
20✔
99

20✔
100
    @query('#hover-overlay', true)
20✔
101
    hoverOverlayElement!: Overlay;
20✔
102

20✔
103
    private getAssignedElementsFromSlot(slot: HTMLSlotElement): HTMLElement[] {
20✔
104
        return slot.assignedElements({ flatten: true }) as HTMLElement[];
191✔
105
    }
191✔
106

20✔
107
    private handleTriggerContent(
20✔
108
        event: Event & { target: HTMLSlotElement }
191✔
109
    ): void {
191✔
110
        this.targetContent = this.getAssignedElementsFromSlot(event.target);
191✔
111
    }
191✔
112

20✔
113
    private handleSlotContent(
20✔
UNCOV
114
        event: Event & { target: HTMLSlotElement }
×
UNCOV
115
    ): void {
×
UNCOV
116
        switch (event.target.name) {
×
UNCOV
117
            case 'click-content':
×
UNCOV
118
                this.clickContent = this.getAssignedElementsFromSlot(
×
UNCOV
119
                    event.target
×
UNCOV
120
                );
×
UNCOV
121
                break;
×
UNCOV
122
            case 'longpress-content':
×
UNCOV
123
                this.longpressContent = this.getAssignedElementsFromSlot(
×
UNCOV
124
                    event.target
×
UNCOV
125
                );
×
UNCOV
126
                break;
×
UNCOV
127
            case 'hover-content':
×
UNCOV
128
                this.hoverContent = this.getAssignedElementsFromSlot(
×
UNCOV
129
                    event.target
×
UNCOV
130
                );
×
UNCOV
131
                break;
×
UNCOV
132
        }
×
UNCOV
133
    }
×
134

20✔
135
    private handleBeforetoggle(event: BeforetoggleOpenEvent): void {
20✔
UNCOV
136
        const { target } = event;
×
UNCOV
137
        let type: OverlayContentTypes;
×
UNCOV
138
        if (target === this.clickOverlayElement) {
×
UNCOV
139
            type = 'click';
×
UNCOV
140
        } else if (target === this.longpressOverlayElement) {
×
UNCOV
141
            type = 'longpress';
×
UNCOV
142
        } else if (target === this.hoverOverlayElement) {
×
UNCOV
143
            type = 'hover';
×
144
            /* c8 ignore next 3 */
20✔
145
        } else {
20✔
146
            return;
20✔
147
        }
20✔
UNCOV
148
        if (event.newState === 'open') {
×
UNCOV
149
            this.open = type;
×
UNCOV
150
        } else if (this.open === type) {
×
UNCOV
151
            this.open = undefined;
×
UNCOV
152
        }
×
UNCOV
153
    }
×
154

20✔
155
    protected override update(changes: PropertyValues): void {
20✔
156
        if (changes.has('clickContent')) {
400✔
157
            this.clickPlacement =
188✔
158
                ((this.clickContent[0]?.getAttribute('placement') ||
188!
159
                    this.clickContent[0]?.getAttribute(
188!
UNCOV
160
                        'direction'
×
161
                    )) as Placement) || undefined;
188✔
162
        }
188✔
163
        if (changes.has('hoverContent')) {
400✔
164
            this.hoverPlacement =
188✔
165
                ((this.hoverContent[0]?.getAttribute('placement') ||
188!
166
                    this.hoverContent[0]?.getAttribute(
188!
UNCOV
167
                        'direction'
×
168
                    )) as Placement) || undefined;
188✔
169
        }
188✔
170
        if (changes.has('longpressContent')) {
400✔
171
            this.longpressPlacement =
188✔
172
                ((this.longpressContent[0]?.getAttribute('placement') ||
188!
173
                    this.longpressContent[0]?.getAttribute(
188!
UNCOV
174
                        'direction'
×
175
                    )) as Placement) || undefined;
188✔
176
        }
188✔
177
        super.update(changes);
400✔
178
    }
400✔
179

20✔
180
    protected renderSlot(
20✔
181
        name: string,
1,148✔
182
        attachListener: boolean
1,148✔
183
    ): TemplateResult {
1,148✔
184
        return html`
1,148✔
185
            <slot
1,148✔
186
                name=${name}
1,148✔
187
                @slotchange=${attachListener ? this.handleSlotContent : null}
1,148!
188
            ></slot>
1,148✔
189
        `;
1,148✔
190
    }
1,148✔
191

20✔
192
    protected renderClickOverlay(): TemplateResult {
20✔
193
        import('@spectrum-web-components/overlay/sp-overlay.js');
400✔
194
        const slot = this.renderSlot(
400✔
195
            'click-content',
400✔
196
            !!this.clickContent.length
400✔
197
        );
400✔
198
        if (!this.clickContent.length) {
400✔
199
            return slot;
400✔
200
        }
400!
UNCOV
201
        return html`
×
UNCOV
202
            <sp-overlay
×
UNCOV
203
                id="click-overlay"
×
204
                ?disabled=${this.disabled || !this.clickContent.length}
400✔
205
                ?open=${this.open === 'click' && !!this.clickContent.length}
400!
206
                .offset=${this.offset}
400✔
207
                .placement=${this.clickPlacement || this.placement}
400!
208
                .triggerElement=${this.targetContent[0]}
400✔
209
                .triggerInteraction=${'click'}
400✔
210
                .type=${this.type !== 'modal' ? 'auto' : 'modal'}
400!
211
                @beforetoggle=${this.handleBeforetoggle}
400✔
212
                .receivesFocus=${this.receivesFocus}
400✔
213
            >
400✔
214
                ${slot}
400✔
215
            </sp-overlay>
400✔
216
        `;
400✔
217
    }
400✔
218

20✔
219
    protected renderHoverOverlay(): TemplateResult {
20✔
220
        import('@spectrum-web-components/overlay/sp-overlay.js');
374✔
221
        const slot = this.renderSlot(
374✔
222
            'hover-content',
374✔
223
            !!this.hoverContent.length
374✔
224
        );
374✔
225
        if (!this.hoverContent.length) {
374✔
226
            return slot;
374✔
227
        }
374!
UNCOV
228
        return html`
×
UNCOV
229
            <sp-overlay
×
UNCOV
230
                id="hover-overlay"
×
231
                ?open=${this.open === 'hover' && !!this.hoverContent.length}
374✔
232
                ?disabled=${this.disabled ||
374!
UNCOV
233
                !this.hoverContent.length ||
×
234
                (!!this.open && this.open !== 'hover')}
374✔
235
                .offset=${this.offset}
374✔
236
                .placement=${this.hoverPlacement || this.placement}
374!
237
                .triggerElement=${this.targetContent[0]}
374✔
238
                .triggerInteraction=${'hover'}
374✔
239
                .type=${'hint'}
374✔
240
                @beforetoggle=${this.handleBeforetoggle}
374✔
241
                .receivesFocus=${this.receivesFocus}
374✔
242
            >
374✔
243
                ${slot}
374✔
244
            </sp-overlay>
374✔
245
        `;
374✔
246
    }
374✔
247

20✔
248
    protected renderLongpressOverlay(): TemplateResult {
20✔
249
        import('@spectrum-web-components/overlay/sp-overlay.js');
374✔
250
        const slot = this.renderSlot(
374✔
251
            'longpress-content',
374✔
252
            !!this.longpressContent.length
374✔
253
        );
374✔
254
        if (!this.longpressContent.length) {
374✔
255
            return slot;
374✔
256
        }
374!
UNCOV
257
        return html`
×
UNCOV
258
            <sp-overlay
×
UNCOV
259
                id="longpress-overlay"
×
260
                ?disabled=${this.disabled || !this.longpressContent.length}
374✔
261
                ?open=${this.open === 'longpress' &&
374!
262
                !!this.longpressContent.length}
374✔
263
                .offset=${this.offset}
374✔
264
                .placement=${this.longpressPlacement || this.placement}
374!
265
                .triggerElement=${this.targetContent[0]}
374✔
266
                .triggerInteraction=${'longpress'}
374✔
267
                .type=${'auto'}
374✔
268
                @beforetoggle=${this.handleBeforetoggle}
374✔
269
                .receivesFocus=${this.receivesFocus}
374✔
270
            >
374✔
271
                ${slot}
374✔
272
            </sp-overlay>
374✔
273
            <slot name="longpress-describedby-descriptor"></slot>
374✔
274
        `;
374✔
275
    }
374✔
276

20✔
277
    protected override render(): TemplateResult {
20✔
278
        const content = this.content.split(' ');
400✔
279
        // Keyboard event availability documented in README.md
400✔
280
        /* eslint-disable lit-a11y/click-events-have-key-events */
400✔
281
        return html`
400✔
282
            <slot
400✔
283
                id="trigger"
400✔
284
                name="trigger"
400✔
285
                @slotchange=${this.handleTriggerContent}
400✔
286
            ></slot>
400✔
287
            ${[
400✔
288
                content.includes('click') ? this.renderClickOverlay() : html``,
400!
289
                content.includes('hover') ? this.renderHoverOverlay() : html``,
400✔
290
                content.includes('longpress')
400✔
291
                    ? this.renderLongpressOverlay()
374✔
292
                    : html``,
26✔
293
            ]}
400✔
294
        `;
400✔
295
        /* eslint-enable lit-a11y/click-events-have-key-events */
400✔
296
    }
400✔
297

20✔
298
    protected override updated(changes: PropertyValues): void {
20✔
299
        super.updated(changes);
400✔
300
        if (this.disabled && changes.has('disabled')) {
400!
UNCOV
301
            this.open = undefined;
×
UNCOV
302
            return;
×
UNCOV
303
        }
×
304
    }
400✔
305

20✔
306
    protected override async getUpdateComplete(): Promise<boolean> {
20✔
307
        const complete = (await super.getUpdateComplete()) as boolean;
125✔
308
        return complete;
125✔
309
    }
125✔
310
}
20✔
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