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

adobe / spectrum-web-components / 18325424308

07 Oct 2025 08:32PM UTC coverage: 97.956% (+0.04%) from 97.919%
18325424308

Pull #5790

github

web-flow
Merge c6b6abc94 into 7d23140c2
Pull Request #5790: [DRAFT]: fix(overlay): Longpress interaction now enables all buttons after overlay opens

5352 of 5636 branches covered (94.96%)

Branch coverage included in aggregate %.

41 of 42 new or added lines in 1 file covered. (97.62%)

70 existing lines in 4 files now uncovered.

34083 of 34622 relevant lines covered (98.44%)

629.41 hits per line

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

99.04
/packages/button/src/ButtonBase.ts
1
/**
65✔
2
 * Copyright 2025 Adobe. All rights reserved.
65✔
3
 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
65✔
4
 * you may not use this file except in compliance with the License. You may obtain a copy
65✔
5
 * of the License at http://www.apache.org/licenses/LICENSE-2.0
65✔
6
 *
65✔
7
 * Unless required by applicable law or agreed to in writing, software distributed under
65✔
8
 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
65✔
9
 * OF ANY KIND, either express or implied. See the License for the specific language
65✔
10
 * governing permissions and limitations under the License.
65✔
11
 */
65✔
12

65✔
13
import {
65✔
14
    CSSResultArray,
65✔
15
    html,
65✔
16
    PropertyValues,
65✔
17
    TemplateResult,
65✔
18
} from '@spectrum-web-components/base';
65✔
19
import {
65✔
20
    property,
65✔
21
    query,
65✔
22
} from '@spectrum-web-components/base/src/decorators.js';
65✔
23
import { LikeAnchor } from '@spectrum-web-components/shared/src/like-anchor.js';
65✔
24
import { Focusable } from '@spectrum-web-components/shared/src/focusable.js';
65✔
25
import { ObserveSlotText } from '@spectrum-web-components/shared/src/observe-slot-text.js';
65✔
26
import buttonStyles from './button-base.css.js';
65✔
27

65✔
28
/**
65✔
29
 * @slot - text content to be displayed in the Button element
65✔
30
 * @slot icon - icon element(s) to display at the start of the button
65✔
31
 */
65✔
32
export class ButtonBase extends ObserveSlotText(LikeAnchor(Focusable), '', [
65✔
33
    'sp-overlay,sp-tooltip',
65✔
34
]) {
65✔
35
    public static override get styles(): CSSResultArray {
65✔
36
        return [buttonStyles];
65✔
37
    }
65✔
38

65✔
39
    // TODO we need to document this property for consumers,
65✔
40
    // as it's not a 1:1 equivalent to active
65✔
41
    @property({ type: Boolean, reflect: true })
65✔
42
    public active = false;
65✔
43

65✔
44
    /**
65✔
45
     * The default behavior of the button.
65✔
46
     * Possible values are: `button` (default), `submit`, and `reset`.
65✔
47
     */
65✔
48
    @property({ type: String })
65✔
49
    public type: 'button' | 'submit' | 'reset' = 'button';
65✔
50

65✔
51
    /**
65✔
52
     * HTML anchor element that component clicks by proxy
65✔
53
     */
65✔
54
    @query('.anchor')
65✔
55
    private anchorElement!: HTMLAnchorElement;
65✔
56

65✔
57
    public override get focusElement(): HTMLElement {
65✔
58
        return this;
9,508✔
59
    }
9,508✔
60

65✔
61
    protected get hasLabel(): boolean {
65✔
62
        return this.slotHasContent;
1,883✔
63
    }
1,883✔
64

65✔
65
    protected get buttonContent(): TemplateResult[] {
65✔
66
        const content = [
1,880✔
67
            html`
1,880✔
68
                <slot name="icon" ?icon-only=${!this.hasLabel}></slot>
1,880✔
69
            `,
1,880✔
70
            html`
1,880✔
71
                <span id="label">
1,880✔
72
                    <slot @slotchange=${this.manageTextObservedSlot}></slot>
1,880✔
73
                </span>
1,880✔
74
            `,
1,880✔
75
        ];
1,880✔
76
        return content;
1,880✔
77
    }
1,880✔
78

65✔
79
    constructor() {
65✔
80
        super();
1,405✔
81
        this.proxyFocus = this.proxyFocus.bind(this);
1,405✔
82

1,405✔
83
        this.addEventListener('click', this.handleClickCapture, {
1,405✔
84
            capture: true,
1,405✔
85
        });
1,405✔
86
    }
1,405✔
87

65✔
88
    private handleClickCapture(event: Event): void | boolean {
65✔
89
        if (this.disabled) {
237✔
90
            event.preventDefault();
1✔
91
            event.stopImmediatePropagation();
1✔
92
            event.stopPropagation();
1✔
93
            return false;
1✔
94
        }
1✔
95

236✔
96
        if (this.shouldProxyClick(event as MouseEvent)) {
237✔
97
            return;
10✔
98
        }
10✔
99
    }
237✔
100

65✔
101
    private proxyFocus(): void {
65✔
102
        this.focus();
2✔
103
    }
2✔
104

65✔
105
    private shouldProxyClick(event?: MouseEvent): boolean {
65✔
106
        let handled = false;
236✔
107

236✔
108
        // Don't proxy clicks with modifier keys (Command/Meta, Ctrl, Shift, Alt)
236✔
109
        if (
236✔
110
            event &&
236✔
111
            (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
236✔
112
        ) {
236✔
113
            return false;
2✔
114
        }
2✔
115

234✔
116
        if (this.anchorElement) {
236✔
117
            // Click HTML anchor element by proxy, but only for non-modified clicks
6✔
118
            this.anchorElement.click();
6✔
119
            handled = true;
6✔
120
            // if the button type is `submit` or `reset`
6✔
121
        } else if (this.type !== 'button') {
236✔
122
            // create an HTML Button Element by proxy, click it, and remove it
4✔
123
            const proxy = document.createElement('button');
4✔
124
            proxy.type = this.type;
4✔
125
            this.insertAdjacentElement('afterend', proxy);
4✔
126
            proxy.click();
4✔
127
            proxy.remove();
4✔
128
            handled = true;
4✔
129
        }
4✔
130
        return handled;
234✔
131
    }
236✔
132

65✔
133
    public override renderAnchor(): TemplateResult {
65✔
134
        return html`
29✔
135
            ${this.buttonContent}
29✔
136
            ${super.renderAnchor({
29✔
137
                id: 'button',
29✔
138
                ariaHidden: true,
29✔
139
                className: 'button anchor',
29✔
140
                tabindex: -1,
29✔
141
            })}
29✔
142
        `;
29✔
143
    }
29✔
144

65✔
145
    protected renderButton(): TemplateResult {
65✔
146
        return html`
1,969✔
147
            ${this.buttonContent}
1,969✔
148
        `;
1,969✔
149
    }
1,969✔
150

65✔
151
    protected override render(): TemplateResult {
65✔
152
        return this.href && this.href.length > 0
2,560✔
153
            ? this.renderAnchor()
29✔
154
            : this.renderButton();
2,531✔
155
    }
2,560✔
156

65✔
157
    protected handleKeydown(event: KeyboardEvent): void {
65✔
158
        const { code } = event;
104✔
159
        switch (code) {
104✔
160
            case 'Space':
104✔
161
                event.preventDefault();
9✔
162
                // allows button to activate when `Space` is pressed
9✔
163
                if (typeof this.href === 'undefined') {
9✔
164
                    this.addEventListener('keyup', this.handleKeyup);
9✔
165
                    this.active = true;
9✔
166
                }
9✔
167
                break;
9✔
168
            default:
104✔
169
                break;
95✔
170
        }
104✔
171
    }
104✔
172

65✔
173
    private handleKeypress(event: KeyboardEvent): void {
65✔
174
        const { code } = event;
29✔
175
        switch (code) {
29✔
176
            case 'Enter':
29✔
177
            case 'NumpadEnter':
29✔
178
                // allows button or link to be activated with `Enter` and `NumpadEnter`
28✔
179
                this.click();
28✔
180
                break;
28✔
181
            default:
29✔
182
                break;
1✔
183
        }
29✔
184
    }
29✔
185

65✔
186
    protected handleKeyup(event: KeyboardEvent): void {
65✔
187
        const { code } = event;
10✔
188
        switch (code) {
10✔
189
            case 'Space':
10✔
190
                this.removeEventListener('keyup', this.handleKeyup);
9✔
191
                this.active = false;
9✔
192
                this.click();
9✔
193
                break;
9✔
194
            default:
10✔
195
                break;
1✔
196
        }
10✔
197
    }
10✔
198

65✔
199
    private manageAnchor(): void {
65✔
200
        // for a link
1,419✔
201
        if (this.href && this.href.length > 0) {
1,419✔
202
            // if the role is set to button
25✔
203
            if (
25✔
204
                !this.hasAttribute('role') ||
25✔
205
                this.getAttribute('role') === 'button'
14✔
206
            ) {
25✔
207
                // change role to link
13✔
208
                this.setAttribute('role', 'link');
13✔
209
            }
13✔
210
            // else for a button
25✔
211
        } else {
1,419✔
212
            // if the role is set to link
1,394✔
213
            if (
1,394✔
214
                !this.hasAttribute('role') ||
1,394✔
215
                this.getAttribute('role') === 'link'
534✔
216
            ) {
1,394✔
217
                // change role to button
861✔
218
                this.setAttribute('role', 'button');
861✔
219
            }
861✔
220
        }
1,394✔
221
    }
1,419✔
222

65✔
223
    protected override firstUpdated(changed: PropertyValues): void {
65✔
224
        super.firstUpdated(changed);
1,405✔
225
        if (!this.hasAttribute('tabindex')) {
1,405✔
226
            this.setAttribute('tabindex', '0');
996✔
227
        }
996✔
228

1,405✔
229
        this.manageAnchor();
1,405✔
230
        this.addEventListener('keydown', this.handleKeydown);
1,405✔
231
        this.addEventListener('keypress', this.handleKeypress);
1,405✔
232
    }
1,405✔
233

65✔
234
    protected override updated(changed: PropertyValues): void {
65✔
235
        super.updated(changed);
2,631✔
236
        if (changed.has('href')) {
2,631✔
237
            this.manageAnchor();
14✔
238
        }
14✔
239

2,631✔
240
        if (changed.has('label')) {
2,631✔
241
            if (this.label) {
666✔
242
                this.setAttribute('aria-label', this.label);
665✔
243
            } else {
666✔
244
                this.removeAttribute('aria-label');
1✔
245
            }
1✔
246
        }
666✔
247

2,631✔
248
        if (this.anchorElement) {
2,631✔
249
            // Ensure the anchor element is not focusable directly via tab
29✔
250
            this.anchorElement.tabIndex = -1;
29✔
251

29✔
252
            // Make sure it has proper ARIA attributes
29✔
253
            if (!this.anchorElement.hasAttribute('aria-hidden')) {
29!
UNCOV
254
                this.anchorElement.setAttribute('aria-hidden', 'true');
×
UNCOV
255
            }
×
256

29✔
257
            // Set up focus delegation
29✔
258
            this.anchorElement.addEventListener('focus', this.proxyFocus);
29✔
259
        }
29✔
260
    }
2,631✔
261
}
65✔
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