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

adobe / spectrum-web-components / 12282443963

11 Dec 2024 06:17PM UTC coverage: 97.436% (-0.8%) from 98.206%
12282443963

Pull #4989

github

web-flow
Merge 92e762395 into 5b674b468
Pull Request #4989: chore: testing something

4996 of 5296 branches covered (94.34%)

Branch coverage included in aggregate %.

7 of 19 new or added lines in 1 file covered. (36.84%)

257 existing lines in 8 files now uncovered.

32739 of 33432 relevant lines covered (97.93%)

380.42 hits per line

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

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

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

38✔
13
import {
38✔
14
    isAndroid,
38✔
15
    isIOS,
38✔
16
} from '@spectrum-web-components/shared/src/platform.js';
38✔
17
import { conditionAttributeWithId } from '@spectrum-web-components/base/src/condition-attribute-with-id.js';
38✔
18
import { randomID } from '@spectrum-web-components/shared/src/random-id.js';
38✔
19

38✔
20
import { noop } from './AbstractOverlay.js';
38✔
21
import {
38✔
22
    InteractionController,
38✔
23
    InteractionTypes,
38✔
24
} from './InteractionController.js';
38✔
25

38✔
26
const LONGPRESS_DURATION = 300;
38✔
27
export const LONGPRESS_INSTRUCTIONS = {
38✔
28
    touch: 'Double tap and long press for additional options',
38✔
29
    keyboard: 'Press Space or Alt+Down Arrow for additional options',
38✔
30
    mouse: 'Click and hold for additional options',
38✔
31
};
38✔
32

38✔
33
type LongpressEvent = {
38✔
34
    source: 'pointer' | 'keyboard';
38✔
35
};
38✔
36

38✔
37
export class LongpressController extends InteractionController {
38✔
UNCOV
38
    override type = InteractionTypes.longpress;
×
UNCOV
39

×
UNCOV
40
    override get activelyOpening(): boolean {
×
UNCOV
41
        return (
×
UNCOV
42
            this.longpressState === 'opening' ||
×
UNCOV
43
            this.longpressState === 'pressed'
×
UNCOV
44
        );
×
UNCOV
45
    }
×
UNCOV
46

×
UNCOV
47
    protected longpressState: null | 'potential' | 'opening' | 'pressed' = null;
×
UNCOV
48

×
UNCOV
49
    override releaseDescription = noop;
×
UNCOV
50

×
UNCOV
51
    private timeout!: ReturnType<typeof setTimeout>;
×
UNCOV
52

×
UNCOV
53
    handleLongpress(): void {
×
UNCOV
54
        this.open = true;
×
UNCOV
55
        this.longpressState =
×
UNCOV
56
            this.longpressState === 'potential' ? 'opening' : 'pressed';
×
UNCOV
57
    }
×
UNCOV
58

×
UNCOV
59
    handlePointerdown(event: PointerEvent): void {
×
UNCOV
60
        if (!this.target) return;
×
UNCOV
61
        if (event.button !== 0) return;
×
UNCOV
62
        this.longpressState = 'potential';
×
UNCOV
63
        document.addEventListener('pointerup', this.handlePointerup);
×
UNCOV
64
        document.addEventListener('pointercancel', this.handlePointerup);
×
UNCOV
65
        // Only dispatch longpress event if the trigger element isn't doing it for us.
×
UNCOV
66
        const triggerHandlesLongpress = 'holdAffordance' in this.target;
×
UNCOV
67
        if (triggerHandlesLongpress) return;
×
UNCOV
68
        this.timeout = setTimeout(() => {
×
UNCOV
69
            if (!this.target) return;
×
UNCOV
70
            this.target.dispatchEvent(
×
UNCOV
71
                new CustomEvent<LongpressEvent>('longpress', {
×
UNCOV
72
                    bubbles: true,
×
UNCOV
73
                    composed: true,
×
UNCOV
74
                    detail: {
×
UNCOV
75
                        source: 'pointer',
×
UNCOV
76
                    },
×
UNCOV
77
                })
×
UNCOV
78
            );
×
UNCOV
79
        }, LONGPRESS_DURATION);
×
UNCOV
80
    }
×
UNCOV
81

×
UNCOV
82
    private handlePointerup = (): void => {
×
UNCOV
83
        clearTimeout(this.timeout);
×
UNCOV
84
        if (!this.target) return;
×
UNCOV
85
        // When triggered by the pointer, the last of `opened`
×
UNCOV
86
        // or `pointerup` should move the `longpressState` to
×
UNCOV
87
        // `null` so that the earlier event can void the "light
×
UNCOV
88
        // dismiss" and keep the Overlay open.
×
UNCOV
89
        this.longpressState =
×
UNCOV
90
            this.overlay?.state === 'opening' ? 'pressed' : null;
×
UNCOV
91
        document.removeEventListener('pointerup', this.handlePointerup);
×
UNCOV
92
        document.removeEventListener('pointercancel', this.handlePointerup);
×
UNCOV
93
    };
×
94

38✔
95
    private handleKeydown(event: KeyboardEvent): void {
38✔
UNCOV
96
        const { code, altKey } = event;
×
UNCOV
97
        if (altKey && code === 'ArrowDown') {
×
UNCOV
98
            event.stopPropagation();
×
UNCOV
99
            event.stopImmediatePropagation();
×
UNCOV
100
        }
×
UNCOV
101
    }
×
102

38✔
103
    private handleKeyup(event: KeyboardEvent): void {
38✔
UNCOV
104
        const { code, altKey } = event;
×
UNCOV
105
        if (code === 'Space' || (altKey && code === 'ArrowDown')) {
×
UNCOV
106
            if (!this.target) {
×
107
                return;
×
108
            }
×
UNCOV
109
            event.stopPropagation();
×
UNCOV
110
            this.target.dispatchEvent(
×
UNCOV
111
                new CustomEvent<LongpressEvent>('longpress', {
×
UNCOV
112
                    bubbles: true,
×
UNCOV
113
                    composed: true,
×
UNCOV
114
                    detail: {
×
UNCOV
115
                        source: 'keyboard',
×
UNCOV
116
                    },
×
UNCOV
117
                })
×
UNCOV
118
            );
×
UNCOV
119
            setTimeout(() => {
×
UNCOV
120
                this.longpressState = null;
×
UNCOV
121
            });
×
UNCOV
122
        }
×
UNCOV
123
    }
×
124

38✔
125
    override prepareDescription(trigger: HTMLElement): void {
38✔
UNCOV
126
        if (
×
UNCOV
127
            // do not reapply until target is recycled
×
UNCOV
128
            this.releaseDescription !== noop ||
×
UNCOV
129
            // require "longpress content" to apply relationship
×
UNCOV
130
            !this.overlay.elements.length
×
UNCOV
131
        ) {
×
UNCOV
132
            return;
×
UNCOV
133
        }
×
UNCOV
134

×
UNCOV
135
        const longpressDescription = document.createElement('div');
×
UNCOV
136
        longpressDescription.id = `longpress-describedby-descriptor-${randomID()}`;
×
UNCOV
137
        const messageType = isIOS() || isAndroid() ? 'touch' : 'keyboard';
×
UNCOV
138
        longpressDescription.textContent = LONGPRESS_INSTRUCTIONS[messageType];
×
UNCOV
139
        longpressDescription.slot = 'longpress-describedby-descriptor';
×
UNCOV
140
        const triggerParent = trigger.getRootNode() as HTMLElement;
×
UNCOV
141
        const overlayParent = this.overlay.getRootNode() as HTMLElement;
×
UNCOV
142
        // Manage the placement of the helper element in an accessible place with
×
UNCOV
143
        // the lowest chance of negatively affecting the layout of the page.
×
UNCOV
144
        if (triggerParent === overlayParent) {
×
145
            // Trigger and Overlay in same DOM tree...
×
146
            // Append helper element to Overlay.
×
147
            this.overlay.append(longpressDescription);
×
UNCOV
148
        } else {
×
UNCOV
149
            // If Trigger in <body>, hide helper
×
UNCOV
150
            longpressDescription.hidden = !('host' in triggerParent);
×
UNCOV
151
            // Trigger and Overlay in different DOM tree, Trigger in shadow tree...
×
UNCOV
152
            // Insert helper element after Trigger.
×
UNCOV
153
            trigger.insertAdjacentElement('afterend', longpressDescription);
×
UNCOV
154
        }
×
UNCOV
155

×
UNCOV
156
        const releaseDescription = conditionAttributeWithId(
×
UNCOV
157
            trigger,
×
UNCOV
158
            'aria-describedby',
×
UNCOV
159
            [longpressDescription.id]
×
UNCOV
160
        );
×
UNCOV
161
        this.releaseDescription = () => {
×
UNCOV
162
            releaseDescription();
×
UNCOV
163
            longpressDescription.remove();
×
UNCOV
164
            this.releaseDescription = noop;
×
UNCOV
165
        };
×
UNCOV
166
    }
×
167

38✔
168
    override shouldCompleteOpen(): void {
38✔
UNCOV
169
        // When triggered by the pointer, the last of `opened`
×
UNCOV
170
        // or `pointerup` should move the `longpressState` to
×
UNCOV
171
        // `null` so that the earlier event can void the "light
×
UNCOV
172
        // dismiss" and keep the Overlay open.
×
UNCOV
173
        this.longpressState =
×
UNCOV
174
            this.longpressState === 'pressed' ? null : this.longpressState;
×
UNCOV
175
    }
×
176

38✔
177
    override init(): void {
38✔
UNCOV
178
        // Clean up listeners if they've already been bound
×
UNCOV
179
        this.abortController?.abort();
×
UNCOV
180
        this.abortController = new AbortController();
×
UNCOV
181
        const { signal } = this.abortController;
×
UNCOV
182
        this.target.addEventListener(
×
UNCOV
183
            'longpress',
×
UNCOV
184
            () => this.handleLongpress(),
×
UNCOV
185
            { signal }
×
UNCOV
186
        );
×
UNCOV
187
        this.target.addEventListener(
×
UNCOV
188
            'pointerdown',
×
UNCOV
189
            (event: PointerEvent) => this.handlePointerdown(event),
×
UNCOV
190
            { signal }
×
UNCOV
191
        );
×
UNCOV
192

×
UNCOV
193
        this.prepareDescription(this.target);
×
UNCOV
194
        if (
×
UNCOV
195
            (this.target as HTMLElement & { holdAffordance: boolean })
×
UNCOV
196
                .holdAffordance
×
UNCOV
197
        ) {
×
UNCOV
198
            // Only bind keyboard events when the trigger element isn't doing it for us.
×
UNCOV
199
            return;
×
UNCOV
200
        }
×
UNCOV
201
        this.target.addEventListener(
×
UNCOV
202
            'keydown',
×
UNCOV
203
            (event: KeyboardEvent) => this.handleKeydown(event),
×
UNCOV
204
            { signal }
×
UNCOV
205
        );
×
UNCOV
206
        this.target.addEventListener(
×
UNCOV
207
            'keyup',
×
UNCOV
208
            (event: KeyboardEvent) => this.handleKeyup(event),
×
UNCOV
209
            { signal }
×
UNCOV
210
        );
×
UNCOV
211
    }
×
212
}
38✔
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