@internetarchive/ia-topnav 1.4.1-alpha-webdev8259.0 → 1.4.1-alpha-webdev8259.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 (43) hide show
  1. package/dist/index.d.ts +0 -1
  2. package/dist/index.js.map +1 -1
  3. package/dist/src/data/menus.js.map +1 -1
  4. package/dist/src/dropdown-menu.js +26 -26
  5. package/dist/src/dropdown-menu.js.map +1 -1
  6. package/dist/src/ia-topnav.d.ts +2 -13
  7. package/dist/src/ia-topnav.js +70 -140
  8. package/dist/src/ia-topnav.js.map +1 -1
  9. package/dist/src/lib/keyboard-navigation.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/media-menu.js +21 -21
  13. package/dist/src/media-menu.js.map +1 -1
  14. package/dist/src/models.d.ts +0 -1
  15. package/dist/src/models.js.map +1 -1
  16. package/dist/src/primary-nav.d.ts +6 -7
  17. package/dist/src/primary-nav.js +98 -142
  18. package/dist/src/primary-nav.js.map +1 -1
  19. package/dist/src/styles/login-button.js +87 -87
  20. package/dist/src/styles/login-button.js.map +1 -1
  21. package/dist/src/styles/primary-nav.js +343 -343
  22. package/dist/src/styles/primary-nav.js.map +1 -1
  23. package/dist/src/user-menu.js +13 -13
  24. package/dist/src/user-menu.js.map +1 -1
  25. package/dist/test/ia-topnav.test.js +15 -87
  26. package/dist/test/ia-topnav.test.js.map +1 -1
  27. package/dist/test/primary-nav.test.js +16 -34
  28. package/dist/test/primary-nav.test.js.map +1 -1
  29. package/index.ts +3 -4
  30. package/package.json +1 -1
  31. package/src/data/menus.ts +652 -652
  32. package/src/dropdown-menu.ts +132 -132
  33. package/src/ia-topnav.ts +301 -383
  34. package/src/lib/keyboard-navigation.ts +166 -166
  35. package/src/login-button.ts +78 -78
  36. package/src/media-menu.ts +143 -143
  37. package/src/models.ts +63 -65
  38. package/src/primary-nav.ts +277 -324
  39. package/src/styles/login-button.ts +90 -90
  40. package/src/styles/primary-nav.ts +346 -346
  41. package/src/user-menu.ts +32 -32
  42. package/test/ia-topnav.test.ts +282 -381
  43. package/test/primary-nav.test.ts +136 -163
@@ -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
+ }