@internetarchive/ia-topnav 1.4.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.prettierignore +1 -1
  2. package/LICENSE +661 -661
  3. package/README.md +147 -147
  4. package/demo/index.html +28 -28
  5. package/dist/src/data/menus.js +0 -2
  6. package/dist/src/data/menus.js.map +1 -1
  7. package/dist/src/ia-topnav.d.ts +7 -10
  8. package/dist/src/ia-topnav.js +95 -133
  9. package/dist/src/ia-topnav.js.map +1 -1
  10. package/dist/src/login-button.js +17 -17
  11. package/dist/src/login-button.js.map +1 -1
  12. package/dist/src/models.d.ts +0 -4
  13. package/dist/src/models.js.map +1 -1
  14. package/dist/src/primary-nav.d.ts +5 -4
  15. package/dist/src/primary-nav.js +98 -119
  16. package/dist/src/primary-nav.js.map +1 -1
  17. package/dist/src/styles/ia-topnav.js +82 -87
  18. package/dist/src/styles/ia-topnav.js.map +1 -1
  19. package/dist/src/styles/primary-nav.js +353 -308
  20. package/dist/src/styles/primary-nav.js.map +1 -1
  21. package/dist/test/ia-topnav.test.js +27 -69
  22. package/dist/test/ia-topnav.test.js.map +1 -1
  23. package/dist/test/primary-nav.test.js +38 -9
  24. package/dist/test/primary-nav.test.js.map +1 -1
  25. package/eslint.config.mjs +53 -53
  26. package/package.json +72 -72
  27. package/prettier.config.js +9 -9
  28. package/src/data/menus.ts +650 -652
  29. package/src/ia-topnav.ts +318 -366
  30. package/src/login-button.ts +78 -78
  31. package/src/models.ts +58 -63
  32. package/src/primary-nav.ts +277 -296
  33. package/src/styles/ia-topnav.ts +85 -90
  34. package/src/styles/primary-nav.ts +356 -311
  35. package/ssl/server.key +28 -28
  36. package/test/ia-topnav.test.ts +282 -343
  37. package/test/primary-nav.test.ts +135 -94
  38. package/tsconfig.json +31 -31
  39. package/web-dev-server.config.mjs +32 -32
  40. package/web-test-runner.config.mjs +41 -41
  41. package/dist/src/lib/location-handler.d.ts +0 -1
  42. package/dist/src/lib/location-handler.js +0 -5
  43. package/dist/src/lib/location-handler.js.map +0 -1
  44. package/dist/src/nav-search.d.ts +0 -19
  45. package/dist/src/nav-search.js +0 -127
  46. package/dist/src/nav-search.js.map +0 -1
  47. package/dist/src/search-menu.d.ts +0 -20
  48. package/dist/src/search-menu.js +0 -162
  49. package/dist/src/search-menu.js.map +0 -1
  50. package/dist/src/styles/nav-search.d.ts +0 -2
  51. package/dist/src/styles/nav-search.js +0 -136
  52. package/dist/src/styles/nav-search.js.map +0 -1
  53. package/dist/src/styles/search-menu.d.ts +0 -2
  54. package/dist/src/styles/search-menu.js +0 -118
  55. package/dist/src/styles/search-menu.js.map +0 -1
  56. package/dist/test/nav-search.test.d.ts +0 -1
  57. package/dist/test/nav-search.test.js +0 -47
  58. package/dist/test/nav-search.test.js.map +0 -1
  59. package/dist/test/search-menu.test.d.ts +0 -1
  60. package/dist/test/search-menu.test.js +0 -42
  61. package/dist/test/search-menu.test.js.map +0 -1
  62. package/src/lib/location-handler.ts +0 -5
  63. package/src/nav-search.ts +0 -117
  64. package/src/search-menu.ts +0 -156
  65. package/src/styles/nav-search.ts +0 -136
  66. package/src/styles/search-menu.ts +0 -118
  67. package/test/nav-search.test.ts +0 -70
  68. package/test/search-menu.test.ts +0 -58
@@ -1,296 +1,277 @@
1
- import { html, nothing, PropertyValues } from 'lit';
2
- import TrackedElement from './tracked-element';
3
- import icons from './assets/img/icons';
4
- import './assets/img/hamburger';
5
- import './login-button';
6
- import './nav-search';
7
- import './media-menu';
8
- import logoWordmarkStacked from './assets/img/wordmark-stacked';
9
- import primaryNavCSS from './styles/primary-nav';
10
- import locationHandler from './lib/location-handler';
11
- import formatUrl from './lib/format-url';
12
- import { customElement, property } from 'lit/decorators.js';
13
- import { IATopNavConfig, IATopNavSecondIdentitySlotMode } from './models';
14
- import { defaultTopNavConfig } from './data/menus';
15
-
16
- @customElement('primary-nav')
17
- export class PrimaryNav extends TrackedElement {
18
- @property({ type: String }) mediaBaseHost = 'https://archive.org';
19
- @property({ type: String }) baseHost = '';
20
- @property({ type: Boolean }) hideSearch = false;
21
- @property({ type: Object }) config: IATopNavConfig = defaultTopNavConfig;
22
- @property({ type: String }) openMenu = '';
23
- @property({ type: String }) screenName = '';
24
- @property({ type: String }) searchIn = '';
25
- @property({ type: String }) searchQuery = '';
26
- @property({ type: String })
27
- secondIdentitySlotMode: IATopNavSecondIdentitySlotMode = '';
28
- @property({ type: String }) selectedMenuOption = '';
29
- @property({ type: Boolean }) signedOutMenuOpen = false;
30
- @property({ type: Boolean }) userMenuOpen = false;
31
- @property({ type: Boolean }) mediaMenuAnimate = false;
32
- @property({ type: String }) username = '';
33
- @property({ type: String }) userProfileImagePath = '';
34
- @property({ type: Object }) currentTab:
35
- | { mediatype: string; moveTo: string }
36
- | undefined;
37
- signedOutMenuToggled: unknown;
38
-
39
- static get styles() {
40
- return primaryNavCSS;
41
- }
42
-
43
- toggleMediaMenu(e: Event) {
44
- this.trackClick(e);
45
- this.dispatchEvent(
46
- new CustomEvent('menuToggled', {
47
- detail: {
48
- menuName: 'media',
49
- },
50
- }),
51
- );
52
- }
53
-
54
- toggleSearchMenu(e: Event) {
55
- this.trackClick(e);
56
- this.dispatchEvent(
57
- new CustomEvent('menuToggled', {
58
- detail: {
59
- menuName: 'search',
60
- },
61
- }),
62
- );
63
- }
64
-
65
- toggleUserMenu(e: Event) {
66
- this.trackClick(e);
67
- this.dispatchEvent(
68
- new CustomEvent('menuToggled', {
69
- detail: {
70
- menuName: 'user',
71
- },
72
- }),
73
- );
74
- }
75
-
76
- updated(props: PropertyValues) {
77
- if (props.has('currentTab')) {
78
- // early return
79
- if (!this.currentTab || Object.keys(this.currentTab).length === 0)
80
- return nothing;
81
-
82
- const isUserMenuTab =
83
- this.currentTab && this.currentTab.mediatype === 'usermenu';
84
- if (isUserMenuTab) {
85
- const mediaButtons = Array.from(
86
- this.shadowRoot
87
- ?.querySelector('media-menu')
88
- ?.shadowRoot?.querySelectorAll('media-button') ?? [],
89
- );
90
- const lastMediaButton = mediaButtons.filter((element) => {
91
- return element.shadowRoot
92
- ?.querySelector('a')
93
- ?.classList.contains('images');
94
- });
95
-
96
- let nextElement;
97
- if (this.username) {
98
- nextElement = this.shadowRoot?.querySelector('a.upload');
99
- } else {
100
- nextElement = this.shadowRoot
101
- ?.querySelector('login-button')
102
- ?.shadowRoot?.querySelector('span a');
103
- }
104
-
105
- const menuItemElement =
106
- lastMediaButton[0]?.shadowRoot?.querySelector('a.menu-item');
107
-
108
- const focusElement =
109
- this.currentTab.moveTo === 'next' ? nextElement : menuItemElement;
110
-
111
- if (focusElement) {
112
- (focusElement as HTMLElement).focus();
113
- }
114
- } else if (this.currentTab.moveTo === 'next') {
115
- if (this.shadowRoot?.querySelector('.user-menu')) {
116
- (this.shadowRoot?.querySelector('.user-menu') as HTMLElement).focus();
117
- } else {
118
- (
119
- this.shadowRoot
120
- ?.querySelector('login-button')
121
- ?.shadowRoot?.querySelectorAll('span a')[0] as HTMLElement
122
- )?.focus();
123
- }
124
- }
125
- }
126
- }
127
-
128
- get userIcon() {
129
- const userMenuClass = this.openMenu === 'user' ? 'active' : '';
130
- const userMenuToolTip =
131
- this.openMenu === 'user' ? 'Close user menu' : 'Expand user menu';
132
-
133
- return html`
134
- <button
135
- class="user-menu ${userMenuClass}"
136
- title="${userMenuToolTip}"
137
- @click="${this.toggleUserMenu}"
138
- data-event-click-tracking="${this.config?.eventCategory}|NavUserMenu"
139
- >
140
- <img
141
- src="${this.mediaBaseHost}${this.userProfileImagePath}"
142
- alt="Profile picture for ${this.screenName}"
143
- />
144
- <span class="screen-name" dir="auto">${this.screenName}</span>
145
- </button>
146
- `;
147
- }
148
-
149
- get loginIcon() {
150
- return html`
151
- <login-button
152
- .baseHost=${this.baseHost}
153
- .config=${this.config}
154
- .dropdownOpen=${this.signedOutMenuOpen}
155
- .openMenu=${this.openMenu}
156
- @signedOutMenuToggled=${this.signedOutMenuToggled}
157
- ></login-button>
158
- `;
159
- }
160
-
161
- get searchMenuOpen() {
162
- return this.openMenu === 'search';
163
- }
164
-
165
- get allowSecondaryIcon() {
166
- return this.secondIdentitySlotMode === 'allow';
167
- }
168
-
169
- get searchMenu() {
170
- if (this.hideSearch) return nothing;
171
-
172
- return html`
173
- <button
174
- class="search-trigger"
175
- @click="${this.toggleSearchMenu}"
176
- data-event-click-tracking="${this.config?.eventCategory}|NavSearchOpen"
177
- >
178
- ${icons.search}
179
- </button>
180
- <nav-search
181
- .baseHost=${this.baseHost}
182
- .config=${this.config}
183
- .locationHandler=${locationHandler}
184
- .open=${this.searchMenuOpen}
185
- .openMenu=${this.openMenu}
186
- .searchIn=${this.searchIn}
187
- .searchQuery=${this.searchQuery}
188
- @blur=${this.emitNavSearchBlurEvent}
189
- ></nav-search>
190
- `;
191
- }
192
-
193
- private emitNavSearchBlurEvent(e: FocusEvent) {
194
- const relatedTarget = e.relatedTarget as HTMLElement;
195
- const isUploadButton = relatedTarget?.classList.contains('upload');
196
-
197
- if (isUploadButton) {
198
- (this.shadowRoot?.querySelector('a.upload') as HTMLElement).focus();
199
- }
200
-
201
- this.dispatchEvent(
202
- new CustomEvent('navSearchBlur', {
203
- detail: {
204
- isUploadButton: !!isUploadButton,
205
- },
206
- bubbles: true,
207
- composed: true,
208
- }),
209
- );
210
- }
211
-
212
- get mobileDonateHeart() {
213
- return html`
214
- <a
215
- class="mobile-donate-link"
216
- .href=${formatUrl(
217
- '/donate/?origin=iawww-mbhrt' as string & Location,
218
- this.baseHost,
219
- )}
220
- >
221
- ${icons.donateUnpadded}
222
- <span class="sr-only">"Donate to the archive"</span>
223
- </a>
224
- `;
225
- }
226
-
227
- get uploadButtonTemplate() {
228
- return html` <a
229
- .href="${formatUrl('/upload' as string & Location, this.baseHost)}"
230
- class="upload"
231
- @focus=${this.toggleMediaMenu}
232
- >
233
- ${icons.upload}
234
- <span>Upload</span>
235
- </a>`;
236
- }
237
-
238
- get userStateTemplate() {
239
- return html`<div class="user-info">
240
- ${this.username ? this.userIcon : this.loginIcon}
241
- </div>`;
242
- }
243
-
244
- get secondLogoSlot() {
245
- return this.allowSecondaryIcon
246
- ? html`
247
- <slot name="opt-sec-logo"></slot>
248
- <slot name="opt-sec-logo-mobile"></slot>
249
- `
250
- : nothing;
251
- }
252
-
253
- get secondLogoClass() {
254
- return this.allowSecondaryIcon ? 'second-logo' : '';
255
- }
256
-
257
- render() {
258
- // const mediaMenuTabIndex = this.openMenu === 'media' ? '' : '-1';
259
- return html`
260
- <nav class=${this.hideSearch ? 'hide-search' : ''}>
261
- <button
262
- class="hamburger"
263
- @click="${this.toggleMediaMenu}"
264
- data-event-click-tracking="${this.config?.eventCategory}|NavHamburger"
265
- title="Open main menu"
266
- >
267
- <icon-hamburger ?active=${this.openMenu === 'media'}></icon-hamburger>
268
- </button>
269
-
270
- <div class=${`branding ${this.secondLogoClass}`}>
271
- <a
272
- .href=${formatUrl('/' as string & Location, this.baseHost)}
273
- @click=${this.trackClick}
274
- data-event-click-tracking="${this.config?.eventCategory}|NavHome"
275
- title="Go home"
276
- class="link-home"
277
- >${icons.iaLogo}${logoWordmarkStacked}</a
278
- >
279
- ${this.secondLogoSlot}
280
- </div>
281
- <media-menu
282
- .baseHost=${this.baseHost}
283
- .config=${this.config}
284
- ?mediaMenuAnimate=${this.mediaMenuAnimate}
285
- .selectedMenuOption=${this.selectedMenuOption}
286
- .openMenu=${this.openMenu}
287
- .currentTab=${this.currentTab}
288
- ></media-menu>
289
- <div class="right-side-section">
290
- ${this.mobileDonateHeart} ${this.userStateTemplate}
291
- ${this.uploadButtonTemplate} ${this.searchMenu}
292
- </div>
293
- </nav>
294
- `;
295
- }
296
- }
1
+ import { html, nothing, PropertyValues } from 'lit';
2
+ import TrackedElement from './tracked-element';
3
+ import icons from './assets/img/icons';
4
+ import './assets/img/hamburger';
5
+ import './login-button';
6
+ import './media-menu';
7
+ import logoWordmarkStacked from './assets/img/wordmark-stacked';
8
+ import primaryNavCSS from './styles/primary-nav';
9
+ import formatUrl from './lib/format-url';
10
+ import { customElement, property } from 'lit/decorators.js';
11
+ import { IATopNavConfig, IATopNavSecondIdentitySlotMode } from './models';
12
+ import { defaultTopNavConfig } from './data/menus';
13
+
14
+ @customElement('primary-nav')
15
+ export class PrimaryNav extends TrackedElement {
16
+ @property({ type: String }) mediaBaseHost = 'https://archive.org';
17
+ @property({ type: String }) baseHost = '';
18
+ @property({ type: Boolean }) hideSearch = false;
19
+ @property({ type: Object }) config: IATopNavConfig = defaultTopNavConfig;
20
+ @property({ type: String }) openMenu = '';
21
+ @property({ type: String }) screenName = '';
22
+ @property({ type: String })
23
+ secondIdentitySlotMode: IATopNavSecondIdentitySlotMode = '';
24
+ @property({ type: String }) selectedMenuOption = '';
25
+ @property({ type: Boolean }) signedOutMenuOpen = false;
26
+ @property({ type: Boolean }) userMenuOpen = false;
27
+ @property({ type: Boolean }) mediaMenuAnimate = false;
28
+ @property({ type: String }) username = '';
29
+ @property({ type: String }) userProfileImagePath = '';
30
+ @property({ type: Object }) currentTab:
31
+ | { mediatype: string; moveTo: string }
32
+ | undefined;
33
+ signedOutMenuToggled: unknown;
34
+
35
+ static get styles() {
36
+ return primaryNavCSS;
37
+ }
38
+
39
+ toggleMediaMenu(e: Event) {
40
+ this.trackClick(e);
41
+ this.dispatchEvent(
42
+ new CustomEvent('menuToggled', {
43
+ detail: {
44
+ menuName: 'media',
45
+ },
46
+ }),
47
+ );
48
+ }
49
+
50
+ toggleSearchMenu(e: Event) {
51
+ this.trackClick(e);
52
+ this.dispatchEvent(
53
+ new CustomEvent('menuToggled', {
54
+ detail: {
55
+ menuName: 'search',
56
+ },
57
+ }),
58
+ );
59
+ }
60
+
61
+ toggleUserMenu(e: Event) {
62
+ this.trackClick(e);
63
+ this.dispatchEvent(
64
+ new CustomEvent('menuToggled', {
65
+ detail: {
66
+ menuName: 'user',
67
+ },
68
+ }),
69
+ );
70
+ }
71
+
72
+ updated(props: PropertyValues) {
73
+ if (props.has('currentTab')) {
74
+ // early return
75
+ if (!this.currentTab || Object.keys(this.currentTab).length === 0)
76
+ return nothing;
77
+
78
+ const isUserMenuTab =
79
+ this.currentTab && this.currentTab.mediatype === 'usermenu';
80
+ if (isUserMenuTab) {
81
+ const mediaButtons = Array.from(
82
+ this.shadowRoot
83
+ ?.querySelector('media-menu')
84
+ ?.shadowRoot?.querySelectorAll('media-button') ?? [],
85
+ );
86
+ const lastMediaButton = mediaButtons.filter((element) => {
87
+ return element.shadowRoot
88
+ ?.querySelector('a')
89
+ ?.classList.contains('images');
90
+ });
91
+
92
+ let nextElement;
93
+ if (this.username) {
94
+ nextElement = this.shadowRoot?.querySelector('a.upload');
95
+ } else {
96
+ nextElement = this.shadowRoot
97
+ ?.querySelector('login-button')
98
+ ?.shadowRoot?.querySelector('span a');
99
+ }
100
+
101
+ const menuItemElement =
102
+ lastMediaButton[0]?.shadowRoot?.querySelector('a.menu-item');
103
+
104
+ const focusElement =
105
+ this.currentTab.moveTo === 'next' ? nextElement : menuItemElement;
106
+
107
+ if (focusElement) {
108
+ (focusElement as HTMLElement).focus();
109
+ }
110
+ } else if (this.currentTab.moveTo === 'next') {
111
+ if (this.shadowRoot?.querySelector('.user-menu')) {
112
+ (this.shadowRoot?.querySelector('.user-menu') as HTMLElement).focus();
113
+ } else {
114
+ (
115
+ this.shadowRoot
116
+ ?.querySelector('login-button')
117
+ ?.shadowRoot?.querySelectorAll('span a')[0] as HTMLElement
118
+ )?.focus();
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ get userIcon() {
125
+ const userMenuClass = this.openMenu === 'user' ? 'active' : '';
126
+ const userMenuToolTip =
127
+ this.openMenu === 'user' ? 'Close user menu' : 'Expand user menu';
128
+
129
+ return html`
130
+ <button
131
+ class="user-menu ${userMenuClass}"
132
+ title="${userMenuToolTip}"
133
+ @click="${this.toggleUserMenu}"
134
+ data-event-click-tracking="${this.config?.eventCategory}|NavUserMenu"
135
+ >
136
+ <img
137
+ src="${this.mediaBaseHost}${this.userProfileImagePath}"
138
+ alt="Profile picture for ${this.screenName}"
139
+ />
140
+ <span class="screen-name" dir="auto">${this.screenName}</span>
141
+ </button>
142
+ `;
143
+ }
144
+
145
+ get loginIcon() {
146
+ return html`
147
+ <login-button
148
+ .baseHost=${this.baseHost}
149
+ .config=${this.config}
150
+ .dropdownOpen=${this.signedOutMenuOpen}
151
+ .openMenu=${this.openMenu}
152
+ @signedOutMenuToggled=${this.signedOutMenuToggled}
153
+ ></login-button>
154
+ `;
155
+ }
156
+
157
+ get searchMenuOpen() {
158
+ return this.openMenu === 'search';
159
+ }
160
+
161
+ get allowSecondaryIcon() {
162
+ return this.secondIdentitySlotMode === 'allow';
163
+ }
164
+
165
+ /**
166
+ * The search slot container, rendered between media-menu and
167
+ * right-side-section so it sits left of the Upload button on desktop.
168
+ */
169
+ get searchSlotContainer() {
170
+ if (this.hideSearch) return nothing;
171
+ return html`
172
+ <div class="search-container ${this.searchMenuOpen ? 'open' : ''}">
173
+ <slot name="search"></slot>
174
+ </div>
175
+ `;
176
+ }
177
+
178
+ get searchMenu() {
179
+ if (this.hideSearch) return nothing;
180
+
181
+ return html`
182
+ <button
183
+ class="search-trigger"
184
+ @click="${this.toggleSearchMenu}"
185
+ data-event-click-tracking="${this.config?.eventCategory}|NavSearchOpen"
186
+ >
187
+ ${icons.search}
188
+ </button>
189
+ `;
190
+ }
191
+
192
+ get mobileDonateHeart() {
193
+ return html`
194
+ <a
195
+ class="mobile-donate-link"
196
+ .href=${formatUrl(
197
+ '/donate/?origin=iawww-mbhrt' as string & Location,
198
+ this.baseHost,
199
+ )}
200
+ >
201
+ ${icons.donateUnpadded}
202
+ <span class="sr-only">"Donate to the archive"</span>
203
+ </a>
204
+ `;
205
+ }
206
+
207
+ get uploadButtonTemplate() {
208
+ return html` <a
209
+ .href="${formatUrl('/upload' as string & Location, this.baseHost)}"
210
+ class="upload"
211
+ @focus=${this.toggleMediaMenu}
212
+ >
213
+ ${icons.upload}
214
+ <span>Upload</span>
215
+ </a>`;
216
+ }
217
+
218
+ get userStateTemplate() {
219
+ return html`<div class="user-info">
220
+ ${this.username ? this.userIcon : this.loginIcon}
221
+ </div>`;
222
+ }
223
+
224
+ get secondLogoSlot() {
225
+ return this.allowSecondaryIcon
226
+ ? html`
227
+ <slot name="opt-sec-logo"></slot>
228
+ <slot name="opt-sec-logo-mobile"></slot>
229
+ `
230
+ : nothing;
231
+ }
232
+
233
+ get secondLogoClass() {
234
+ return this.allowSecondaryIcon ? 'second-logo' : '';
235
+ }
236
+
237
+ render() {
238
+ // const mediaMenuTabIndex = this.openMenu === 'media' ? '' : '-1';
239
+ return html`
240
+ <nav class=${this.hideSearch ? 'hide-search' : ''}>
241
+ <button
242
+ class="hamburger"
243
+ @click="${this.toggleMediaMenu}"
244
+ data-event-click-tracking="${this.config?.eventCategory}|NavHamburger"
245
+ title="Open main menu"
246
+ >
247
+ <icon-hamburger ?active=${this.openMenu === 'media'}></icon-hamburger>
248
+ </button>
249
+
250
+ <div class=${`branding ${this.secondLogoClass}`}>
251
+ <a
252
+ .href=${formatUrl('/' as string & Location, this.baseHost)}
253
+ @click=${this.trackClick}
254
+ data-event-click-tracking="${this.config?.eventCategory}|NavHome"
255
+ title="Go home"
256
+ class="link-home"
257
+ >${icons.iaLogo}${logoWordmarkStacked}</a
258
+ >
259
+ ${this.secondLogoSlot}
260
+ </div>
261
+ <media-menu
262
+ .baseHost=${this.baseHost}
263
+ .config=${this.config}
264
+ ?mediaMenuAnimate=${this.mediaMenuAnimate}
265
+ .selectedMenuOption=${this.selectedMenuOption}
266
+ .openMenu=${this.openMenu}
267
+ .currentTab=${this.currentTab}
268
+ ></media-menu>
269
+ ${this.searchSlotContainer}
270
+ <div class="right-side-section">
271
+ ${this.mobileDonateHeart} ${this.userStateTemplate}
272
+ ${this.uploadButtonTemplate} ${this.searchMenu}
273
+ </div>
274
+ </nav>
275
+ `;
276
+ }
277
+ }