@internetarchive/ia-topnav 1.3.30 → 1.4.1-alpha-webdev8259.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 (53) 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/index.d.ts +1 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/src/data/menus.js.map +1 -1
  8. package/dist/src/dropdown-menu.js +26 -26
  9. package/dist/src/dropdown-menu.js.map +1 -1
  10. package/dist/src/ia-topnav.d.ts +4 -1
  11. package/dist/src/ia-topnav.js +96 -81
  12. package/dist/src/ia-topnav.js.map +1 -1
  13. package/dist/src/lib/keyboard-navigation.js.map +1 -1
  14. package/dist/src/login-button.js +17 -17
  15. package/dist/src/login-button.js.map +1 -1
  16. package/dist/src/media-menu.js +21 -21
  17. package/dist/src/media-menu.js.map +1 -1
  18. package/dist/src/models.d.ts +1 -0
  19. package/dist/src/models.js.map +1 -1
  20. package/dist/src/primary-nav.d.ts +3 -1
  21. package/dist/src/primary-nav.js +118 -95
  22. package/dist/src/primary-nav.js.map +1 -1
  23. package/dist/src/styles/login-button.js +87 -87
  24. package/dist/src/styles/login-button.js.map +1 -1
  25. package/dist/src/styles/primary-nav.js +343 -308
  26. package/dist/src/styles/primary-nav.js.map +1 -1
  27. package/dist/src/user-menu.js +13 -13
  28. package/dist/src/user-menu.js.map +1 -1
  29. package/dist/test/ia-topnav.test.js +39 -9
  30. package/dist/test/ia-topnav.test.js.map +1 -1
  31. package/dist/test/primary-nav.test.js +55 -7
  32. package/dist/test/primary-nav.test.js.map +1 -1
  33. package/eslint.config.mjs +53 -53
  34. package/index.ts +4 -3
  35. package/package.json +72 -72
  36. package/prettier.config.js +9 -9
  37. package/src/data/menus.ts +652 -652
  38. package/src/dropdown-menu.ts +132 -132
  39. package/src/ia-topnav.ts +383 -366
  40. package/src/lib/keyboard-navigation.ts +166 -166
  41. package/src/login-button.ts +78 -78
  42. package/src/media-menu.ts +143 -143
  43. package/src/models.ts +65 -63
  44. package/src/primary-nav.ts +324 -296
  45. package/src/styles/login-button.ts +90 -90
  46. package/src/styles/primary-nav.ts +346 -311
  47. package/src/user-menu.ts +32 -32
  48. package/ssl/server.key +28 -28
  49. package/test/ia-topnav.test.ts +381 -343
  50. package/test/primary-nav.test.ts +163 -94
  51. package/tsconfig.json +31 -31
  52. package/web-dev-server.config.mjs +32 -32
  53. package/web-test-runner.config.mjs +41 -41
@@ -1,166 +1,166 @@
1
- export default class KeyboardNavigation {
2
- elementsContainer: HTMLElement;
3
- menuOption: string;
4
- focusableElements: HTMLElement[];
5
- focusedIndex: number;
6
-
7
- /**
8
- * Constructor for the KeyboardNavigation class.
9
- * @param {HTMLElement} elementsContainer - The container element that holds the focusable elements.
10
- * @param {string} menuOption - The type of menu option ('web' or 'usermenu').
11
- */
12
- constructor(elementsContainer: HTMLElement, menuOption: string) {
13
- this.elementsContainer = elementsContainer;
14
- this.menuOption = menuOption;
15
- this.focusableElements = this.getFocusableElements();
16
- this.focusedIndex = 0; // always start from first element
17
-
18
- if (menuOption !== 'search') {
19
- this.focusableElements[this.focusedIndex]?.focus();
20
- }
21
- this.handleKeyDown = this.handleKeyDown.bind(this);
22
- }
23
-
24
- /**
25
- * Gets an array of focusable elements within the container.
26
- * @returns {HTMLElement[]} An array of focusable elements.
27
- */
28
- getFocusableElements(): HTMLElement[] {
29
- const focusableTagSelectors = 'a[href], button, input, [tabindex]';
30
-
31
- const isFocusable = (el: Element) =>
32
- !el.hasAttribute('disabled') &&
33
- el.getAttribute('aria-hidden') !== 'true' &&
34
- el.getAttribute('tabindex') !== '-1';
35
-
36
- let elements;
37
- if (this.menuOption === 'web') {
38
- // wayback focusable elements
39
- const waybackSlider =
40
- this.elementsContainer.querySelector('wayback-slider')?.shadowRoot;
41
- const waybackSearch = waybackSlider?.querySelector('wayback-search');
42
- const waybackSearchElements = Array.from(
43
- waybackSearch?.shadowRoot?.querySelectorAll(focusableTagSelectors) ??
44
- [],
45
- );
46
-
47
- const normalElements = Array.from(
48
- waybackSlider?.querySelectorAll(focusableTagSelectors) ?? [],
49
- );
50
-
51
- // wayback save-form focusable elements
52
- const savePageForm = waybackSlider?.querySelector('save-page-form');
53
- const savePageFormElements = Array.from(
54
- savePageForm?.shadowRoot?.querySelectorAll(focusableTagSelectors) ?? [],
55
- );
56
-
57
- elements = [
58
- ...waybackSearchElements,
59
- ...normalElements,
60
- ...savePageFormElements,
61
- ];
62
- } else {
63
- elements = this.elementsContainer.querySelectorAll(focusableTagSelectors);
64
- }
65
-
66
- return Array.from(elements ?? []).filter(isFocusable) as HTMLElement[];
67
- }
68
-
69
- /**
70
- * Handles keyboard events and focuses the appropriate element.
71
- * @param {KeyboardEvent} event - The keyboard event object.
72
- */
73
- handleKeyDown(event: KeyboardEvent) {
74
- const target = event.composedPath()[0] as HTMLElement;
75
-
76
- // Ignore events from editable fields
77
- if (
78
- target instanceof HTMLInputElement ||
79
- target instanceof HTMLTextAreaElement ||
80
- target.isContentEditable
81
- ) {
82
- return;
83
- }
84
-
85
- const { key } = event;
86
- const isArrowKey = [
87
- 'ArrowDown',
88
- 'ArrowRight',
89
- 'ArrowUp',
90
- 'ArrowLeft',
91
- ].includes(key);
92
- const isTabKey = key === 'Tab';
93
-
94
- if (isArrowKey) {
95
- this.handleArrowKey(key);
96
- event.preventDefault();
97
- } else if (isTabKey) {
98
- this.handleTabKey(event);
99
- }
100
- }
101
-
102
- /**
103
- * Handles arrow key events and focuses the next or previous element for topnav sub-nav and usermenu
104
- * @param {string} key - The key that was pressed ('ArrowDown', 'ArrowRight', 'ArrowUp', or 'ArrowLeft').
105
- */
106
- handleArrowKey(key: string) {
107
- const isDownOrRight = ['ArrowDown', 'ArrowRight'].includes(key);
108
- if (isDownOrRight) {
109
- this.focusNext();
110
- } else {
111
- this.focusPrevious();
112
- }
113
- }
114
-
115
- /**
116
- * Handles the Tab key event and focuses the next or previous menu item.
117
- * @param {KeyboardEvent} event - The keyboard event object.
118
- */
119
- handleTabKey(event: KeyboardEvent) {
120
- const isShiftPressed = event.shiftKey;
121
-
122
- this.emitFocusToOtherMenuItems(isShiftPressed);
123
-
124
- this.focusableElements[this.focusedIndex]?.blur();
125
- if (!['search'].includes(this.menuOption)) {
126
- event.preventDefault();
127
- }
128
- }
129
-
130
- /**
131
- * Focuses the previous focusable element in the container.
132
- */
133
- focusPrevious() {
134
- if (this.focusableElements.length === 0) return;
135
- this.focusedIndex =
136
- (this.focusedIndex - 1 + this.focusableElements.length) %
137
- this.focusableElements.length;
138
- this.focusableElements[this.focusedIndex]?.focus();
139
- }
140
-
141
- /**
142
- * Focuses the next focusable element in the container.
143
- */
144
- focusNext() {
145
- if (this.focusableElements.length === 0) return;
146
- this.focusedIndex = (this.focusedIndex + 1) % this.focusableElements.length;
147
- this.focusableElements[this.focusedIndex]?.focus();
148
- }
149
-
150
- /**
151
- * Focuses the other parent menu items based on the provided flag.
152
- * @param {boolean} isPrevious - A flag indicating whether to focus the previous menu item.
153
- */
154
- emitFocusToOtherMenuItems(isPrevious: boolean = false) {
155
- this.elementsContainer.dispatchEvent(
156
- new CustomEvent('focusToOtherMenuItem', {
157
- bubbles: true,
158
- composed: true,
159
- detail: {
160
- mediatype: this.menuOption,
161
- moveTo: isPrevious ? 'prev' : 'next',
162
- },
163
- }),
164
- );
165
- }
166
- }
1
+ export default class KeyboardNavigation {
2
+ elementsContainer: HTMLElement;
3
+ menuOption: string;
4
+ focusableElements: HTMLElement[];
5
+ focusedIndex: number;
6
+
7
+ /**
8
+ * Constructor for the KeyboardNavigation class.
9
+ * @param {HTMLElement} elementsContainer - The container element that holds the focusable elements.
10
+ * @param {string} menuOption - The type of menu option ('web' or 'usermenu').
11
+ */
12
+ constructor(elementsContainer: HTMLElement, menuOption: string) {
13
+ this.elementsContainer = elementsContainer;
14
+ this.menuOption = menuOption;
15
+ this.focusableElements = this.getFocusableElements();
16
+ this.focusedIndex = 0; // always start from first element
17
+
18
+ if (menuOption !== 'search') {
19
+ this.focusableElements[this.focusedIndex]?.focus();
20
+ }
21
+ this.handleKeyDown = this.handleKeyDown.bind(this);
22
+ }
23
+
24
+ /**
25
+ * Gets an array of focusable elements within the container.
26
+ * @returns {HTMLElement[]} An array of focusable elements.
27
+ */
28
+ getFocusableElements(): HTMLElement[] {
29
+ const focusableTagSelectors = 'a[href], button, input, [tabindex]';
30
+
31
+ const isFocusable = (el: Element) =>
32
+ !el.hasAttribute('disabled') &&
33
+ el.getAttribute('aria-hidden') !== 'true' &&
34
+ el.getAttribute('tabindex') !== '-1';
35
+
36
+ let elements;
37
+ if (this.menuOption === 'web') {
38
+ // wayback focusable elements
39
+ const waybackSlider =
40
+ this.elementsContainer.querySelector('wayback-slider')?.shadowRoot;
41
+ const waybackSearch = waybackSlider?.querySelector('wayback-search');
42
+ const waybackSearchElements = Array.from(
43
+ waybackSearch?.shadowRoot?.querySelectorAll(focusableTagSelectors) ??
44
+ [],
45
+ );
46
+
47
+ const normalElements = Array.from(
48
+ waybackSlider?.querySelectorAll(focusableTagSelectors) ?? [],
49
+ );
50
+
51
+ // wayback save-form focusable elements
52
+ const savePageForm = waybackSlider?.querySelector('save-page-form');
53
+ const savePageFormElements = Array.from(
54
+ savePageForm?.shadowRoot?.querySelectorAll(focusableTagSelectors) ?? [],
55
+ );
56
+
57
+ elements = [
58
+ ...waybackSearchElements,
59
+ ...normalElements,
60
+ ...savePageFormElements,
61
+ ];
62
+ } else {
63
+ elements = this.elementsContainer.querySelectorAll(focusableTagSelectors);
64
+ }
65
+
66
+ return Array.from(elements ?? []).filter(isFocusable) as HTMLElement[];
67
+ }
68
+
69
+ /**
70
+ * Handles keyboard events and focuses the appropriate element.
71
+ * @param {KeyboardEvent} event - The keyboard event object.
72
+ */
73
+ handleKeyDown(event: KeyboardEvent) {
74
+ const target = event.composedPath()[0] as HTMLElement;
75
+
76
+ // Ignore events from editable fields
77
+ if (
78
+ target instanceof HTMLInputElement ||
79
+ target instanceof HTMLTextAreaElement ||
80
+ target.isContentEditable
81
+ ) {
82
+ return;
83
+ }
84
+
85
+ const { key } = event;
86
+ const isArrowKey = [
87
+ 'ArrowDown',
88
+ 'ArrowRight',
89
+ 'ArrowUp',
90
+ 'ArrowLeft',
91
+ ].includes(key);
92
+ const isTabKey = key === 'Tab';
93
+
94
+ if (isArrowKey) {
95
+ this.handleArrowKey(key);
96
+ event.preventDefault();
97
+ } else if (isTabKey) {
98
+ this.handleTabKey(event);
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Handles arrow key events and focuses the next or previous element for topnav sub-nav and usermenu
104
+ * @param {string} key - The key that was pressed ('ArrowDown', 'ArrowRight', 'ArrowUp', or 'ArrowLeft').
105
+ */
106
+ handleArrowKey(key: string) {
107
+ const isDownOrRight = ['ArrowDown', 'ArrowRight'].includes(key);
108
+ if (isDownOrRight) {
109
+ this.focusNext();
110
+ } else {
111
+ this.focusPrevious();
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Handles the Tab key event and focuses the next or previous menu item.
117
+ * @param {KeyboardEvent} event - The keyboard event object.
118
+ */
119
+ handleTabKey(event: KeyboardEvent) {
120
+ const isShiftPressed = event.shiftKey;
121
+
122
+ this.emitFocusToOtherMenuItems(isShiftPressed);
123
+
124
+ this.focusableElements[this.focusedIndex]?.blur();
125
+ if (!['search'].includes(this.menuOption)) {
126
+ event.preventDefault();
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Focuses the previous focusable element in the container.
132
+ */
133
+ focusPrevious() {
134
+ if (this.focusableElements.length === 0) return;
135
+ this.focusedIndex =
136
+ (this.focusedIndex - 1 + this.focusableElements.length) %
137
+ this.focusableElements.length;
138
+ this.focusableElements[this.focusedIndex]?.focus();
139
+ }
140
+
141
+ /**
142
+ * Focuses the next focusable element in the container.
143
+ */
144
+ focusNext() {
145
+ if (this.focusableElements.length === 0) return;
146
+ this.focusedIndex = (this.focusedIndex + 1) % this.focusableElements.length;
147
+ this.focusableElements[this.focusedIndex]?.focus();
148
+ }
149
+
150
+ /**
151
+ * Focuses the other parent menu items based on the provided flag.
152
+ * @param {boolean} isPrevious - A flag indicating whether to focus the previous menu item.
153
+ */
154
+ emitFocusToOtherMenuItems(isPrevious: boolean = false) {
155
+ this.elementsContainer.dispatchEvent(
156
+ new CustomEvent('focusToOtherMenuItem', {
157
+ bubbles: true,
158
+ composed: true,
159
+ detail: {
160
+ mediatype: this.menuOption,
161
+ moveTo: isPrevious ? 'prev' : 'next',
162
+ },
163
+ }),
164
+ );
165
+ }
166
+ }
@@ -1,78 +1,78 @@
1
- import { html } from 'lit';
2
- import TrackedElement from './tracked-element';
3
- import icons from './assets/img/icons';
4
- import loginButtonCSS from './styles/login-button';
5
- import formatUrl from './lib/format-url';
6
- import { makeBooleanString } from './lib/make-boolean-string';
7
- import { customElement, property, state } from 'lit/decorators.js';
8
- import { IATopNavConfig } from './models';
9
- import { defaultTopNavConfig } from './data/menus';
10
-
11
- @customElement('login-button')
12
- export class LoginButton extends TrackedElement {
13
- @property({ type: String }) baseHost = '';
14
- @property({ type: Object }) config: IATopNavConfig = defaultTopNavConfig;
15
- @property({ type: String }) openMenu = '';
16
-
17
- @state() private dropdownTabIndex = '';
18
-
19
- static get styles() {
20
- return loginButtonCSS;
21
- }
22
-
23
- get signupPath() {
24
- return formatUrl('/account/signup', this.baseHost);
25
- }
26
-
27
- get loginPath() {
28
- return formatUrl('/login', this.baseHost);
29
- }
30
-
31
- get analyticsEvent() {
32
- return `${this.config?.eventCategory}|NavLoginIcon`;
33
- }
34
-
35
- get menuOpened(): boolean {
36
- return this.openMenu === 'login';
37
- }
38
-
39
- get avatarClass() {
40
- return `dropdown-toggle${this.menuOpened ? ' active' : ''}`;
41
- }
42
-
43
- toggleDropdown(e: Event) {
44
- e.preventDefault();
45
- this.trackClick(e);
46
- this.dropdownTabIndex = this.menuOpened ? '' : '-1';
47
- this.dispatchEvent(
48
- new CustomEvent('menuToggled', {
49
- bubbles: true,
50
- composed: true,
51
- detail: {
52
- menuName: 'login',
53
- },
54
- }),
55
- );
56
- }
57
-
58
- render() {
59
- return html`
60
- <div class="logged-out-toolbar">
61
- <button
62
- class="logged-out-menu ${this.avatarClass}"
63
- @click=${this.toggleDropdown}
64
- data-event-click-tracking="${this.analyticsEvent}"
65
- aria-label="Toggle login menu"
66
- aria-expanded="${makeBooleanString(this.menuOpened)}"
67
- >
68
- ${icons.user}
69
- </button>
70
- <span>
71
- <a href="${this.signupPath}">Sign up</a>
72
- |
73
- <a href="${this.loginPath}">Log in</a>
74
- </span>
75
- </div>
76
- `;
77
- }
78
- }
1
+ import { html } from 'lit';
2
+ import TrackedElement from './tracked-element';
3
+ import icons from './assets/img/icons';
4
+ import loginButtonCSS from './styles/login-button';
5
+ import formatUrl from './lib/format-url';
6
+ import { makeBooleanString } from './lib/make-boolean-string';
7
+ import { customElement, property, state } from 'lit/decorators.js';
8
+ import { IATopNavConfig } from './models';
9
+ import { defaultTopNavConfig } from './data/menus';
10
+
11
+ @customElement('login-button')
12
+ export class LoginButton extends TrackedElement {
13
+ @property({ type: String }) baseHost = '';
14
+ @property({ type: Object }) config: IATopNavConfig = defaultTopNavConfig;
15
+ @property({ type: String }) openMenu = '';
16
+
17
+ @state() private dropdownTabIndex = '';
18
+
19
+ static get styles() {
20
+ return loginButtonCSS;
21
+ }
22
+
23
+ get signupPath() {
24
+ return formatUrl('/account/signup', this.baseHost);
25
+ }
26
+
27
+ get loginPath() {
28
+ return formatUrl('/login', this.baseHost);
29
+ }
30
+
31
+ get analyticsEvent() {
32
+ return `${this.config?.eventCategory}|NavLoginIcon`;
33
+ }
34
+
35
+ get menuOpened(): boolean {
36
+ return this.openMenu === 'login';
37
+ }
38
+
39
+ get avatarClass() {
40
+ return `dropdown-toggle${this.menuOpened ? ' active' : ''}`;
41
+ }
42
+
43
+ toggleDropdown(e: Event) {
44
+ e.preventDefault();
45
+ this.trackClick(e);
46
+ this.dropdownTabIndex = this.menuOpened ? '' : '-1';
47
+ this.dispatchEvent(
48
+ new CustomEvent('menuToggled', {
49
+ bubbles: true,
50
+ composed: true,
51
+ detail: {
52
+ menuName: 'login',
53
+ },
54
+ }),
55
+ );
56
+ }
57
+
58
+ render() {
59
+ return html`
60
+ <div class="logged-out-toolbar">
61
+ <button
62
+ class="logged-out-menu ${this.avatarClass}"
63
+ @click=${this.toggleDropdown}
64
+ data-event-click-tracking="${this.analyticsEvent}"
65
+ aria-label="Toggle login menu"
66
+ aria-expanded="${makeBooleanString(this.menuOpened)}"
67
+ >
68
+ ${icons.user}
69
+ </button>
70
+ <span>
71
+ <a href="${this.signupPath}">Sign up</a>
72
+ |
73
+ <a href="${this.loginPath}">Log in</a>
74
+ </span>
75
+ </div>
76
+ `;
77
+ }
78
+ }