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

adobe / spectrum-web-components / 13553164764

26 Feb 2025 08:53PM CUT coverage: 97.966% (-0.2%) from 98.185%
13553164764

Pull #5031

github

web-flow
Merge b7398ad1e into 191a15bd9
Pull Request #5031: fix(action menu): keyboard accessibility omnibus

5295 of 5602 branches covered (94.52%)

Branch coverage included in aggregate %.

627 of 678 new or added lines in 11 files covered. (92.48%)

27 existing lines in 8 files now uncovered.

33662 of 34164 relevant lines covered (98.53%)

644.21 hits per line

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

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

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

30✔
13
import {
30✔
14
    ReactiveController,
30✔
15
    TemplateResult,
30✔
16
} from '@spectrum-web-components/base';
30✔
17
import { AbstractOverlay } from '@spectrum-web-components/overlay/src/AbstractOverlay';
30✔
18
import { Overlay } from '@spectrum-web-components/overlay/src/Overlay.js';
30✔
19
import { PickerBase } from './Picker.js';
30✔
20

30✔
21
export enum InteractionTypes {
30✔
22
    'desktop',
30✔
23
    'mobile',
30✔
24
}
30✔
25
export const SAFARI_FOCUS_RING_CLASS = 'remove-focus-ring-safari-hack';
30✔
26

30✔
27
export class InteractionController implements ReactiveController {
30✔
28
    abortController!: AbortController;
30✔
29

30✔
30
    public preventNextToggle: 'no' | 'maybe' | 'yes' = 'no';
30✔
31
    public pointerdownState = false;
30✔
32
    public enterKeydownOn: EventTarget | null = null;
30✔
33

30✔
34
    public container!: TemplateResult;
30✔
35

30✔
36
    get activelyOpening(): boolean {
30✔
37
        return false;
30✔
38
    }
30✔
39

30✔
40
    private _open = false;
30✔
41

30✔
42
    public get open(): boolean {
30✔
43
        return this._open;
433✔
44
    }
433✔
45

30✔
46
    /**
30✔
47
     * Set `open`
30✔
48
     */
30✔
49
    public set open(open: boolean) {
30✔
50
        if (this._open === open) return;
1,759✔
51
        this._open = open;
279✔
52

279✔
53
        if (this.overlay) {
1,216✔
54
            this.host.open = open;
157✔
55
            return;
157✔
56
        }
157✔
57

122✔
58
        // When there is no Overlay and `open` is moving to `true`, lazily import/create
122✔
59
        // an Overlay and apply that state to it.
122✔
60
        customElements
122✔
61
            .whenDefined('sp-overlay')
122✔
62
            .then(async (): Promise<void> => {
122✔
63
                const { Overlay } = await import(
122✔
64
                    '@spectrum-web-components/overlay/src/Overlay.js'
122✔
65
                );
122✔
66
                this.overlay = new Overlay();
122✔
67
                this.host.open = true;
122✔
68
                this.host.requestUpdate();
122✔
69
            });
122✔
70
        import('@spectrum-web-components/overlay/sp-overlay.js');
122✔
71
    }
1,759✔
72

30✔
73
    private _overlay!: AbstractOverlay;
30✔
74

30✔
75
    public get overlay(): AbstractOverlay {
30✔
76
        return this._overlay;
4,959✔
77
    }
4,959✔
78

30✔
79
    public set overlay(overlay: AbstractOverlay | undefined) {
30✔
80
        if (!overlay) return;
122!
81
        if (this.overlay === overlay) return;
122!
82
        this._overlay = overlay;
122✔
83
        this.initOverlay();
122✔
84
    }
122✔
85

30✔
86
    type!: InteractionTypes;
30✔
87

30✔
88
    constructor(
30✔
89
        public target: HTMLElement,
470✔
90
        public host: PickerBase
470✔
91
    ) {
470✔
92
        this.target = target;
470✔
93
        this.host = host;
470✔
94
        this.host.addController(this);
470✔
95
        this.init();
470✔
96
    }
470✔
97

30✔
98
    releaseDescription(): void {}
30✔
99

30✔
100
    protected handleBeforetoggle(
30✔
101
        event: Event & {
252✔
102
            target: Overlay;
252✔
103
            newState: 'open' | 'closed';
252✔
104
        }
252✔
105
    ): void {
252✔
106
        if (event.composedPath()[0] !== event.target) {
252!
107
            return;
×
108
        }
×
109
        if (event.newState === 'closed') {
252✔
110
            if (this.preventNextToggle === 'no') {
125✔
111
                this.open = false;
119✔
112
            } else if (!this.pointerdownState) {
125✔
113
                // Prevent browser driven closure while opening the Picker
×
114
                // and the expected event series has not completed.
×
115
                this.overlay?.manuallyKeepOpen();
×
116
            }
×
117
        }
125✔
118
        if (!this.open) {
252✔
119
            this.host.optionsMenu.updateSelectedItemIndex();
125✔
120
            this.host.optionsMenu.closeDescendentOverlays();
125✔
121
        }
125✔
122
    }
252✔
123

30✔
124
    initOverlay(): void {
30✔
125
        if (this.overlay) {
122✔
126
            this.overlay.addEventListener('beforetoggle', (event: Event) => {
122✔
127
                this.handleBeforetoggle(
252✔
128
                    event as Event & {
252✔
129
                        target: Overlay;
252✔
130
                        newState: 'open' | 'closed';
252✔
131
                    }
252✔
132
                );
252✔
133
            });
122✔
134
            this.overlay.type = this.host.isMobile.matches ? 'modal' : 'auto';
122✔
135
            this.overlay.triggerElement = this.host as HTMLElement;
122✔
136
            this.overlay.placement =
122✔
137
                this.host.isMobile.matches && !this.host.forcePopover
122✔
138
                    ? undefined
2✔
139
                    : this.host.placement;
120✔
140
            // We should not be applying open is set programmatically via the picker's open.property.
122✔
141
            // Focus should only be applied if a user action causes the menu to open. Otherwise,
122✔
142
            // we could be pulling focus from a user when an picker with an open menu loads.
122✔
143
            this.overlay.receivesFocus = 'false';
122✔
144
            this.overlay.willPreventClose =
122✔
145
                this.preventNextToggle !== 'no' && this.open;
122✔
146
            this.overlay.addEventListener(
122✔
147
                'slottable-request',
122✔
148
                this.host.handleSlottableRequest
122✔
149
            );
122✔
150
        }
122✔
151
    }
122✔
152

30✔
153
    public handlePointerdown(_event: PointerEvent): void {}
30✔
154

30✔
155
    public handleButtonFocus(event: FocusEvent): void {
30✔
156
        // When focus comes from a pointer event, and the related target is the Menu,
258✔
157
        // we don't want to reopen the Menu.
258✔
158
        if (
258✔
159
            this.preventNextToggle === 'maybe' &&
258✔
160
            event.relatedTarget === this.host.optionsMenu
38✔
161
        ) {
258!
UNCOV
162
            this.preventNextToggle = 'yes';
×
UNCOV
163
        }
×
164
        if (this.preventNextToggle === 'no') this.host.close();
258✔
165
    }
258✔
166

30✔
167
    public handleActivate(_event: Event): void {}
30✔
168

30✔
169
    /* c8 ignore next 3 */
30✔
170
    init(): void {}
30✔
171

30✔
172
    abort(): void {
30✔
173
        this.releaseDescription();
4✔
174
        this.abortController?.abort();
4!
175
    }
4✔
176

30✔
177
    hostConnected(): void {
30✔
178
        this.init();
476✔
179
        this.host.addEventListener('sp-closed', ()=> {
476✔
180
            if(!this.open && this.host.optionsMenu.matches(':focus-within') && !this.host.button.matches(':focus')) {
127✔
181
                this.host.button.focus();
55✔
182
            }
55✔
183
        });
476✔
184
    }
476✔
185

30✔
186
    hostDisconnected(): void {
30✔
187
        this.abortController?.abort();
476!
188
    }
476✔
189

30✔
190
    public hostUpdated(): void {
30✔
191
        if (
1,029✔
192
            this.overlay &&
1,029✔
193
            this.host.dependencyManager.loaded &&
478✔
194
            this.host.open !== this.overlay.open
470✔
195
        ) {
1,029✔
196
            this.overlay.willPreventClose = this.preventNextToggle !== 'no';
216✔
197
            this.overlay.open = this.host.open;
216✔
198
        }
216✔
199
    }
1,029✔
200
}
30✔
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