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

adobe / spectrum-web-components / 17085845634

20 Aug 2025 01:18AM UTC coverage: 94.672%. First build
17085845634

Pull #5703

github

web-flow
Merge 670472557 into 67c34812c
Pull Request #5703: chore: initial POC of prefixed tag names

4878 of 5220 branches covered (93.45%)

Branch coverage included in aggregate %.

53 of 72 new or added lines in 6 files covered. (73.61%)

32775 of 34552 relevant lines covered (94.86%)

555.66 hits per line

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

94.16
/tools/base/src/Base.ts
1
/**
130✔
2
 * Copyright 2025 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

130✔
16
import { defineElement } from './define-element.js';
130✔
17

130✔
18
type ThemeRoot = HTMLElement & {
130✔
19
    startManagingContentDirection: (el: HTMLElement) => void;
130✔
20
    stopManagingContentDirection: (el: HTMLElement) => void;
130✔
21
};
130✔
22

130✔
23
export type Constructor<T = Record<string, unknown>> = {
130✔
24
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
130✔
25
    new (...args: any[]): T;
130✔
26
    prototype: T;
130✔
27
};
130✔
28
export type PrefixedConstructor<T = Record<string, unknown>> = {
130✔
29
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
130✔
30
    new (...args: any[]): T;
130✔
31
    prototype: T;
130✔
32
    VERSION: string;
130✔
33
    tagName: string;
130✔
34
    prefix: string;
130✔
35
    tag: string;
130✔
36
};
130✔
37

130✔
38
export interface SpectrumInterface {
130✔
39
    shadowRoot: ShadowRoot;
130✔
40
    isLTR: boolean;
130✔
41
    hasVisibleFocusInTree(): boolean;
130✔
42
    dir: 'ltr' | 'rtl';
130✔
43
}
130✔
44

130✔
45
const observedForElements: Set<HTMLElement> = new Set();
130✔
46

130✔
47
const updateRTL = (): void => {
130✔
48
    const dir =
384✔
49
        document.documentElement.dir === 'rtl'
384✔
50
            ? document.documentElement.dir
4✔
51
            : 'ltr';
380✔
52
    observedForElements.forEach((el) => {
384✔
53
        el.setAttribute('dir', dir);
127✔
54
    });
384✔
55
};
384✔
56

130✔
57
const rtlObserver = new MutationObserver(updateRTL);
130✔
58

130✔
59
rtlObserver.observe(document.documentElement, {
130✔
60
    attributes: true,
130✔
61
    attributeFilter: ['dir'],
130✔
62
});
130✔
63

130✔
64
type ContentDirectionManager = HTMLElement & {
130✔
65
    startManagingContentDirection?(): void;
130✔
66
};
130✔
67

130✔
68
const canManageContentDirection = (el: ContentDirectionManager): boolean =>
130✔
69
    typeof el.startManagingContentDirection !== 'undefined' ||
145,907✔
70
    el.tagName === 'SP-THEME';
138,899✔
71

130✔
72
export function SpectrumMixin<T extends Constructor<ReactiveElement>>(
130✔
73
    constructor: T,
175✔
74
    tagName: string = 'base'
175✔
75
): T & PrefixedConstructor<SpectrumInterface> {
175✔
76
    class SpectrumMixinElement extends constructor {
175✔
77
        /**
175✔
78
         * @private
175✔
79
         */
175✔
80
        public override shadowRoot!: ShadowRoot;
175✔
81
        private _dirParent?: HTMLElement;
175✔
82

175✔
83
        /**
175✔
84
         * @private
175✔
85
         */
175✔
86
        public override dir!: 'ltr' | 'rtl';
175✔
87

175✔
88
        /**
175✔
89
         * @private
175✔
90
         */
175✔
91
        public get isLTR(): boolean {
175✔
92
            return this.dir === 'ltr';
829✔
93
        }
829✔
94

174✔
95
        public static VERSION = version;
174✔
96
        public static prefix = 'sp';
174✔
97
        public static tagName = tagName;
174✔
98

174✔
99
        public static get tag(): string {
175✔
100
            // note static getters this is the constructor so this method
79✔
101
            // gets the derived class prefix and tag
79✔
102
            return `${this.prefix}-${this.tagName}`;
79✔
103
        }
79✔
104

174✔
105
        public hasVisibleFocusInTree(): boolean {
175✔
106
            const getAncestors = (root: Document = document): HTMLElement[] => {
97✔
107
                // eslint-disable-next-line @spectrum-web-components/document-active-element
97✔
108
                let currentNode = root.activeElement as HTMLElement;
97✔
109
                while (
97✔
110
                    currentNode?.shadowRoot &&
97✔
111
                    currentNode.shadowRoot.activeElement
127✔
112
                ) {
97✔
113
                    currentNode = currentNode.shadowRoot
98✔
114
                        .activeElement as HTMLElement;
98✔
115
                }
98✔
116
                const ancestors: HTMLElement[] = currentNode
97✔
117
                    ? [currentNode]
97✔
118
                    : [];
×
119
                while (currentNode) {
97✔
120
                    const ancestor =
695✔
121
                        currentNode.assignedSlot ||
695✔
122
                        currentNode.parentElement ||
597✔
123
                        (currentNode.getRootNode() as ShadowRoot)?.host;
293✔
124
                    if (ancestor) {
695✔
125
                        ancestors.push(ancestor as HTMLElement);
598✔
126
                    }
598✔
127
                    currentNode = ancestor as HTMLElement;
695✔
128
                }
695✔
129
                return ancestors;
97✔
130
            };
97✔
131
            const activeElement = getAncestors(
97✔
132
                this.getRootNode() as Document
97✔
133
            )[0];
97✔
134
            if (!activeElement) {
97✔
135
                return false;
×
136
            }
×
137
            // Browsers without support for the `:focus-visible`
97✔
138
            // selector will throw on the following test (Safari, older things).
97✔
139
            // Some won't throw, but will be focusing item rather than the menu and
97✔
140
            // will rely on the polyfill to know whether focus is "visible" or not.
97✔
141
            try {
97✔
142
                return (
97✔
143
                    activeElement.matches(':focus-visible') ||
97✔
144
                    activeElement.matches('.focus-visible')
23✔
145
                );
97✔
146
                /* c8 ignore next 3 */
130✔
147
            } catch (error) {
130✔
148
                return activeElement.matches('.focus-visible');
130✔
149
            }
130✔
150
        }
97✔
151

174✔
152
        public override connectedCallback(): void {
175✔
153
            if (!this.hasAttribute('dir')) {
18,929✔
154
                let dirParent = ((this as HTMLElement).assignedSlot ||
18,897✔
155
                    this.parentNode) as HTMLElement;
14,801✔
156
                while (
18,897✔
157
                    dirParent !== document.documentElement &&
18,897✔
158
                    !canManageContentDirection(
145,907✔
159
                        dirParent as ContentDirectionManager
145,907✔
160
                    )
145,907✔
161
                ) {
18,897✔
162
                    dirParent = ((dirParent as HTMLElement).assignedSlot || // step into the shadow DOM of the parent of a slotted node
138,891✔
163
                        dirParent.parentNode || // DOM Element detected
114,911✔
164
                        (dirParent as unknown as ShadowRoot)
35,886✔
165
                            .host) as HTMLElement;
35,886✔
166
                }
138,899✔
167
                this.dir =
18,905✔
168
                    dirParent.dir === 'rtl' ? dirParent.dir : this.dir || 'ltr';
18,905✔
169
                if (dirParent === document.documentElement) {
18,897✔
170
                    observedForElements.add(this);
11,889✔
171
                } else {
18,889✔
172
                    const { localName } = dirParent;
7,008✔
173
                    if (
7,008✔
174
                        localName.search('-') > -1 &&
7,008✔
175
                        !customElements.get(localName)
7,008✔
176
                    ) {
7,008✔
177
                        /* c8 ignore next 5 */
130✔
178
                        customElements.whenDefined(localName).then(() => {
130✔
179
                            (
130✔
180
                                dirParent as ThemeRoot
130✔
181
                            ).startManagingContentDirection(this);
130✔
182
                        });
130✔
183
                    } else {
7,008✔
184
                        (dirParent as ThemeRoot).startManagingContentDirection(
7,008✔
185
                            this
7,008✔
186
                        );
7,008✔
187
                    }
7,008✔
188
                }
7,008✔
189
                this._dirParent = dirParent as HTMLElement;
18,897✔
190
            }
18,897✔
191
            super.connectedCallback();
18,929✔
192
        }
18,929✔
193

182✔
194
        public override disconnectedCallback(): void {
175✔
195
            super.disconnectedCallback();
18,923✔
196
            if (this._dirParent) {
18,923✔
197
                if (this._dirParent === document.documentElement) {
18,891✔
198
                    observedForElements.delete(this);
11,883✔
199
                } else {
18,891✔
200
                    (this._dirParent as ThemeRoot).stopManagingContentDirection(
7,016✔
201
                        this
7,008✔
202
                    );
7,008✔
203
                }
7,016✔
204
                this.removeAttribute('dir');
18,891✔
205
            }
18,891✔
206
        }
18,923✔
207
    }
182✔
208
    return SpectrumMixinElement;
175✔
209
}
175✔
210

130✔
211
export class SpectrumElement extends SpectrumMixin(LitElement, 'base') {
130✔
212
    public static override VERSION = version;
130✔
213
}
130✔
214

130✔
215
export function PrefixedMixin<T extends PrefixedConstructor<SpectrumInterface>>(
130✔
NEW
216
    constructor: T,
×
NEW
217
    tagName: string,
×
NEW
218
    prefix: string = 'sp'
×
NEW
219
): T & PrefixedConstructor<SpectrumInterface> {
×
NEW
220
    class PrefixedMixinElement extends constructor {
×
NEW
221
        public static override prefix = prefix;
×
NEW
222
        public static override tagName = tagName;
×
NEW
223
    }
×
NEW
224
    return PrefixedMixinElement;
×
NEW
225
}
×
226

129✔
227
export function definePrefixedElement(
130✔
NEW
228
    name: string,
×
NEW
229
    prefix: string,
×
NEW
230
    constructor: PrefixedConstructor<SpectrumInterface>
×
NEW
231
): void {
×
NEW
232
    const PrefixedClass = PrefixedMixin(constructor, name, prefix);
×
NEW
233
    defineElement(
×
NEW
234
        PrefixedClass.tag,
×
NEW
235
        PrefixedClass as unknown as CustomElementConstructor
×
NEW
236
    );
×
237
}
×
238

130✔
239
if (window.__swc.DEBUG) {
130✔
240
    const ignoreWarningTypes = {
130✔
241
        default: false,
130✔
242
        accessibility: false,
130✔
243
        api: false,
130✔
244
    };
130✔
245
    const ignoreWarningLevels = {
130✔
246
        default: false,
130✔
247
        low: false,
130✔
248
        medium: false,
130✔
249
        high: false,
130✔
250
        deprecation: false,
130✔
251
    };
130✔
252
    window.__swc = {
130✔
253
        ...window.__swc,
130✔
254
        ignoreWarningLocalNames: {
130✔
255
            /* c8 ignore next 1 */
130✔
256
            ...(window.__swc?.ignoreWarningLocalNames || {}),
130✔
257
        },
130✔
258
        ignoreWarningTypes: {
130✔
259
            ...ignoreWarningTypes,
130✔
260
            /* c8 ignore next 1 */
130✔
261
            ...(window.__swc?.ignoreWarningTypes || {}),
130✔
262
        },
130✔
263
        ignoreWarningLevels: {
130✔
264
            ...ignoreWarningLevels,
130✔
265
            /* c8 ignore next 1 */
130✔
266
            ...(window.__swc?.ignoreWarningLevels || {}),
130✔
267
        },
130✔
268
        issuedWarnings: new Set(),
130✔
269
        warn: (
130✔
270
            element,
1,997✔
271
            message,
1,997✔
272
            url,
1,997✔
273
            { type = 'api', level = 'default', issues } = {}
1,997✔
274
        ): void => {
1,997✔
275
            const { localName = 'base' } = element || {};
1,997✔
276
            const id = `${localName}:${type}:${level}` as BrandedSWCWarningID;
1,997✔
277
            if (!window.__swc.verbose && window.__swc.issuedWarnings.has(id))
1,997✔
278
                return;
1,997✔
279
            /* c8 ignore next 3 */
130✔
280
            if (window.__swc.ignoreWarningLocalNames[localName]) return;
130✔
281
            if (window.__swc.ignoreWarningTypes[type]) return;
130✔
282
            if (window.__swc.ignoreWarningLevels[level]) return;
130✔
283
            window.__swc.issuedWarnings.add(id);
327✔
284
            let listedIssues = '';
327✔
285
            if (issues && issues.length) {
1,997✔
286
                issues.unshift('');
46✔
287
                listedIssues = issues.join('\n    - ') + '\n';
46✔
288
            }
46✔
289
            const intro = level === 'deprecation' ? 'DEPRECATION NOTICE: ' : '';
1,997✔
290
            const inspectElement = element
1,997✔
291
                ? '\nInspect this issue in the follow element:'
100✔
292
                : '';
228✔
293
            const displayURL = (element ? '\n\n' : '\n') + url + '\n';
1,997✔
294
            const messages: unknown[] = [];
1,997✔
295
            messages.push(
1,997✔
296
                intro + message + '\n' + listedIssues + inspectElement
1,997✔
297
            );
1,997✔
298
            if (element) {
1,997✔
299
                messages.push(element);
100✔
300
            }
100✔
301
            messages.push(displayURL, {
327✔
302
                data: {
327✔
303
                    localName,
327✔
304
                    type,
327✔
305
                    level,
327✔
306
                },
327✔
307
            });
327✔
308
            console.warn(...messages);
327✔
309
        },
1,997✔
310
    };
130✔
311

130✔
312
    window.__swc.warn(
130✔
313
        undefined,
130✔
314
        'Spectrum Web Components is in dev mode. Not recommended for production!',
130✔
315
        'https://opensource.adobe.com/spectrum-web-components/dev-mode/',
130✔
316
        { type: 'default' }
130✔
317
    );
130✔
318
}
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