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

adobe / spectrum-web-components / 13935064496

18 Mar 2025 10:56PM UTC coverage: 97.833% (-0.1%) from 97.981%
13935064496

Pull #5095

github

web-flow
Merge c94c4fd27 into 762e236da
Pull Request #5095: feat(accordion,base,theme): initial global system reporting

5301 of 5604 branches covered (94.59%)

Branch coverage included in aggregate %.

107 of 128 new or added lines in 5 files covered. (83.59%)

43 existing lines in 4 files now uncovered.

33624 of 34183 relevant lines covered (98.36%)

703.37 hits per line

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

96.36
/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 { adoptStyles, CSSResultOrNative, LitElement, ReactiveElement } from 'lit';
130✔
14
import StyleObserver from 'style-observer';
130✔
15
import type { StyleObserverCallback } from 'style-observer';
130✔
16
import { createContext } from '@lit/context';
130✔
17
import { provide } from '@lit/context';
130✔
18
import { version } from '@spectrum-web-components/base/src/version.js';
130✔
19

130✔
20
export { consume, provide } from '@lit/context';
130✔
21

130✔
22
export type SystemThemes = 'spectrum'|'express'|'spectrum-two';
130✔
23
export type SystemThemeConfig = Map<SystemThemes, CSSResultOrNative | null>;
130✔
24

130✔
25
type ThemeRoot = HTMLElement & {
130✔
26
    startManagingContentDirection: (el: HTMLElement) => void;
130✔
27
    stopManagingContentDirection: (el: HTMLElement) => void;
130✔
28
};
130✔
29

130✔
30
type Constructor<T = Record<string, unknown>> = {
130✔
31
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
130✔
32
    new (...args: any[]): T;
130✔
33
    prototype: T;
130✔
34
};
130✔
35

130✔
36
export const systemContext = createContext<SystemThemes>('spectrum');
130✔
37

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

130✔
47
const observedForElements: Set<HTMLElement> = new Set();
130✔
48

130✔
49
const updateRTL = (): void => {
130✔
50
    const dir =
309✔
51
        document.documentElement.dir === 'rtl'
309✔
52
            ? document.documentElement.dir
3✔
53
            : 'ltr';
307✔
54
    observedForElements.forEach((el) => {
309✔
55
        el.setAttribute('dir', dir);
2✔
56
    });
309✔
57
};
309✔
58

130✔
59
const rtlObserver = new MutationObserver(updateRTL);
130✔
60

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

129✔
66
type ContentDirectionManager = HTMLElement & {
129✔
67
    startManagingContentDirection?(): void;
129✔
68
};
129✔
69

129✔
70
const canManageContentDirection = (el: ContentDirectionManager): boolean =>
129✔
71
    typeof el.startManagingContentDirection !== 'undefined' ||
195,754✔
72
    el.tagName === 'SP-THEME';
187,765✔
73

130✔
74
export function SpectrumMixin<T extends Constructor<ReactiveElement>>(
130✔
75
    constructor: T
130✔
76
): T & Constructor<SpectrumInterface> {
130✔
77
    class SpectrumMixinElement extends constructor {
130✔
78
        /**
23,672✔
79
         * @todo This should have a way to add a super call to the constructor
23,672✔
80
         */
23,672✔
81
        private styleProcessing: StyleObserverCallback = (records) => {
23,672✔
82
            // Get the value of the context CSS custom property
16✔
83
            records.forEach((record, idx) => {
16✔
84
                // There should only be one record
16✔
85
                if (idx > 0) return;
16✔
86

16✔
87
                this.system = record.value as SystemThemes;
16✔
88
            });
16✔
89
        };
16✔
90

23,687✔
91
        private styleObserver: InstanceType<typeof StyleObserver> = new StyleObserver(this.styleProcessing, '--context');
23,672✔
92
        public get systemTheming(): SystemThemeConfig {
23,672✔
93
            return new Map([
23,672✔
94
                ['spectrum', null],
23,671✔
95
                ['express', null],
23,671✔
96
                ['spectrum-two', null],
23,672✔
97
            ]);
23,672✔
98
        }
23,672✔
99

23,672✔
100
        /**
23,672✔
101
         * @private
23,672✔
102
         */
23,672✔
103
        public override shadowRoot!: ShadowRoot;
23,672✔
104
        private _dirParent?: HTMLElement;
23,672✔
105

23,672✔
106
        /**
23,672✔
107
         * @private
23,672✔
108
         */
23,672✔
109
        public override dir!: 'ltr' | 'rtl';
23,672✔
110

23,672✔
111
        /**
23,672✔
112
         * @private
23,672✔
113
         */
23,672✔
114
        public get isLTR(): boolean {
23,672✔
115
            return this.dir === 'ltr';
2,721✔
116
        }
2,721✔
117

23,672✔
118
        /**
23,672✔
119
         * @description The system context of the element. This is used to determine the
23,672✔
120
         * styling, markup, and/or API of the element and allows for the element to report on deprecations.
23,672✔
121
         * @memberof SpectrumMixinElement
23,672✔
122
         */
23,672✔
123
        @provide({context: systemContext})
23,672✔
124
        public system: SystemThemes = 'spectrum';
23,672!
125

129✔
126
        protected override update(changes: Map<PropertyKey, unknown>): void {
129✔
127
            super.update(changes);
41,616✔
128
            if (changes.has(this.system)) {
41,616✔
NEW
129
              this.forceSystemUpdate(changes.get(this.system) as SystemThemes);
×
NEW
130
            }
×
131
        }
41,616✔
132

129✔
133
        /**
129✔
134
         * @description Forces the element to update its system context and swap themes if system theming is available.
129✔
135
         * @param {(SystemThemes|undefined)} system - The optional system context to force the element to update to.
129✔
136
         * @memberof SpectrumMixinElement
129✔
137
         */
129✔
138
        public forceSystemUpdate(system: SystemThemes|undefined = undefined): void {
129✔
NEW
139
            const updateValue = system || this.system;
×
NEW
140

×
NEW
141
            // Swap themes if system theming is available
×
NEW
142
            let theme: CSSResultOrNative | null;
×
NEW
143
            if (this.systemTheming && this.systemTheming.has(updateValue) && this.systemTheming.get(updateValue)) {
✔
NEW
144
                theme = this.systemTheming.get(updateValue)!;
×
NEW
145
                this.updateComplete.then(() => {
×
NEW
146
                    if (theme !== null && this.shadowRoot) {
×
NEW
147
                        adoptStyles(this.shadowRoot, [theme!]);
×
NEW
148
                    }
×
NEW
149
                });
×
NEW
150
            }
×
NEW
151
        }
×
152

129✔
153
        public hasVisibleFocusInTree(): boolean {
129✔
154
            const getAncestors = (root: Document = document): HTMLElement[] => {
162✔
155
                // eslint-disable-next-line @spectrum-web-components/document-active-element
162✔
156
                let currentNode = root.activeElement as HTMLElement;
162✔
157
                while (
162✔
158
                    currentNode?.shadowRoot &&
162✔
159
                    currentNode.shadowRoot.activeElement
197✔
160
                ) {
162✔
161
                    currentNode = currentNode.shadowRoot
124✔
162
                        .activeElement as HTMLElement;
124✔
163
                }
124✔
164
                const ancestors: HTMLElement[] = currentNode
162✔
165
                    ? [currentNode]
157✔
166
                    : [];
5✔
167
                while (currentNode) {
162✔
168
                    const ancestor =
1,344✔
169
                        currentNode.assignedSlot ||
1,344✔
170
                        currentNode.parentElement ||
1,066✔
171
                        (currentNode.getRootNode() as ShadowRoot)?.host;
559✔
172
                    if (ancestor) {
1,344✔
173
                        ancestors.push(ancestor as HTMLElement);
1,187✔
174
                    }
1,187✔
175
                    currentNode = ancestor as HTMLElement;
1,344✔
176
                }
1,344✔
177
                return ancestors;
162✔
178
            };
162✔
179
            const activeElement = getAncestors(
162✔
180
                this.getRootNode() as Document
162✔
181
            )[0];
162✔
182
            if (!activeElement) {
162✔
183
                return false;
5✔
184
            }
5✔
185
            // Browsers without support for the `:focus-visible`
157✔
186
            // selector will throw on the following test (Safari, older things).
157✔
187
            // Some won't throw, but will be focusing item rather than the menu and
157✔
188
            // will rely on the polyfill to know whether focus is "visible" or not.
157✔
189
            try {
157✔
190
                return (
157✔
191
                    activeElement.matches(':focus-visible') ||
157✔
192
                    activeElement.matches('.focus-visible')
37✔
193
                );
162✔
194
                /* c8 ignore next 3 */
130✔
195
            } catch (error) {
130✔
196
                return activeElement.matches('.focus-visible');
130✔
197
            }
130✔
198
        }
162✔
199

129✔
200
        public override connectedCallback(): void {
129✔
201
            if (!this.hasAttribute('dir')) {
24,085✔
202
                let dirParent = ((this as HTMLElement).assignedSlot ||
24,060✔
203
                    this.parentNode) as HTMLElement;
19,662✔
204
                while (
24,060✔
205
                    dirParent !== document.documentElement &&
24,060✔
206
                    !canManageContentDirection(
195,754✔
207
                        dirParent as ContentDirectionManager
195,754✔
208
                    )
195,754✔
209
                ) {
24,060✔
210
                    dirParent = ((dirParent as HTMLElement).assignedSlot || // step into the shadow DOM of the parent of a slotted node
187,764✔
211
                        dirParent.parentNode || // DOM Element detected
156,919✔
212
                        (dirParent as unknown as ShadowRoot)
46,131✔
213
                            .host) as HTMLElement;
46,131✔
214
                }
187,764✔
215
                this.dir =
24,060✔
216
                    dirParent.dir === 'rtl' ? dirParent.dir : this.dir || 'ltr';
24,060✔
217
                if (dirParent === document.documentElement) {
24,060✔
218
                    observedForElements.add(this);
16,070✔
219
                } else {
24,060✔
220
                    const { localName } = dirParent;
7,990✔
221
                    if (
7,990✔
222
                        localName.search('-') > -1 &&
7,990✔
223
                        !customElements.get(localName)
7,990✔
224
                    ) {
7,990✔
225
                        /* c8 ignore next 5 */
130✔
226
                        customElements.whenDefined(localName).then(() => {
130✔
227
                            (
130✔
228
                                dirParent as ThemeRoot
130✔
229
                            ).startManagingContentDirection(this);
130✔
230
                        });
130✔
231
                    } else {
7,990✔
232
                        (dirParent as ThemeRoot).startManagingContentDirection(
7,990✔
233
                            this
7,990✔
234
                        );
7,990✔
235
                    }
7,990✔
236
                }
7,990✔
237
                this._dirParent = dirParent as HTMLElement;
24,060✔
238
            }
24,060✔
239

24,085✔
240
            // Initialize the system context
24,085✔
241
            this.system = this.style.getPropertyValue('--context') as SystemThemes;
24,085✔
242

24,085✔
243
            // Set up the styleObserver to watch for changes to the context CSS custom property
24,085✔
244
            this.styleObserver.observe(this);
24,085✔
245

24,085✔
246
            super.connectedCallback();
24,085✔
247
        }
24,085✔
248

129✔
249
        public override disconnectedCallback(): void {
129✔
250
            super.disconnectedCallback();
24,085✔
251
            if (this._dirParent) {
24,085✔
252
                if (this._dirParent === document.documentElement) {
24,060✔
253
                    observedForElements.delete(this);
16,070✔
254
                } else {
24,060✔
255
                    (this._dirParent as ThemeRoot).stopManagingContentDirection(
7,990✔
256
                        this
7,990✔
257
                    );
7,990✔
258
                }
7,990✔
259
                this.removeAttribute('dir');
24,060✔
260
            }
24,060✔
261

24,085✔
262
            // Stop observing context
24,085✔
263
            this.styleObserver.unobserve(this);
24,085✔
264
        }
24,085✔
265
    }
129✔
266

129✔
267
    return SpectrumMixinElement;
129✔
268
}
129✔
269

129✔
270
export class SpectrumElement extends SpectrumMixin(LitElement) {
137✔
271
    static VERSION = version;
137✔
272
}
130✔
273

130✔
274
if (window.__swc.DEBUG) {
130✔
275
    const ignoreWarningTypes = {
130✔
276
        default: false,
130✔
277
        accessibility: false,
130✔
278
        api: false,
130✔
279
    };
130✔
280
    const ignoreWarningLevels = {
130✔
281
        default: false,
130✔
282
        low: false,
130✔
283
        medium: false,
130✔
284
        high: false,
130✔
285
        deprecation: false,
130✔
286
    };
130✔
287
    window.__swc = {
130✔
288
        ...window.__swc,
130✔
289
        ignoreWarningLocalNames: {
130✔
290
            /* c8 ignore next 1 */
130✔
291
            ...(window.__swc?.ignoreWarningLocalNames || {}),
130✔
292
        },
130✔
293
        ignoreWarningTypes: {
130✔
294
            ...ignoreWarningTypes,
130✔
295
            /* c8 ignore next 1 */
130✔
296
            ...(window.__swc?.ignoreWarningTypes || {}),
130✔
297
        },
130✔
298
        ignoreWarningLevels: {
130✔
299
            ...ignoreWarningLevels,
130✔
300
            /* c8 ignore next 1 */
130✔
301
            ...(window.__swc?.ignoreWarningLevels || {}),
130✔
302
        },
130✔
303
        issuedWarnings: new Set(),
130✔
304
        warn: (
130✔
305
            element,
2,153✔
306
            message,
2,153✔
307
            url,
2,153✔
308
            { type = 'api', level = 'default', issues } = {}
2,153✔
309
        ): void => {
2,153✔
310
            const { localName = 'base' } = element || {};
2,153✔
311
            const id = `${localName}:${type}:${level}` as BrandedSWCWarningID;
2,153✔
312
            if (!window.__swc.verbose && window.__swc.issuedWarnings.has(id))
2,153✔
313
                return;
2,153✔
314
            /* c8 ignore next 3 */
130✔
315
            if (window.__swc.ignoreWarningLocalNames[localName]) return;
130✔
316
            if (window.__swc.ignoreWarningTypes[type]) return;
130✔
317
            if (window.__swc.ignoreWarningLevels[level]) return;
130✔
318
            window.__swc.issuedWarnings.add(id);
304✔
319
            let listedIssues = '';
304✔
320
            if (issues && issues.length) {
2,153✔
321
                issues.unshift('');
44✔
322
                listedIssues = issues.join('\n    - ') + '\n';
44✔
323
            }
44✔
324
            const intro = level === 'deprecation' ? 'DEPRECATION NOTICE: ' : '';
2,153✔
325
            const inspectElement = element
2,153✔
326
                ? '\nInspect this issue in the follow element:'
80✔
327
                : '';
225✔
328
            const displayURL = (element ? '\n\n' : '\n') + url + '\n';
2,153✔
329
            const messages: unknown[] = [];
2,153✔
330
            messages.push(
2,153✔
331
                intro + message + '\n' + listedIssues + inspectElement
2,153✔
332
            );
2,153✔
333
            if (element) {
2,153✔
334
                messages.push(element);
80✔
335
            }
80✔
336
            messages.push(displayURL, {
304✔
337
                data: {
304✔
338
                    localName,
304✔
339
                    type,
304✔
340
                    level,
304✔
341
                },
304✔
342
            });
304✔
343
            console.warn(...messages);
304✔
344
        },
2,153✔
345
    };
130✔
346

130✔
347
    window.__swc.warn(
130✔
348
        undefined,
130✔
349
        'Spectrum Web Components is in dev mode. Not recommended for production!',
130✔
350
        'https://opensource.adobe.com/spectrum-web-components/dev-mode/',
130✔
351
        { type: 'default' }
130✔
352
    );
130✔
353
}
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

© 2025 Coveralls, Inc