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

adobe / spectrum-web-components / 14094674923

26 Mar 2025 10:27PM UTC coverage: 86.218% (-11.8%) from 98.002%
14094674923

Pull #5221

github

web-flow
Merge 2a1ea92e7 into 3184c1e6a
Pull Request #5221: RFC | leverage css module imports in components

1737 of 2032 branches covered (85.48%)

Branch coverage included in aggregate %.

14184 of 16434 relevant lines covered (86.31%)

85.29 hits per line

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

17.39
/packages/top-nav/src/TopNav.ts
1
/* eslint-disable lit-a11y/click-events-have-key-events */
1✔
2
/*
1✔
3
Copyright 2020 Adobe. All rights reserved.
1✔
4
This file is licensed to you under the Apache License, Version 2.0 (the "License");
1✔
5
you may not use this file except in compliance with the License. You may obtain a copy
1✔
6
of the License at http://www.apache.org/licenses/LICENSE-2.0
1✔
7

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

1✔
14
import {
1✔
15
    CSSResultArray,
1✔
16
    html,
1✔
17
    PropertyValues,
1✔
18
    SizedMixin,
1✔
19
    SpectrumElement,
1✔
20
    TemplateResult,
1✔
21
} from '@spectrum-web-components/base';
1✔
22
import {
1✔
23
    property,
1✔
24
    query,
1✔
25
} from '@spectrum-web-components/base/src/decorators.js';
1✔
26
import { ifDefined } from '@spectrum-web-components/base/src/directives.js';
1✔
27
import { ResizeController } from '@lit-labs/observers/resize-controller.js';
1✔
28
import { TopNavItem } from './TopNavItem.js';
1✔
29

1✔
30
import tabsSizes from '@spectrum-web-components/tabs/src/tabs-sizes.css.js';
1✔
31
import tabStyles from '@spectrum-web-components/tabs/src/tabs.css.js';
1✔
32
import { ScaledIndicator } from '@spectrum-web-components/tabs/src/Tabs.js';
1✔
33

1✔
34
const noSelectionStyle = 'transform: translateX(0px) scaleX(0) scaleY(0)';
1✔
35

1✔
36
/**
1✔
37
 * @element sp-top-nav
1✔
38
 *
1✔
39
 * @slot - Nav Items to display as a group
1✔
40
 * @attr {Boolean} compact - The collection of tabs take up less space
1✔
41
 */
1✔
42

1✔
43
export class TopNav extends SizedMixin(SpectrumElement) {
1✔
44
    public static override get styles(): CSSResultArray {
×
45
        return [tabsSizes, tabStyles, ScaledIndicator.baseStyles()];
×
46
    }
×
47

×
48
    @property({ reflect: true })
×
49
    public override dir!: 'ltr' | 'rtl';
×
50

×
51
    @property({ type: String })
×
52
    public label = '';
×
53

×
54
    /**
×
55
     * A space separated list of part of the URL to ignore when matching
×
56
     * for the "selected" Top Nav Item. Currently supported values are
×
57
     * `hash` and `search`, which will remove the `#hash` and
×
58
     * `?search=value` respectively.
×
59
     */
×
60
    @property({ attribute: 'ignore-url-parts' })
×
61
    public ignoreURLParts = '';
×
62

×
63
    @property()
×
64
    public selectionIndicatorStyle = noSelectionStyle;
×
65

×
66
    @property({ attribute: false })
×
67
    public shouldAnimate = false;
×
68

×
69
    /**
×
70
     * The Top Nav is displayed without a border.
×
71
     */
×
72
    @property({ type: Boolean, reflect: true })
×
73
    public quiet = false;
×
74

×
75
    private onClick = (event: Event): void => {
×
76
        const target = event.target as TopNavItem;
×
77
        this.shouldAnimate = true;
×
78
        this.selectTarget(target);
×
79
    };
×
80

×
81
    @property({ reflect: true })
×
82
    public set selected(value: string | undefined) {
×
83
        const oldValue = this.selected;
×
84

×
85
        if (value === oldValue) {
×
86
            return;
×
87
        }
×
88
        this.updateCheckedState(value);
×
89

×
90
        this._selected = value;
×
91
        this.requestUpdate('selected', oldValue);
×
92
    }
×
93

×
94
    public get selected(): string | undefined {
×
95
        return this._selected;
×
96
    }
×
97

×
98
    private _selected!: string | undefined;
×
99

×
100
    @query('slot')
×
101
    private slotEl!: HTMLSlotElement;
×
102

×
103
    protected get items(): TopNavItem[] {
×
104
        return this._items;
×
105
    }
×
106

×
107
    protected set items(items: TopNavItem[]) {
×
108
        if (items === this.items) return;
×
109
        this._items.forEach((item) => {
×
110
            this.resizeController.unobserve(item);
×
111
        });
×
112
        items.forEach((item) => {
×
113
            this.resizeController.observe(item);
×
114
        });
×
115
        this._items = items;
×
116
    }
×
117

×
118
    private _items: TopNavItem[] = [];
×
119

×
120
    protected resizeController = new ResizeController(this, {
×
121
        callback: () => {
×
122
            this.updateSelectionIndicator();
×
123
        },
×
124
    });
×
125

×
126
    private manageItems(): void {
×
127
        this.items = this.slotEl
×
128
            .assignedElements({ flatten: true })
×
129
            .filter((el) => el.localName === 'sp-top-nav-item') as TopNavItem[];
×
130
        let { href } = window.location;
×
131
        const ignoredURLParts = this.ignoreURLParts.split(' ');
×
132
        if (ignoredURLParts.includes('hash')) {
×
133
            href = href.replace(window.location.hash, '');
×
134
        }
×
135
        if (ignoredURLParts.includes('search')) {
×
136
            href = href.replace(window.location.search, '');
×
137
        }
×
138
        const selectedChild = this.items.find((item) => item.value === href);
×
139
        if (selectedChild) {
×
140
            this.selectTarget(selectedChild);
×
141
        } else {
×
142
            this.selected = '';
×
143
        }
×
144
    }
×
145

×
146
    protected override render(): TemplateResult {
×
147
        return html`
×
148
            <div @click=${this.onClick} id="list">
×
149
                <slot @slotchange=${this.onSlotChange}></slot>
×
150
                <div
×
151
                    id="selection-indicator"
×
152
                    class=${ifDefined(
×
153
                        this.shouldAnimate ? undefined : 'first-position'
×
154
                    )}
×
155
                    style=${this.selectionIndicatorStyle}
×
156
                ></div>
×
157
            </div>
×
158
        `;
×
159
    }
×
160

×
161
    protected override firstUpdated(changes: PropertyValues): void {
×
162
        super.firstUpdated(changes);
×
163
        this.setAttribute('direction', 'horizontal');
×
164
        this.setAttribute('role', 'navigation');
×
165
    }
×
166

×
167
    protected override updated(changes: PropertyValues): void {
×
168
        super.updated(changes);
×
169
        if (changes.has('dir')) {
×
170
            this.updateSelectionIndicator();
×
171
        }
×
172
        if (
×
173
            !this.shouldAnimate &&
×
174
            typeof changes.get('shouldAnimate') !== 'undefined'
×
175
        ) {
×
176
            this.shouldAnimate = true;
×
177
        }
×
178
        if (
×
179
            changes.has('label') &&
×
180
            (this.label || typeof changes.get('label') !== 'undefined')
×
181
        ) {
×
182
            if (this.label.length) {
×
183
                this.setAttribute('aria-label', this.label);
×
184
            } else {
×
185
                this.removeAttribute('aria-label');
×
186
            }
×
187
        }
×
188
    }
×
189

×
190
    private selectTarget(target: TopNavItem): void {
×
191
        const { value } = target;
×
192
        if (value) {
×
193
            this.selected = value;
×
194
        }
×
195
    }
×
196

×
197
    protected onSlotChange(): void {
×
198
        this.manageItems();
×
199
    }
×
200

×
201
    protected updateCheckedState(value: string | undefined): void {
×
202
        this.items.forEach((item) => {
×
203
            item.selected = false;
×
204
        });
×
205

×
206
        requestAnimationFrame(() => {
×
207
            if (value && value.length) {
×
208
                const currentItem = this.items.find(
×
209
                    (item) =>
×
210
                        item.value === value ||
×
211
                        item.value === window.location.href
×
212
                );
×
213

×
214
                if (currentItem) {
×
215
                    currentItem.selected = true;
×
216
                } else {
×
217
                    this.selected = '';
×
218
                }
×
219
            }
×
220

×
221
            this.updateSelectionIndicator();
×
222
        });
×
223
    }
×
224

×
225
    private updateSelectionIndicator = async (): Promise<void> => {
×
226
        const selectedItem = this.items.find(
×
227
            (item) =>
×
228
                item.value === this.selected ||
×
229
                item.value === window.location.href
×
230
        );
×
231
        if (!selectedItem) {
×
232
            this.selectionIndicatorStyle = noSelectionStyle;
×
233
            return;
×
234
        }
×
235
        await Promise.all([
×
236
            selectedItem.updateComplete,
×
237
            document.fonts ? document.fonts.ready : Promise.resolve(),
×
238
        ]);
×
239
        const { width } = selectedItem.getBoundingClientRect();
×
240
        this.selectionIndicatorStyle = ScaledIndicator.transformX(
×
241
            selectedItem.offsetLeft,
×
242
            width
×
243
        );
×
244
    };
×
245

1✔
246
    public override connectedCallback(): void {
1✔
247
        super.connectedCallback();
×
248
        window.addEventListener('resize', this.updateSelectionIndicator);
×
249
        if ('fonts' in document) {
×
250
            document.fonts.addEventListener(
×
251
                'loadingdone',
×
252
                this.updateSelectionIndicator
×
253
            );
×
254
        }
×
255
    }
×
256

1✔
257
    public override disconnectedCallback(): void {
1✔
258
        window.removeEventListener('resize', this.updateSelectionIndicator);
×
259
        if ('fonts' in document) {
×
260
            (
×
261
                document as unknown as {
×
262
                    fonts: {
×
263
                        removeEventListener: (
×
264
                            name: string,
×
265
                            callback: () => void
×
266
                        ) => void;
×
267
                    };
×
268
                }
×
269
            ).fonts.removeEventListener(
×
270
                'loadingdone',
×
271
                this.updateSelectionIndicator
×
272
            );
×
273
        }
×
274
        super.disconnectedCallback();
×
275
    }
×
276
}
1✔
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