• 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

22.64
/packages/overlay/src/LongpressController.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 {
1✔
14
    isAndroid,
1✔
15
    isIOS,
1✔
16
} from '@spectrum-web-components/shared/src/platform.js';
1✔
17
import { conditionAttributeWithId } from '@spectrum-web-components/base/src/condition-attribute-with-id.js';
1✔
18
import { randomID } from '@spectrum-web-components/shared/src/random-id.js';
1✔
19

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

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

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

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

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

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

×
49
    override releaseDescription = noop;
×
50

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

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

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

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

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

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

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

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

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

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

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

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