@internetarchive/ia-topnav 2.0.0 → 2.0.1

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 (52) hide show
  1. package/dist/src/data/menus.js.map +1 -1
  2. package/dist/src/dropdown-menu.d.ts +3 -4
  3. package/dist/src/dropdown-menu.js +33 -40
  4. package/dist/src/dropdown-menu.js.map +1 -1
  5. package/dist/src/ia-topnav.d.ts +3 -0
  6. package/dist/src/ia-topnav.js +13 -3
  7. package/dist/src/ia-topnav.js.map +1 -1
  8. package/dist/src/login-button.d.ts +3 -0
  9. package/dist/src/login-button.js +11 -1
  10. package/dist/src/login-button.js.map +1 -1
  11. package/dist/src/models.js.map +1 -1
  12. package/dist/src/primary-nav.d.ts +4 -0
  13. package/dist/src/primary-nav.js +20 -1
  14. package/dist/src/primary-nav.js.map +1 -1
  15. package/dist/src/signed-out-dropdown.d.ts +2 -1
  16. package/dist/src/signed-out-dropdown.js +1 -2
  17. package/dist/src/signed-out-dropdown.js.map +1 -1
  18. package/dist/src/styles/dropdown-menu.js +169 -168
  19. package/dist/src/styles/dropdown-menu.js.map +1 -1
  20. package/dist/src/styles/ia-topnav.js +82 -82
  21. package/dist/src/styles/ia-topnav.js.map +1 -1
  22. package/dist/src/styles/primary-nav.js +353 -353
  23. package/dist/src/styles/primary-nav.js.map +1 -1
  24. package/dist/src/user-menu.d.ts +2 -2
  25. package/dist/src/user-menu.js +14 -15
  26. package/dist/src/user-menu.js.map +1 -1
  27. package/dist/test/ia-topnav.test.js +9 -9
  28. package/dist/test/ia-topnav.test.js.map +1 -1
  29. package/dist/test/primary-nav.test.js +7 -7
  30. package/dist/test/primary-nav.test.js.map +1 -1
  31. package/package.json +1 -1
  32. package/src/data/menus.ts +650 -650
  33. package/src/dropdown-menu.ts +132 -132
  34. package/src/ia-topnav.ts +17 -3
  35. package/src/login-button.ts +12 -1
  36. package/src/models.ts +58 -58
  37. package/src/primary-nav.ts +24 -1
  38. package/src/signed-out-dropdown.ts +11 -11
  39. package/src/styles/dropdown-menu.ts +172 -171
  40. package/src/styles/ia-topnav.ts +85 -85
  41. package/src/styles/primary-nav.ts +356 -356
  42. package/src/user-menu.ts +31 -32
  43. package/test/ia-topnav.test.ts +282 -282
  44. package/test/primary-nav.test.ts +135 -135
  45. package/dist/src/styles/signed-out-dropdown.d.ts +0 -2
  46. package/dist/src/styles/signed-out-dropdown.js +0 -31
  47. package/dist/src/styles/signed-out-dropdown.js.map +0 -1
  48. package/dist/src/styles/user-menu.d.ts +0 -2
  49. package/dist/src/styles/user-menu.js +0 -31
  50. package/dist/src/styles/user-menu.js.map +0 -1
  51. package/src/styles/signed-out-dropdown.ts +0 -31
  52. package/src/styles/user-menu.ts +0 -31
@@ -1,132 +1,132 @@
1
- import { CSSResult, html, nothing, PropertyValues, TemplateResult } from 'lit';
2
- import { property } from 'lit/decorators.js';
3
-
4
- import icons from './assets/img/icons';
5
- import { defaultTopNavConfig } from './data/menus';
6
- import formatUrl from './lib/format-url';
7
- import { makeBooleanString } from './lib/make-boolean-string';
8
- import { IATopNavConfig, IATopNavLink } from './models';
9
- import dropdownMenuCSS from './styles/dropdown-menu';
10
- import TrackedElement from './tracked-element';
11
- import { ifDefined } from 'lit/directives/if-defined.js';
12
- import KeyboardNavigation from './lib/keyboard-navigation';
13
-
14
- export default class DropdownMenu extends TrackedElement {
15
- @property({ type: String }) baseHost = '';
16
- @property({ type: Object }) config: IATopNavConfig = defaultTopNavConfig;
17
- @property({ type: Boolean }) hideSearch = false;
18
- @property({ type: Array }) menuItems: IATopNavLink[] | IATopNavLink[][] = [];
19
- @property({ type: Boolean }) animated = false;
20
- @property({ type: Boolean }) open = false;
21
-
22
- private previousKeydownListener?: // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
- (this: HTMLElement, ev: KeyboardEvent) => any;
24
-
25
- static get styles(): CSSResult[] {
26
- return [dropdownMenuCSS];
27
- }
28
-
29
- updated(props: PropertyValues) {
30
- if (props.has('open') && this.open) {
31
- const container = this.shadowRoot?.querySelector(
32
- '.nav-container',
33
- ) as HTMLElement;
34
-
35
- if (container) {
36
- const keyboardNavigation = new KeyboardNavigation(
37
- container,
38
- 'usermenu',
39
- );
40
- this.addEventListener('keydown', keyboardNavigation.handleKeyDown);
41
- if (this.previousKeydownListener) {
42
- this.removeEventListener('keydown', this.previousKeydownListener);
43
- }
44
- this.previousKeydownListener = keyboardNavigation.handleKeyDown;
45
- }
46
- }
47
- }
48
-
49
- get dropdownItems() {
50
- if (!this.menuItems) return nothing;
51
-
52
- if (!Array.isArray(this.menuItems[0])) {
53
- const submenu = this.menuItems as IATopNavLink[];
54
- return this.dropdownSection(submenu);
55
- }
56
- return this.menuItems.map((submenu, i) => {
57
- const joiner = i ? DropdownMenu.dropdownDivider : html``;
58
- if (!Array.isArray(submenu)) {
59
- return;
60
- }
61
- return [joiner, ...this.dropdownSection(submenu)];
62
- });
63
- }
64
-
65
- static get dropdownDivider() {
66
- return html`<li class="divider"></li>`;
67
- }
68
-
69
- private dropdownSection(submenu: IATopNavLink[]): TemplateResult[] {
70
- return submenu.map(
71
- (item) => html`
72
- <li>
73
- ${item.url
74
- ? this.dropdownLink(item)
75
- : DropdownMenu.dropdownText(item)}
76
- </li>
77
- `,
78
- );
79
- }
80
-
81
- dropdownLink(link: IATopNavLink): TemplateResult {
82
- const calloutText = this.config?.callouts?.[link.title];
83
- const isMobileUpload = link.class === 'mobile-upload';
84
- const isTabbable = this.open && !isMobileUpload;
85
-
86
- return html`<a
87
- href="${formatUrl(link.url, this.baseHost)}"
88
- class=${ifDefined(link.class)}
89
- tabindex="${isTabbable ? '' : '-1'}"
90
- @click=${this.trackClick}
91
- data-event-click-tracking="${this.config
92
- ?.eventCategory}|Nav${link.analyticsEvent}"
93
- aria-label=${calloutText ? `New feature: ${link.title}` : nothing}
94
- >
95
- ${isMobileUpload ? icons.uploadUnpadded : nothing} ${link.title}
96
- ${calloutText
97
- ? html`<span class="callout" aria-hidden="true">${calloutText}</span>`
98
- : nothing}
99
- </a>`;
100
- }
101
-
102
- static dropdownText(item: IATopNavLink) {
103
- return html`<span class="info-item">${item.title}</span>`;
104
- }
105
-
106
- get menuClass() {
107
- const hiddenClass = this.hideSearch ? ' search-hidden' : '';
108
- if (this.open) {
109
- return `open${hiddenClass}`;
110
- }
111
- if (this.animated) {
112
- return `closed${hiddenClass}`;
113
- }
114
- return `initial${hiddenClass}`;
115
- }
116
-
117
- render() {
118
- return html`
119
- <div class="nav-container">
120
- <nav
121
- class="${this.menuClass}"
122
- aria-hidden="${makeBooleanString(!this.open)}"
123
- aria-expanded="${makeBooleanString(this.open)}"
124
- >
125
- <ul>
126
- ${this.dropdownItems}
127
- </ul>
128
- </nav>
129
- </div>
130
- `;
131
- }
132
- }
1
+ import {
2
+ CSSResultGroup,
3
+ html,
4
+ nothing,
5
+ PropertyValues,
6
+ TemplateResult,
7
+ } from 'lit';
8
+ import { property } from 'lit/decorators.js';
9
+
10
+ import icons from './assets/img/icons';
11
+ import { defaultTopNavConfig } from './data/menus';
12
+ import formatUrl from './lib/format-url';
13
+ import { makeBooleanString } from './lib/make-boolean-string';
14
+ import { IATopNavConfig, IATopNavLink } from './models';
15
+ import dropdownMenuCSS from './styles/dropdown-menu';
16
+ import TrackedElement from './tracked-element';
17
+ import { ifDefined } from 'lit/directives/if-defined.js';
18
+ import KeyboardNavigation from './lib/keyboard-navigation';
19
+
20
+ export default class DropdownMenu extends TrackedElement {
21
+ @property({ type: String }) baseHost = '';
22
+ @property({ type: Object }) config: IATopNavConfig = defaultTopNavConfig;
23
+ @property({ type: Array }) menuItems: IATopNavLink[] | IATopNavLink[][] = [];
24
+ @property({ type: Boolean }) animated = false;
25
+ @property({ type: Boolean }) open = false;
26
+
27
+ private previousKeydownListener?: // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
+ (this: HTMLElement, ev: KeyboardEvent) => any;
29
+
30
+ static get styles(): CSSResultGroup {
31
+ return dropdownMenuCSS;
32
+ }
33
+
34
+ updated(props: PropertyValues) {
35
+ if (props.has('open') && this.open) {
36
+ const container = this.shadowRoot?.querySelector(
37
+ '.nav-container',
38
+ ) as HTMLElement;
39
+
40
+ if (container) {
41
+ const keyboardNavigation = new KeyboardNavigation(
42
+ container,
43
+ 'usermenu',
44
+ );
45
+ this.addEventListener('keydown', keyboardNavigation.handleKeyDown);
46
+ if (this.previousKeydownListener) {
47
+ this.removeEventListener('keydown', this.previousKeydownListener);
48
+ }
49
+ this.previousKeydownListener = keyboardNavigation.handleKeyDown;
50
+ }
51
+ }
52
+ }
53
+
54
+ get dropdownItems() {
55
+ if (!this.menuItems) return nothing;
56
+
57
+ if (!Array.isArray(this.menuItems[0])) {
58
+ const submenu = this.menuItems as IATopNavLink[];
59
+ return this.dropdownSection(submenu);
60
+ }
61
+ return this.menuItems.map((submenu, i) => {
62
+ const joiner = i ? DropdownMenu.dropdownDivider : html``;
63
+ if (!Array.isArray(submenu)) {
64
+ return;
65
+ }
66
+ return [joiner, ...this.dropdownSection(submenu)];
67
+ });
68
+ }
69
+
70
+ static get dropdownDivider() {
71
+ return html`<li class="divider"></li>`;
72
+ }
73
+
74
+ private dropdownSection(submenu: IATopNavLink[]): TemplateResult[] {
75
+ return submenu.map(
76
+ (item) => html`
77
+ <li>
78
+ ${item.url
79
+ ? this.dropdownLink(item)
80
+ : DropdownMenu.dropdownText(item)}
81
+ </li>
82
+ `,
83
+ );
84
+ }
85
+
86
+ dropdownLink(link: IATopNavLink): TemplateResult {
87
+ const calloutText = this.config?.callouts?.[link.title];
88
+ const isMobileUpload = link.class === 'mobile-upload';
89
+ const isTabbable = this.open && !isMobileUpload;
90
+
91
+ return html`<a
92
+ href="${formatUrl(link.url, this.baseHost)}"
93
+ class=${ifDefined(link.class)}
94
+ tabindex="${isTabbable ? '' : '-1'}"
95
+ @click=${this.trackClick}
96
+ data-event-click-tracking="${this.config
97
+ ?.eventCategory}|Nav${link.analyticsEvent}"
98
+ aria-label=${calloutText ? `New feature: ${link.title}` : nothing}
99
+ >
100
+ ${isMobileUpload ? icons.uploadUnpadded : nothing} ${link.title}
101
+ ${calloutText
102
+ ? html`<span class="callout" aria-hidden="true">${calloutText}</span>`
103
+ : nothing}
104
+ </a>`;
105
+ }
106
+
107
+ static dropdownText(item: IATopNavLink) {
108
+ return html`<span class="info-item">${item.title}</span>`;
109
+ }
110
+
111
+ get menuClass() {
112
+ if (this.open) return 'open';
113
+ if (this.animated) return 'closed';
114
+ return 'initial';
115
+ }
116
+
117
+ render() {
118
+ return html`
119
+ <div class="nav-container">
120
+ <nav
121
+ class="${this.menuClass}"
122
+ aria-hidden="${makeBooleanString(!this.open)}"
123
+ aria-expanded="${makeBooleanString(this.open)}"
124
+ >
125
+ <ul>
126
+ ${this.dropdownItems}
127
+ </ul>
128
+ </nav>
129
+ </div>
130
+ `;
131
+ }
132
+ }
package/src/ia-topnav.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { LitElement, PropertyValues, html, nothing } from 'lit';
2
- import { customElement, property, state } from 'lit/decorators.js';
2
+ import { customElement, property, query, state } from 'lit/decorators.js';
3
3
 
4
4
  import { buildTopNavMenus, defaultTopNavConfig } from './data/menus';
5
5
  import './desktop-subnav';
@@ -11,6 +11,7 @@ import {
11
11
  IATopNavSecondIdentitySlotMode,
12
12
  } from './models';
13
13
  import './primary-nav';
14
+ import type { PrimaryNav } from './primary-nav';
14
15
  import './signed-out-dropdown';
15
16
  import iaTopNavCSS from './styles/ia-topnav';
16
17
  import './user-menu';
@@ -56,6 +57,11 @@ export class IATopNav extends LitElement {
56
57
  moveTo: string;
57
58
  };
58
59
 
60
+ @query('primary-nav') private primaryNav?: PrimaryNav;
61
+ /** Only one of user-menu or signed-out-dropdown is rendered at a time. */
62
+ @query('user-menu, signed-out-dropdown')
63
+ private accountDropdown?: HTMLElement;
64
+
59
65
  @state() private menus: IATopNavMenuConfig = buildTopNavMenus();
60
66
 
61
67
  private boundHandleKeydown = this.handleDocumentKeydown.bind(this);
@@ -127,6 +133,16 @@ export class IATopNav extends LitElement {
127
133
  return;
128
134
  }
129
135
  this.closeMediaSlider();
136
+
137
+ if (this.openMenu === 'user' || this.openMenu === 'login') {
138
+ if (this.primaryNav && this.accountDropdown) {
139
+ const right = this.primaryNav.getAccountDropdownOffset();
140
+ this.accountDropdown.style.setProperty(
141
+ '--dropdownMenuRight',
142
+ `${right}px`,
143
+ );
144
+ }
145
+ }
130
146
  }
131
147
 
132
148
  openMediaSlider() {
@@ -200,7 +216,6 @@ export class IATopNav extends LitElement {
200
216
  .menuItems=${this.userMenuItems}
201
217
  ?open=${this.openMenu === 'user'}
202
218
  .username=${this.username}
203
- ?hideSearch=${this.hideSearch}
204
219
  tabindex="${this.userMenuTabIndex}"
205
220
  @menuToggled=${this.menuToggled}
206
221
  @trackClick=${this.trackClick}
@@ -216,7 +231,6 @@ export class IATopNav extends LitElement {
216
231
  .baseHost=${this.normalizedBaseHost}
217
232
  .config=${this.config}
218
233
  .open=${this.signedOutOpened}
219
- ?hideSearch=${this.hideSearch}
220
234
  tabindex="${this.signedOutTabIndex}"
221
235
  .menuItems=${this.signedOutMenuItems}
222
236
  @focusToOtherMenuItem=${(e: CustomEvent) => {
@@ -4,7 +4,7 @@ import icons from './assets/img/icons';
4
4
  import loginButtonCSS from './styles/login-button';
5
5
  import formatUrl from './lib/format-url';
6
6
  import { makeBooleanString } from './lib/make-boolean-string';
7
- import { customElement, property, state } from 'lit/decorators.js';
7
+ import { customElement, property, query, state } from 'lit/decorators.js';
8
8
  import { IATopNavConfig } from './models';
9
9
  import { defaultTopNavConfig } from './data/menus';
10
10
 
@@ -14,12 +14,23 @@ export class LoginButton extends TrackedElement {
14
14
  @property({ type: Object }) config: IATopNavConfig = defaultTopNavConfig;
15
15
  @property({ type: String }) openMenu = '';
16
16
 
17
+ @query('button.logged-out-menu') private toggleButton?: HTMLButtonElement;
18
+
17
19
  @state() private dropdownTabIndex = '';
18
20
 
19
21
  static get styles() {
20
22
  return loginButtonCSS;
21
23
  }
22
24
 
25
+ /** Distance (px) from this element's right edge to the right edge of the dropdown toggle icon. */
26
+ getDropdownToggleOffset(): number {
27
+ if (!this.toggleButton) return 0;
28
+ return (
29
+ this.getBoundingClientRect().right -
30
+ this.toggleButton.getBoundingClientRect().right
31
+ );
32
+ }
33
+
23
34
  get signupPath() {
24
35
  return formatUrl('/signup', this.baseHost);
25
36
  }
package/src/models.ts CHANGED
@@ -1,58 +1,58 @@
1
- export interface IATopNavConfig {
2
- /**
3
- * Google Analytics event category
4
- */
5
- eventCategory?: string;
6
-
7
- /**
8
- * Copy to display for number of pages archived at the top of the Wayback search form
9
- *
10
- * ie. "425 billion"
11
- */
12
- waybackPagesArchived?: string;
13
-
14
- /**
15
- * Map from dropdown item ids to any callout text that should be applied beside them
16
- */
17
- callouts?: Record<string, string>;
18
- }
19
-
20
- export interface IATopNavLink {
21
- title: string;
22
-
23
- url?: string;
24
-
25
- class?: string;
26
-
27
- icon?: string;
28
-
29
- analyticsEvent?: string;
30
-
31
- external?: boolean;
32
- }
33
-
34
- export interface IATopNavMediaMenu {
35
- heading: string;
36
- iconLinks: IATopNavLink[];
37
- featuredLinks: IATopNavLink[];
38
- links: IATopNavLink[];
39
- mobileAppsLinks: IATopNavLink[];
40
- browserExtensionsLinks: IATopNavLink[];
41
- archiveItLinks: IATopNavLink[];
42
- }
43
-
44
- export interface IATopNavMenuConfig {
45
- audio: IATopNavMediaMenu;
46
- images: IATopNavMediaMenu;
47
- more: IATopNavMediaMenu;
48
- signedOut: IATopNavLink[];
49
- software: IATopNavMediaMenu;
50
- texts: IATopNavMediaMenu;
51
- user: IATopNavLink[];
52
- userAdmin: IATopNavLink[];
53
- userAdminFlags: IATopNavLink[];
54
- video: IATopNavMediaMenu;
55
- web: IATopNavMediaMenu;
56
- }
57
-
58
- export type IATopNavSecondIdentitySlotMode = 'allow' | '';
1
+ export interface IATopNavConfig {
2
+ /**
3
+ * Google Analytics event category
4
+ */
5
+ eventCategory?: string;
6
+
7
+ /**
8
+ * Copy to display for number of pages archived at the top of the Wayback search form
9
+ *
10
+ * ie. "425 billion"
11
+ */
12
+ waybackPagesArchived?: string;
13
+
14
+ /**
15
+ * Map from dropdown item ids to any callout text that should be applied beside them
16
+ */
17
+ callouts?: Record<string, string>;
18
+ }
19
+
20
+ export interface IATopNavLink {
21
+ title: string;
22
+
23
+ url?: string;
24
+
25
+ class?: string;
26
+
27
+ icon?: string;
28
+
29
+ analyticsEvent?: string;
30
+
31
+ external?: boolean;
32
+ }
33
+
34
+ export interface IATopNavMediaMenu {
35
+ heading: string;
36
+ iconLinks: IATopNavLink[];
37
+ featuredLinks: IATopNavLink[];
38
+ links: IATopNavLink[];
39
+ mobileAppsLinks: IATopNavLink[];
40
+ browserExtensionsLinks: IATopNavLink[];
41
+ archiveItLinks: IATopNavLink[];
42
+ }
43
+
44
+ export interface IATopNavMenuConfig {
45
+ audio: IATopNavMediaMenu;
46
+ images: IATopNavMediaMenu;
47
+ more: IATopNavMediaMenu;
48
+ signedOut: IATopNavLink[];
49
+ software: IATopNavMediaMenu;
50
+ texts: IATopNavMediaMenu;
51
+ user: IATopNavLink[];
52
+ userAdmin: IATopNavLink[];
53
+ userAdminFlags: IATopNavLink[];
54
+ video: IATopNavMediaMenu;
55
+ web: IATopNavMediaMenu;
56
+ }
57
+
58
+ export type IATopNavSecondIdentitySlotMode = 'allow' | '';
@@ -3,11 +3,12 @@ import TrackedElement from './tracked-element';
3
3
  import icons from './assets/img/icons';
4
4
  import './assets/img/hamburger';
5
5
  import './login-button';
6
+ import type { LoginButton } from './login-button';
6
7
  import './media-menu';
7
8
  import logoWordmarkStacked from './assets/img/wordmark-stacked';
8
9
  import primaryNavCSS from './styles/primary-nav';
9
10
  import formatUrl from './lib/format-url';
10
- import { customElement, property } from 'lit/decorators.js';
11
+ import { customElement, property, query } from 'lit/decorators.js';
11
12
  import { IATopNavConfig, IATopNavSecondIdentitySlotMode } from './models';
12
13
  import { defaultTopNavConfig } from './data/menus';
13
14
 
@@ -32,10 +33,32 @@ export class PrimaryNav extends TrackedElement {
32
33
  | undefined;
33
34
  signedOutMenuToggled: unknown;
34
35
 
36
+ @query('button.user-menu') private userMenuButton?: HTMLButtonElement;
37
+ @query('login-button') private loginButton?: HTMLElement;
38
+
35
39
  static get styles() {
36
40
  return primaryNavCSS;
37
41
  }
38
42
 
43
+ /** Distance (px) from this element's right edge to the right edge of the account dropdown toggle. */
44
+ getAccountDropdownOffset(): number {
45
+ const hostRect = this.getBoundingClientRect();
46
+
47
+ if (this.userMenuButton) {
48
+ return hostRect.right - this.userMenuButton.getBoundingClientRect().right;
49
+ }
50
+
51
+ if (this.loginButton) {
52
+ const loginRect = this.loginButton.getBoundingClientRect();
53
+ const innerOffset = (
54
+ this.loginButton as LoginButton
55
+ ).getDropdownToggleOffset();
56
+ return hostRect.right - loginRect.right + innerOffset;
57
+ }
58
+
59
+ return 0;
60
+ }
61
+
39
62
  toggleMediaMenu(e: Event) {
40
63
  this.trackClick(e);
41
64
  this.dispatchEvent(
@@ -1,11 +1,11 @@
1
- import { customElement } from 'lit/decorators.js';
2
- import DropdownMenu from './dropdown-menu';
3
- import dropdownMenuCSS from './styles/dropdown-menu';
4
- import signedOutDropdownStyles from './styles/signed-out-dropdown';
5
-
6
- @customElement('signed-out-dropdown')
7
- export class SignedOutDropdown extends DropdownMenu {
8
- static get styles() {
9
- return [dropdownMenuCSS, signedOutDropdownStyles];
10
- }
11
- }
1
+ import { CSSResultGroup } from 'lit';
2
+ import { customElement } from 'lit/decorators.js';
3
+ import DropdownMenu from './dropdown-menu';
4
+ import dropdownMenuCSS from './styles/dropdown-menu';
5
+
6
+ @customElement('signed-out-dropdown')
7
+ export class SignedOutDropdown extends DropdownMenu {
8
+ static get styles(): CSSResultGroup {
9
+ return dropdownMenuCSS;
10
+ }
11
+ }