• 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

87.56
/packages/overlay/src/HoverController.ts
1
/*
40✔
2
Copyright 2024 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

40✔
13
import { conditionAttributeWithId } from '@spectrum-web-components/base/src/condition-attribute-with-id.js';
40✔
14
import { randomID } from '@spectrum-web-components/shared/src/random-id.js';
40✔
15

40✔
16
import {
40✔
17
    InteractionController,
40✔
18
    InteractionTypes,
40✔
19
} from './InteractionController.js';
40✔
20
import { noop } from './AbstractOverlay.js';
40✔
21

40✔
22
const HOVER_DELAY = 300;
40✔
23

40✔
24
export class HoverController extends InteractionController {
40✔
25
    override type = InteractionTypes.hover;
229✔
26

229✔
27
    private elementIds: string[] = [];
229✔
28

229✔
29
    focusedin = false;
229✔
30

229✔
31
    private hoverTimeout?: ReturnType<typeof setTimeout>;
229✔
32

229✔
33
    pointerentered = false;
229✔
34

40✔
35
    handleTargetFocusin(): void {
40✔
36
        if (!this.target.matches(':focus-visible')) {
14✔
37
            return;
6✔
38
        }
6✔
39
        this.open = true;
8✔
40
        this.focusedin = true;
8✔
41
    }
14✔
42

40✔
43
    handleTargetFocusout(): void {
40✔
44
        this.focusedin = false;
13✔
45
        if (this.pointerentered) return;
13✔
46
        this.open = false;
11✔
47
    }
13✔
48

40✔
49
    handleTargetPointerenter(): void {
40✔
50
        if (this.hoverTimeout) {
11✔
51
            clearTimeout(this.hoverTimeout);
2✔
52
            this.hoverTimeout = undefined;
2✔
53
        }
2✔
54
        if (this.overlay?.disabled) return;
11!
55
        this.open = true;
11✔
56
        this.pointerentered = true;
11✔
57
    }
11✔
58

40✔
59
    handleTargetPointerleave(): void {
40✔
60
        this.doPointerleave();
6✔
61
    }
6✔
62

40✔
63
    // set a timeout once the pointer enters and the overlay is shown
40✔
64
    // give the user time to enter the overlay
40✔
65
    handleHostPointerenter(): void {
40✔
66
        if (this.hoverTimeout) {
4✔
67
            clearTimeout(this.hoverTimeout);
1✔
68
            this.hoverTimeout = undefined;
1✔
69
        }
1✔
70
    }
4✔
71

40✔
72
    handleHostPointerleave(): void {
40✔
73
        this.doPointerleave();
4✔
74
    }
4✔
75

40✔
76
    override prepareDescription(): void {
40✔
77
        // require "content" to apply relationship
255✔
78
        if (!this.overlay.elements.length) return;
255✔
79

223✔
80
        const triggerRoot = this.target.getRootNode();
223✔
81
        const contentRoot = this.overlay.elements[0].getRootNode();
223✔
82
        const overlayRoot = this.overlay.getRootNode();
223✔
83
        if (triggerRoot === overlayRoot) {
251✔
84
            this.prepareOverlayRelativeDescription();
1✔
85
        } else if (triggerRoot === contentRoot) {
249✔
UNCOV
86
            this.prepareContentRelativeDescription();
×
UNCOV
87
        }
×
88
    }
255✔
89

40✔
90
    private prepareOverlayRelativeDescription(): void {
40✔
91
        const releaseDescription = conditionAttributeWithId(
1✔
92
            this.target,
1✔
93
            'aria-describedby',
1✔
94
            [this.overlay.id]
1✔
95
        );
1✔
96
        this.releaseDescription = () => {
1✔
97
            releaseDescription();
1✔
98
            this.releaseDescription = noop;
1✔
99
        };
1✔
100
    }
1✔
101

40✔
102
    private prepareContentRelativeDescription(): void {
40✔
UNCOV
103
        const elementIds: string[] = [];
×
UNCOV
104
        const appliedIds = this.overlay.elements.map((el) => {
×
UNCOV
105
            elementIds.push(el.id);
×
UNCOV
106
            if (!el.id) {
×
UNCOV
107
                el.id = `${this.overlay.tagName.toLowerCase()}-helper-${randomID()}`;
×
UNCOV
108
            }
×
UNCOV
109
            return el.id;
×
UNCOV
110
        });
×
UNCOV
111
        this.elementIds = elementIds;
×
UNCOV
112
        const releaseDescription = conditionAttributeWithId(
×
UNCOV
113
            this.target,
×
UNCOV
114
            'aria-describedby',
×
UNCOV
115
            appliedIds
×
UNCOV
116
        );
×
UNCOV
117
        this.releaseDescription = () => {
×
UNCOV
118
            releaseDescription();
×
UNCOV
119
            this.overlay.elements.map((el, index) => {
×
UNCOV
120
                el.id = this.elementIds[index];
×
UNCOV
121
            });
×
UNCOV
122
            this.releaseDescription = noop;
×
UNCOV
123
        };
×
UNCOV
124
    }
×
125

40✔
126
    protected doPointerleave(): void {
40✔
127
        this.pointerentered = false;
10✔
128
        const triggerElement = this.target as HTMLElement;
10✔
129
        if (this.focusedin && triggerElement.matches(':focus-visible')) return;
10!
130

10✔
131
        this.hoverTimeout = setTimeout(() => {
10✔
132
            this.open = false;
8✔
133
        }, HOVER_DELAY);
10✔
134
    }
10✔
135

40✔
136
    override init(): void {
40✔
137
        // Clean up listeners if they've already been bound
231✔
138
        this.abortController?.abort();
231✔
139
        this.abortController = new AbortController();
231✔
140
        const { signal } = this.abortController;
231✔
141
        this.target.addEventListener(
231✔
142
            'focusin',
231✔
143
            () => this.handleTargetFocusin(),
231✔
144
            { signal }
231✔
145
        );
231✔
146
        this.target.addEventListener(
231✔
147
            'focusout',
231✔
148
            () => this.handleTargetFocusout(),
231✔
149
            { signal }
231✔
150
        );
231✔
151
        this.target.addEventListener(
231✔
152
            'pointerenter',
231✔
153
            () => this.handleTargetPointerenter(),
231✔
154
            { signal }
231✔
155
        );
231✔
156
        this.target.addEventListener(
231✔
157
            'pointerleave',
231✔
158
            () => this.handleTargetPointerleave(),
231✔
159
            { signal }
231✔
160
        );
231✔
161
        if (this.overlay) {
231✔
162
            this.initOverlay();
225✔
163
        }
225✔
164
    }
231✔
165

40✔
166
    override initOverlay(): void {
40✔
167
        if (!this.abortController) {
452✔
168
            return;
2✔
169
        }
2✔
170
        const { signal } = this.abortController;
450✔
171
        this.overlay.addEventListener(
450✔
172
            'pointerenter',
450✔
173
            () => this.handleHostPointerenter(),
450✔
174
            { signal }
450✔
175
        );
450✔
176
        this.overlay.addEventListener(
450✔
177
            'pointerleave',
450✔
178
            () => this.handleHostPointerleave(),
450✔
179
            { signal }
450✔
180
        );
450✔
181
    }
452✔
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