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

adobe / spectrum-web-components / 14107800110

27 Mar 2025 01:28PM UTC coverage: 97.696% (-0.3%) from 98.002%
14107800110

Pull #5270

github

web-flow
Merge c0b05db6d into f26b2ccf3
Pull Request #5270: Nikkimk/menu focus hover

5259 of 5574 branches covered (94.35%)

Branch coverage included in aggregate %.

36 of 38 new or added lines in 3 files covered. (94.74%)

101 existing lines in 4 files now uncovered.

33619 of 34221 relevant lines covered (98.24%)

648.1 hits per line

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

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

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

130✔
13
import { LitElement, ReactiveElement } from 'lit';
130✔
14
import { version } from '@spectrum-web-components/base/src/version.js';
130✔
15
type ThemeRoot = HTMLElement & {
130✔
16
    startManagingContentDirection: (el: HTMLElement) => void;
130✔
17
    stopManagingContentDirection: (el: HTMLElement) => void;
130✔
18
};
130✔
19

130✔
20
type Constructor<T = Record<string, unknown>> = {
130✔
21
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
130✔
22
    new (...args: any[]): T;
130✔
23
    prototype: T;
130✔
24
};
130✔
25

130✔
26
export interface SpectrumInterface {
130✔
27
    shadowRoot: ShadowRoot;
130✔
28
    isLTR: boolean;
130✔
29
    hasVisibleFocusInTree(): boolean;
130✔
30
    dir: 'ltr' | 'rtl';
130✔
31
}
130✔
32

130✔
33
const observedForElements: Set<HTMLElement> = new Set();
130✔
34

130✔
35
const updateRTL = (): void => {
130✔
36
    const dir =
296✔
37
        document.documentElement.dir === 'rtl'
296✔
38
            ? document.documentElement.dir
2✔
39
            : 'ltr';
294✔
40
    observedForElements.forEach((el) => {
296✔
41
        el.setAttribute('dir', dir);
1✔
42
    });
296✔
43
};
296✔
44

130✔
45
const rtlObserver = new MutationObserver(updateRTL);
130✔
46

130✔
47
rtlObserver.observe(document.documentElement, {
130✔
48
    attributes: true,
130✔
49
    attributeFilter: ['dir'],
130✔
50
});
130✔
51

130✔
52
type ContentDirectionManager = HTMLElement & {
130✔
53
    startManagingContentDirection?(): void;
130✔
54
};
130✔
55

130✔
56
const canManageContentDirection = (el: ContentDirectionManager): boolean =>
130✔
57
    typeof el.startManagingContentDirection !== 'undefined' ||
189,246✔
58
    el.tagName === 'SP-THEME';
181,567✔
59

130✔
60
export function SpectrumMixin<T extends Constructor<ReactiveElement>>(
130✔
61
    constructor: T
130✔
62
): T & Constructor<SpectrumInterface> {
130✔
63
    class SpectrumMixinElement extends constructor {
130✔
64
        /**
130✔
65
         * @private
130✔
66
         */
130✔
67
        public override shadowRoot!: ShadowRoot;
130✔
68
        private _dirParent?: HTMLElement;
130✔
69

130✔
70
        /**
130✔
71
         * @private
130✔
72
         */
130✔
73
        public override dir!: 'ltr' | 'rtl';
130✔
74

130✔
75
        /**
130✔
76
         * @private
130✔
77
         */
130✔
78
        public get isLTR(): boolean {
130✔
79
            return this.dir === 'ltr';
2,700✔
80
        }
2,700✔
81

129✔
82
        public hasVisibleFocusInTree(): boolean {
130✔
83
            const getAncestors = (root: Document = document): HTMLElement[] => {
149✔
84
                // eslint-disable-next-line @spectrum-web-components/document-active-element
149✔
85
                let currentNode = root.activeElement as HTMLElement;
149✔
86
                while (
149✔
87
                    currentNode?.shadowRoot &&
149✔
88
                    currentNode.shadowRoot.activeElement
201✔
89
                ) {
149✔
90
                    currentNode = currentNode.shadowRoot
148✔
91
                        .activeElement as HTMLElement;
148✔
92
                }
148✔
93
                const ancestors: HTMLElement[] = currentNode
149✔
94
                    ? [currentNode]
149✔
UNCOV
95
                    : [];
×
96
                while (currentNode) {
149✔
97
                    const ancestor =
1,015✔
98
                        currentNode.assignedSlot ||
1,015✔
99
                        currentNode.parentElement ||
888✔
100
                        (currentNode.getRootNode() as ShadowRoot)?.host;
425✔
101
                    if (ancestor) {
1,015✔
102
                        ancestors.push(ancestor as HTMLElement);
866✔
103
                    }
866✔
104
                    currentNode = ancestor as HTMLElement;
1,015✔
105
                }
1,015✔
106
                return ancestors;
149✔
107
            };
149✔
108
            const activeElement = getAncestors(
149✔
109
                this.getRootNode() as Document
149✔
110
            )[0];
149✔
111
            if (!activeElement) {
149✔
UNCOV
112
                return false;
×
UNCOV
113
            }
×
114
            // Browsers without support for the `:focus-visible`
149✔
115
            // selector will throw on the following test (Safari, older things).
149✔
116
            // Some won't throw, but will be focusing item rather than the menu and
149✔
117
            // will rely on the polyfill to know whether focus is "visible" or not.
149✔
118
            try {
149✔
119
                return (
149✔
120
                    activeElement.matches(':focus-visible') ||
149✔
121
                    activeElement.matches('.focus-visible')
39✔
122
                );
149✔
123
                /* c8 ignore next 3 */
130✔
124
            } catch (error) {
130✔
125
                return activeElement.matches('.focus-visible');
130✔
126
            }
130✔
127
        }
149✔
128

129✔
129
        public override connectedCallback(): void {
130✔
130
            if (!this.hasAttribute('dir')) {
23,041✔
131
                let dirParent = ((this as HTMLElement).assignedSlot ||
23,017✔
132
                    this.parentNode) as HTMLElement;
18,742✔
133
                while (
23,017✔
134
                    dirParent !== document.documentElement &&
23,017✔
135
                    !canManageContentDirection(
189,246✔
136
                        dirParent as ContentDirectionManager
189,246✔
137
                    )
189,246✔
138
                ) {
23,017✔
139
                    dirParent = ((dirParent as HTMLElement).assignedSlot || // step into the shadow DOM of the parent of a slotted node
181,559✔
140
                        dirParent.parentNode || // DOM Element detected
151,798✔
141
                        (dirParent as unknown as ShadowRoot)
44,624✔
142
                            .host) as HTMLElement;
44,624✔
143
                }
181,567✔
144
                this.dir =
23,025✔
145
                    dirParent.dir === 'rtl' ? dirParent.dir : this.dir || 'ltr';
23,025✔
146
                if (dirParent === document.documentElement) {
23,017✔
147
                    observedForElements.add(this);
15,338✔
148
                } else {
23,009✔
149
                    const { localName } = dirParent;
7,679✔
150
                    if (
7,679✔
151
                        localName.search('-') > -1 &&
7,679✔
152
                        !customElements.get(localName)
7,679✔
153
                    ) {
7,679✔
154
                        /* c8 ignore next 5 */
130✔
155
                        customElements.whenDefined(localName).then(() => {
130✔
156
                            (
130✔
157
                                dirParent as ThemeRoot
130✔
158
                            ).startManagingContentDirection(this);
130✔
159
                        });
130✔
160
                    } else {
7,679✔
161
                        (dirParent as ThemeRoot).startManagingContentDirection(
7,679✔
162
                            this
7,679✔
163
                        );
7,679✔
164
                    }
7,679✔
165
                }
7,679✔
166
                this._dirParent = dirParent as HTMLElement;
23,017✔
167
            }
23,017✔
168
            super.connectedCallback();
23,041✔
169
        }
23,041✔
170

137✔
171
        public override disconnectedCallback(): void {
130✔
172
            super.disconnectedCallback();
23,016✔
173
            if (this._dirParent) {
23,016✔
174
                if (this._dirParent === document.documentElement) {
22,992✔
175
                    observedForElements.delete(this);
15,316✔
176
                } else {
22,992✔
177
                    (this._dirParent as ThemeRoot).stopManagingContentDirection(
7,684✔
178
                        this
7,676✔
179
                    );
7,676✔
180
                }
7,684✔
181
                this.removeAttribute('dir');
22,992✔
182
            }
22,992✔
183
        }
23,016✔
184
    }
137✔
185
    return SpectrumMixinElement;
130✔
186
}
130✔
187

130✔
188
export class SpectrumElement extends SpectrumMixin(LitElement) {
130✔
189
    static VERSION = version;
130✔
190
}
130✔
191

130✔
192
if (window.__swc.DEBUG) {
130✔
193
    const ignoreWarningTypes = {
130✔
194
        default: false,
130✔
195
        accessibility: false,
130✔
196
        api: false,
130✔
197
    };
130✔
198
    const ignoreWarningLevels = {
130✔
199
        default: false,
130✔
200
        low: false,
130✔
201
        medium: false,
130✔
202
        high: false,
130✔
203
        deprecation: false,
130✔
204
    };
130✔
205
    window.__swc = {
130✔
206
        ...window.__swc,
130✔
207
        ignoreWarningLocalNames: {
130✔
208
            /* c8 ignore next 1 */
130✔
209
            ...(window.__swc?.ignoreWarningLocalNames || {}),
130✔
210
        },
130✔
211
        ignoreWarningTypes: {
130✔
212
            ...ignoreWarningTypes,
130✔
213
            /* c8 ignore next 1 */
130✔
214
            ...(window.__swc?.ignoreWarningTypes || {}),
130✔
215
        },
130✔
216
        ignoreWarningLevels: {
130✔
217
            ...ignoreWarningLevels,
130✔
218
            /* c8 ignore next 1 */
130✔
219
            ...(window.__swc?.ignoreWarningLevels || {}),
130✔
220
        },
130✔
221
        issuedWarnings: new Set(),
130✔
222
        warn: (
130✔
223
            element,
1,934✔
224
            message,
1,934✔
225
            url,
1,934✔
226
            { type = 'api', level = 'default', issues } = {}
1,934✔
227
        ): void => {
1,934✔
228
            const { localName = 'base' } = element || {};
1,934✔
229
            const id = `${localName}:${type}:${level}` as BrandedSWCWarningID;
1,934✔
230
            if (!window.__swc.verbose && window.__swc.issuedWarnings.has(id))
1,934✔
231
                return;
1,934✔
232
            /* c8 ignore next 3 */
130✔
233
            if (window.__swc.ignoreWarningLocalNames[localName]) return;
130✔
234
            if (window.__swc.ignoreWarningTypes[type]) return;
130✔
235
            if (window.__swc.ignoreWarningLevels[level]) return;
130✔
236
            window.__swc.issuedWarnings.add(id);
308✔
237
            let listedIssues = '';
308✔
238
            if (issues && issues.length) {
1,934✔
239
                issues.unshift('');
43✔
240
                listedIssues = issues.join('\n    - ') + '\n';
43✔
241
            }
43✔
242
            const intro = level === 'deprecation' ? 'DEPRECATION NOTICE: ' : '';
1,934✔
243
            const inspectElement = element
1,934✔
244
                ? '\nInspect this issue in the follow element:'
83✔
245
                : '';
226✔
246
            const displayURL = (element ? '\n\n' : '\n') + url + '\n';
1,934✔
247
            const messages: unknown[] = [];
1,934✔
248
            messages.push(
1,934✔
249
                intro + message + '\n' + listedIssues + inspectElement
1,934✔
250
            );
1,934✔
251
            if (element) {
1,934✔
252
                messages.push(element);
83✔
253
            }
83✔
254
            messages.push(displayURL, {
308✔
255
                data: {
308✔
256
                    localName,
308✔
257
                    type,
308✔
258
                    level,
308✔
259
                },
308✔
260
            });
308✔
261
            console.warn(...messages);
308✔
262
        },
1,934✔
263
    };
130✔
264

130✔
265
    window.__swc.warn(
130✔
266
        undefined,
130✔
267
        'Spectrum Web Components is in dev mode. Not recommended for production!',
130✔
268
        'https://opensource.adobe.com/spectrum-web-components/dev-mode/',
130✔
269
        { type: 'default' }
130✔
270
    );
130✔
271
}
130✔
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

© 2026 Coveralls, Inc