• 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

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

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

1✔
13
import { conditionAttributeWithId } from '@spectrum-web-components/base/src/condition-attribute-with-id.js';
1✔
14
import { isWebKit } from '@spectrum-web-components/shared';
1✔
15
import { randomID } from '@spectrum-web-components/shared/src/random-id.js';
1✔
16
import { noop } from './AbstractOverlay.js';
1✔
17
import {
1✔
18
    InteractionController,
1✔
19
    InteractionTypes,
1✔
20
    lastInteractionType,
1✔
21
    SAFARI_FOCUS_RING_CLASS,
1✔
22
} from './InteractionController.js';
1✔
23

1✔
24
const HOVER_DELAY = 300;
1✔
25

1✔
26
export class HoverController extends InteractionController {
1✔
27
    override type = InteractionTypes.hover;
2✔
28

2✔
29
    private elementIds: string[] = [];
2✔
30

2✔
31
    focusedin = false;
2✔
32

2✔
33
    private hoverTimeout?: ReturnType<typeof setTimeout>;
2✔
34

2✔
35
    pointerentered = false;
2✔
36

1✔
37
    handleKeyup(event: KeyboardEvent): void {
1✔
38
        if (event.code === 'Tab' || event.code === 'Escape') {
6!
39
            this.open = true;
6✔
40
            this.removeSafariFocusRingClass();
6✔
41
        }
6✔
42
    }
6✔
43

1✔
44
    handleTargetFocusin(): void {
1✔
45
        if (!this.target.matches(':focus-visible')) {
6!
46
            return;
×
47
        }
×
48

6✔
49
        if (
6✔
50
            isWebKit() &&
6!
51
            this.target[lastInteractionType] === InteractionTypes.click
×
52
        ) {
6!
53
            this.target.classList.add(SAFARI_FOCUS_RING_CLASS);
×
54
            return;
×
55
        }
×
56

6✔
57
        this.open = true;
6✔
58
        this.focusedin = true;
6✔
59
        this.removeSafariFocusRingClass();
6✔
60
    }
6✔
61

1✔
62
    handleTargetFocusout(): void {
1✔
63
        this.removeSafariFocusRingClass();
6✔
64
        this.focusedin = false;
6✔
65
        if (this.pointerentered) return;
6!
66
        this.open = false;
6✔
67
    }
6✔
68

1✔
69
    handleTargetPointerenter(): void {
1✔
70
        if (this.hoverTimeout) {
×
71
            clearTimeout(this.hoverTimeout);
×
72
            this.hoverTimeout = undefined;
×
73
        }
×
74
        if (this.overlay?.disabled) return;
×
75
        this.open = true;
×
76
        this.pointerentered = true;
×
77
    }
×
78

1✔
79
    handleTargetPointerleave(): void {
1✔
80
        this.doPointerleave();
×
81
    }
×
82

1✔
83
    // set a timeout once the pointer enters and the overlay is shown
1✔
84
    // give the user time to enter the overlay
1✔
85
    handleHostPointerenter(): void {
1✔
86
        if (this.hoverTimeout) {
×
87
            clearTimeout(this.hoverTimeout);
×
88
            this.hoverTimeout = undefined;
×
89
        }
×
90
    }
×
91

1✔
92
    handleHostPointerleave(): void {
1✔
93
        this.doPointerleave();
×
94
    }
×
95

1✔
96
    override prepareDescription(): void {
1✔
97
        // require "content" to apply relationship
×
98
        if (!this.overlay.elements.length) return;
×
99

×
100
        const triggerRoot = this.target.getRootNode();
×
101
        const contentRoot = this.overlay.elements[0].getRootNode();
×
102
        const overlayRoot = this.overlay.getRootNode();
×
103
        if (triggerRoot === overlayRoot) {
×
104
            this.prepareOverlayRelativeDescription();
×
105
        } else if (triggerRoot === contentRoot) {
×
106
            this.prepareContentRelativeDescription();
×
107
        }
×
108
    }
×
109

1✔
110
    private prepareOverlayRelativeDescription(): void {
1✔
111
        const releaseDescription = conditionAttributeWithId(
×
112
            this.target,
×
113
            'aria-describedby',
×
114
            [this.overlay.id]
×
115
        );
×
116
        this.releaseDescription = () => {
×
117
            releaseDescription();
×
118
            this.releaseDescription = noop;
×
119
        };
×
120
    }
×
121

1✔
122
    private prepareContentRelativeDescription(): void {
1✔
123
        const elementIds: string[] = [];
×
124
        const appliedIds = this.overlay.elements.map((el) => {
×
125
            elementIds.push(el.id);
×
126
            if (!el.id) {
×
127
                el.id = `${this.overlay.tagName.toLowerCase()}-helper-${randomID()}`;
×
128
            }
×
129
            return el.id;
×
130
        });
×
131
        this.elementIds = elementIds;
×
132
        const releaseDescription = conditionAttributeWithId(
×
133
            this.target,
×
134
            'aria-describedby',
×
135
            appliedIds
×
136
        );
×
137
        this.releaseDescription = () => {
×
138
            releaseDescription();
×
139
            this.overlay.elements.map((el, index) => {
×
140
                el.id = this.elementIds[index];
×
141
            });
×
142
            this.releaseDescription = noop;
×
143
        };
×
144
    }
×
145

1✔
146
    protected doPointerleave(): void {
1✔
147
        this.pointerentered = false;
×
148
        const triggerElement = this.target as HTMLElement;
×
149
        if (this.focusedin && triggerElement.matches(':focus-visible')) return;
×
150

×
151
        this.hoverTimeout = setTimeout(() => {
×
152
            this.open = false;
×
153
        }, HOVER_DELAY);
×
154
    }
×
155

1✔
156
    override init(): void {
1✔
157
        // Clean up listeners if they've already been bound
2✔
158
        this.abortController?.abort();
2!
159
        this.abortController = new AbortController();
2✔
160
        const { signal } = this.abortController;
2✔
161
        this.target.addEventListener(
2✔
162
            'keyup',
2✔
163
            (event) => this.handleKeyup(event),
2✔
164
            { signal }
2✔
165
        );
2✔
166
        this.target.addEventListener(
2✔
167
            'focusin',
2✔
168
            () => this.handleTargetFocusin(),
2✔
169
            { signal }
2✔
170
        );
2✔
171
        this.target.addEventListener(
2✔
172
            'focusout',
2✔
173
            () => this.handleTargetFocusout(),
2✔
174
            { signal }
2✔
175
        );
2✔
176
        this.target.addEventListener(
2✔
177
            'pointerenter',
2✔
178
            () => this.handleTargetPointerenter(),
2✔
179
            { signal }
2✔
180
        );
2✔
181
        this.target.addEventListener(
2✔
182
            'pointerleave',
2✔
183
            () => this.handleTargetPointerleave(),
2✔
184
            { signal }
2✔
185
        );
2✔
186
        if (this.overlay) {
2!
187
            this.initOverlay();
×
188
        }
×
189
    }
2✔
190

1✔
191
    override initOverlay(): void {
1✔
192
        if (!this.abortController) {
×
193
            return;
×
194
        }
×
195
        const { signal } = this.abortController;
×
196
        this.overlay.addEventListener(
×
197
            'pointerenter',
×
198
            () => this.handleHostPointerenter(),
×
199
            { signal }
×
200
        );
×
201
        this.overlay.addEventListener(
×
202
            'pointerleave',
×
203
            () => this.handleHostPointerleave(),
×
204
            { signal }
×
205
        );
×
206
    }
×
207

1✔
208
    private removeSafariFocusRingClass(): void {
1✔
209
        if (
18✔
210
            isWebKit() &&
18!
211
            this.target.classList.contains(SAFARI_FOCUS_RING_CLASS)
×
212
        )
18✔
213
            this.target.classList.remove(SAFARI_FOCUS_RING_CLASS);
18!
214
    }
18✔
215
}
1✔
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