@internetarchive/ia-topnav 1.3.5-alpha2 → 1.3.5-alpha21

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@internetarchive/ia-topnav",
3
- "version": "1.3.5-alpha2",
3
+ "version": "1.3.5-alpha21",
4
4
  "description": "Top nav for Internet Archive",
5
5
  "license": "AGPL-3.0-only",
6
6
  "main": "index.js",
@@ -57,6 +57,7 @@ class DropdownMenu extends TrackedElement {
57
57
  return html`<a
58
58
  href="${formatUrl(link.url, this.baseHost)}"
59
59
  class="${link.class}"
60
+ tabindex="${this.open ? '' : '-1'}"
60
61
  @click=${this.trackClick}
61
62
  data-event-click-tracking="${this.config.eventCategory}|Nav${link.analyticsEvent}"
62
63
  aria-label=${calloutText ? `New feature: ${link.title}` : nothing}>
package/src/ia-topnav.js CHANGED
@@ -62,6 +62,7 @@ export default class IATopNav extends LitElement {
62
62
  username: { type: String },
63
63
  userProfileImagePath: { type: String },
64
64
  secondIdentitySlotMode: { type: String },
65
+ currentTab: { type: Object },
65
66
  };
66
67
  }
67
68
 
@@ -77,11 +78,12 @@ export default class IATopNav extends LitElement {
77
78
  this.searchIn = '';
78
79
  this.selectedMenuOption = '';
79
80
  this.secondIdentitySlotMode = '';
81
+ this.currentTab = {};
80
82
  }
81
83
 
82
84
  updated(props) {
83
85
  if (props.has('username') || props.has('localLinks') || props.has('baseHost') ||
84
- props.has('waybackPagesArchived') || props.has('itemIdentifier')) {
86
+ props.has('waybackPagesArchived') || props.has('itemIdentifier')) {
85
87
  this.menuSetup();
86
88
  }
87
89
  }
@@ -89,7 +91,10 @@ export default class IATopNav extends LitElement {
89
91
  firstUpdated() {
90
92
  // close open menu on `esc` click
91
93
  document.addEventListener('keydown', e => {
92
- if (e.key === 'Escape') this.closeMenus();
94
+ if (e.key === 'Escape') {
95
+ this.openMenu = '';
96
+ this.mediaSliderOpen = false;
97
+ }
93
98
  }, false);
94
99
  }
95
100
 
@@ -199,6 +204,7 @@ export default class IATopNav extends LitElement {
199
204
  tabindex="${this.userMenuTabIndex}"
200
205
  @menuToggled=${this.menuToggled}
201
206
  @trackClick=${this.trackClick}
207
+ @focusToOtherMenuItem=${(e) => this.currentTab = e.detail}
202
208
  ></user-menu>
203
209
  `;
204
210
  }
@@ -274,6 +280,7 @@ export default class IATopNav extends LitElement {
274
280
  .selectedMenuOption=${this.selectedMenuOption}
275
281
  .username=${this.username}
276
282
  .userProfileImagePath=${this.userProfileImagePath}
283
+ .currentTab=${this.currentTab}
277
284
  ?hideSearch=${this.hideSearch}
278
285
  @mediaTypeSelected=${this.mediaTypeSelected}
279
286
  @toggleSearchMenu=${this.toggleSearchMenu}
@@ -289,6 +296,8 @@ export default class IATopNav extends LitElement {
289
296
  .selectedMenuOption=${this.selectedMenuOption}
290
297
  .mediaSliderOpen=${this.mediaSliderOpen}
291
298
  .menus=${this.menus}
299
+ tabindex="${this.mediaSliderOpen ? '1' : '-1'}"
300
+ @focusToOtherMenuItem=${(e) => this.currentTab = e.detail}
292
301
  ></media-slider>
293
302
  </div>
294
303
  ${this.username ? this.userMenu : this.signedOutDropdown}
@@ -305,6 +314,7 @@ export default class IATopNav extends LitElement {
305
314
  <desktop-subnav
306
315
  .baseHost=${this.baseHost}
307
316
  .menuItems=${this.desktopSubnavMenuItems}
317
+ @focus=${this.closeMenus}
308
318
  ></desktop-subnav>
309
319
  <div id="close-layer" class="${this.closeLayerClass}" @click=${this.closeMenus}></div>
310
320
  `;
@@ -0,0 +1,128 @@
1
+ export default class KeyboardNavigation {
2
+ /**
3
+ * Constructor for the KeyboardNavigation class.
4
+ * @param {HTMLElement} elementsContainer - The container element that holds the focusable elements.
5
+ * @param {string} menuOption - The type of menu option ('web' or 'usermenu').
6
+ */
7
+ constructor(elementsContainer, menuOption) {
8
+ this.elementsContainer = elementsContainer;
9
+ this.menuOption = menuOption;
10
+ this.focusableElements = this.getFocusableElements();
11
+ this.focusedIndex = this.getInitialFocusedIndex();
12
+
13
+ this.focusableElements[this.focusedIndex]?.focus();
14
+ this.handleKeyDown = this.handleKeyDown.bind(this);
15
+ }
16
+
17
+ /**
18
+ * Returns the initial focused index based on the menu option.
19
+ * @returns {number} The initial focused index (0 for 'web', 1 for 'usermenu').
20
+ */
21
+ getInitialFocusedIndex() {
22
+ return this.menuOption === 'usermenu' ? 1 : 0;
23
+ }
24
+
25
+ /**
26
+ * Gets an array of focusable elements within the container.
27
+ * @returns {HTMLElement[]} An array of focusable elements.
28
+ */
29
+ getFocusableElements() {
30
+ const focusableTagSelectors = 'a[href], button, input, [tabindex]:not([tabindex="-1"])';
31
+ const isDisabledOrHidden = el => !el.hasAttribute('disabled') && !el.getAttribute('aria-hidden');
32
+
33
+ let elements;
34
+ if (this.menuOption === 'web') {
35
+ // wayback focusable elements
36
+ const waybackSlider = this.elementsContainer.querySelector('wayback-slider').shadowRoot;
37
+ const waybackSearch = waybackSlider.querySelector('wayback-search');
38
+ const waybackSearchElements = Array.from(waybackSearch.shadowRoot.querySelectorAll(focusableTagSelectors));
39
+
40
+ const normalElements = Array.from(waybackSlider.querySelectorAll(focusableTagSelectors));
41
+
42
+ // wayback save-form focusable elements
43
+ const savePageForm = waybackSlider.querySelector('save-page-form');
44
+ const savePageFormElements = Array.from(savePageForm.shadowRoot.querySelectorAll(focusableTagSelectors));
45
+
46
+ elements = [...waybackSearchElements, ...normalElements, ...savePageFormElements];
47
+ } else {
48
+ elements = this.elementsContainer.querySelectorAll(focusableTagSelectors);
49
+ }
50
+
51
+ return Array.from(elements).filter(isDisabledOrHidden);
52
+ }
53
+
54
+ /**
55
+ * Handles keyboard events and focuses the appropriate element.
56
+ * @param {KeyboardEvent} event - The keyboard event object.
57
+ */
58
+ handleKeyDown(event) {
59
+ const { key } = event;
60
+ const isArrowKey = ['ArrowDown', 'ArrowRight', 'ArrowUp', 'ArrowLeft'].includes(key);
61
+ const isTabKey = key === 'Tab';
62
+
63
+ if (isArrowKey) {
64
+ this.handleArrowKey(key);
65
+ event.preventDefault();
66
+ } else if (isTabKey) {
67
+ this.handleTabKey(event);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Handles arrow key events and focuses the next or previous element for topnav sub-nav and usermenu
73
+ * @param {string} key - The key that was pressed ('ArrowDown', 'ArrowRight', 'ArrowUp', or 'ArrowLeft').
74
+ */
75
+ handleArrowKey(key) {
76
+ const isDownOrRight = ['ArrowDown', 'ArrowRight'].includes(key);
77
+ isDownOrRight ? this.focusNext() : this.focusPrevious();
78
+ }
79
+
80
+ /**
81
+ * Focuses the previous focusable element in the container.
82
+ */
83
+ focusPrevious() {
84
+ if (this.focusableElements.length === 0) return;
85
+ this.focusedIndex = (this.focusedIndex - 1 + this.focusableElements.length) % this.focusableElements.length;
86
+ this.focusableElements[this.focusedIndex]?.focus();
87
+ }
88
+
89
+ /**
90
+ * Focuses the next focusable element in the container.
91
+ */
92
+ focusNext() {
93
+ if (this.focusableElements.length === 0) return;
94
+ this.focusedIndex = (this.focusedIndex + 1) % this.focusableElements.length;
95
+ this.focusableElements[this.focusedIndex]?.focus();
96
+ }
97
+
98
+ /**
99
+ * Handles the Tab key event and focuses the next or previous menu item.
100
+ * @param {KeyboardEvent} event - The keyboard event object.
101
+ */
102
+ handleTabKey(event) {
103
+ if (this.menuOption) {
104
+ const isShiftPressed = event.shiftKey;
105
+ this.focusToOtherMenuItems(isShiftPressed);
106
+ }
107
+
108
+ this.focusableElements[this.focusedIndex]?.blur();
109
+ event.preventDefault();
110
+ }
111
+
112
+ /**
113
+ * Focuses the other parent menu items based on the provided flag.
114
+ * @param {boolean} isPrevious - A flag indicating whether to focus the previous menu item.
115
+ */
116
+ focusToOtherMenuItems(isPrevious = false) {
117
+ this.elementsContainer.dispatchEvent(
118
+ new CustomEvent('focusToOtherMenuItem', {
119
+ bubbles: true,
120
+ composed: true,
121
+ detail: {
122
+ mediatype: this.menuOption,
123
+ moveTo: isPrevious ? 'prev' : 'next',
124
+ },
125
+ })
126
+ );
127
+ }
128
+ }
package/src/media-menu.js CHANGED
@@ -67,6 +67,7 @@ class MediaMenu extends LitElement {
67
67
  config: { type: Object },
68
68
  openMenu: { type: String },
69
69
  selectedMenuOption: { type: String },
70
+ currentTab: { type: Object },
70
71
  };
71
72
  }
72
73
 
@@ -75,6 +76,26 @@ class MediaMenu extends LitElement {
75
76
  this.config = {};
76
77
  this.openMenu = '';
77
78
  this.selectedMenuOption = '';
79
+ this.currentTab = {};
80
+ }
81
+
82
+ updated(props) {
83
+ if (props.has('currentTab')) {
84
+ const mediaButtons = Array.from(this.shadowRoot.querySelectorAll('media-button'));
85
+
86
+ mediaButtons.map((button, index) => {
87
+ const linkItem = button.shadowRoot.querySelector('a.menu-item');
88
+ if (linkItem) {
89
+ if (linkItem.classList.contains(`${this.selectedMenuOption}`)) {
90
+ linkItem.classList.remove('selected');
91
+ linkItem.blur();
92
+
93
+ const newFocusIndex = this.currentTab.moveTo === 'next' ? index + 1 : index - 1;
94
+ mediaButtons[newFocusIndex].shadowRoot.querySelector('a.menu-item').focus();
95
+ }
96
+ }
97
+ });
98
+ }
78
99
  }
79
100
 
80
101
  get mediaMenuOptionsTemplate() {
@@ -96,6 +117,7 @@ class MediaMenu extends LitElement {
96
117
  .mediatype=${menu}
97
118
  .openMenu=${this.openMenu}
98
119
  .selected=${selected}
120
+ .selectedMenuOption=${this.selectedMenuOption}
99
121
  data-mediatype="${menu}"
100
122
  ></media-button>
101
123
  `;
@@ -117,7 +139,6 @@ class MediaMenu extends LitElement {
117
139
  <div class="overflow-clip">
118
140
  <nav
119
141
  class="media-menu-inner"
120
- aria-hidden="${!this.menuOpened}"
121
142
  aria-expanded="${this.menuOpened}"
122
143
  >
123
144
  <div class="menu-group">
@@ -1,6 +1,7 @@
1
1
  import { LitElement, html } from 'https://offshoot.prod.archive.org/lit.js';
2
2
  import './media-subnav.js';
3
3
  import mediaSliderCSS from './styles/media-slider.js';
4
+ import KeyboardNavigation from './lib/keyboard-navigation.js';
4
5
 
5
6
  class MediaSlider extends LitElement {
6
7
  static get styles() {
@@ -26,6 +27,19 @@ class MediaSlider extends LitElement {
26
27
  this.selectedMenuOption = 'texts';
27
28
  }
28
29
 
30
+ updated(props) {
31
+ if (props.has('selectedMenuOption') && this.selectedMenuOption) {
32
+ const container = this.shadowRoot?.querySelector('.has-focused')?.shadowRoot;
33
+
34
+ if (container) {
35
+ const keyboardNavigation = new KeyboardNavigation(container, this.selectedMenuOption);
36
+ this.addEventListener('keydown', keyboardNavigation.handleKeyDown);
37
+ this.removeEventListener('keydown', this.previousKeydownListener);
38
+ this.previousKeydownListener = keyboardNavigation.handleKeyDown;
39
+ }
40
+ }
41
+ }
42
+
29
43
  shouldUpdate() {
30
44
  const scrollPane = this.shadowRoot ? this.shadowRoot.querySelector('.information-menu') : null;
31
45
 
@@ -47,49 +61,49 @@ class MediaSlider extends LitElement {
47
61
  <media-subnav
48
62
  .baseHost=${this.baseHost}
49
63
  .config=${this.config}
50
- class="${this.selectedMenuOption === 'audio' ? '' : 'hidden'}"
64
+ class="${this.selectedMenuOption === 'audio' ? 'has-focused' : 'hidden'}"
51
65
  menu="audio"
52
66
  .menuItems=${this.menus.audio}
53
67
  ></media-subnav>
54
68
  <media-subnav
55
69
  .baseHost=${this.baseHost}
56
70
  .config=${this.config}
57
- class="${this.selectedMenuOption === 'images' ? '' : 'hidden'}"
71
+ class="${this.selectedMenuOption === 'images' ? 'has-focused' : 'hidden'}"
58
72
  menu="images"
59
73
  .menuItems=${this.menus.images}
60
74
  ></media-subnav>
61
75
  <media-subnav
62
76
  .baseHost=${this.baseHost}
63
77
  .config=${this.config}
64
- class="${this.selectedMenuOption === 'software' ? '' : 'hidden'}"
78
+ class="${this.selectedMenuOption === 'software' ? 'has-focused' : 'hidden'}"
65
79
  menu="software"
66
80
  .menuItems=${this.menus.software}
67
81
  ></media-subnav>
68
82
  <media-subnav
69
83
  .baseHost=${this.baseHost}
70
84
  .config=${this.config}
71
- class="${this.selectedMenuOption === 'texts' ? '' : 'hidden'}"
85
+ class="${this.selectedMenuOption === 'texts' ? 'has-focused' : 'hidden'}"
72
86
  menu="texts"
73
87
  .menuItems=${this.menus.texts}
74
88
  ></media-subnav>
75
89
  <media-subnav
76
90
  .baseHost=${this.baseHost}
77
91
  .config=${this.config}
78
- class="${this.selectedMenuOption === 'video' ? '' : 'hidden'}"
92
+ class="${this.selectedMenuOption === 'video' ? 'has-focused' : 'hidden'}"
79
93
  menu="video"
80
94
  .menuItems=${this.menus.video}
81
95
  ></media-subnav>
82
96
  <media-subnav
83
97
  .baseHost=${this.baseHost}
84
98
  .config=${this.config}
85
- class="${this.selectedMenuOption === 'web' ? '' : 'hidden'}"
99
+ class="${this.selectedMenuOption === 'web' ? 'has-focused' : 'hidden'}"
86
100
  menu="web"
87
101
  .menuItems=${this.menus.web}
88
102
  ></media-subnav>
89
103
  <media-subnav
90
104
  .baseHost=${this.baseHost}
91
105
  .config=${this.config}
92
- class="${this.selectedMenuOption === 'more' ? '' : 'hidden'}"
106
+ class="${this.selectedMenuOption === 'more' ? 'has-focused' : 'hidden'}"
93
107
  menu="more"
94
108
  .menuItems=${this.menus.more}
95
109
  ></media-subnav>
package/src/nav-search.js CHANGED
@@ -98,9 +98,8 @@ class NavSearch extends TrackedElement {
98
98
  class="search-field"
99
99
  placeholder="Search"
100
100
  autocomplete="off"
101
- tabindex="2"
102
- @focus=${this.toggleSearchMenu}
103
101
  value=${this.searchQuery || ''}
102
+ @focus=${this.toggleSearchMenu}
104
103
  />
105
104
  ${this.searchInsideInput}
106
105
  <button
@@ -31,6 +31,7 @@ class PrimaryNav extends TrackedElement {
31
31
  userMenuOpen: { type: Boolean },
32
32
  username: { type: String },
33
33
  userProfileImagePath: { type: String },
34
+ currentTab: { type: Object },
34
35
  };
35
36
  }
36
37
 
@@ -44,6 +45,7 @@ class PrimaryNav extends TrackedElement {
44
45
  this.userMenuOpen = false;
45
46
  this.mediaBaseHost = 'https://archive.org';
46
47
  this.secondIdentitySlotMode = '';
48
+ this.currentTab = {};
47
49
  }
48
50
 
49
51
  toggleMediaMenu(e) {
@@ -79,6 +81,33 @@ class PrimaryNav extends TrackedElement {
79
81
  );
80
82
  }
81
83
 
84
+ updated(props) {
85
+ const { currentTab } = this;
86
+ const isUserMenuTab = currentTab && currentTab.mediatype === 'usermenu';
87
+ if (props.has('currentTab')) {
88
+ if (isUserMenuTab) {
89
+ const mediaButtons = Array.from(this.shadowRoot.querySelector('media-menu').shadowRoot.querySelectorAll('media-button'));
90
+ const lastMediaButton = mediaButtons.filter(element => {
91
+ return element.shadowRoot.querySelector('a').classList.contains('images')
92
+ });
93
+
94
+ const focusElement = currentTab.moveTo === 'next'
95
+ ? this.shadowRoot.querySelector('a.upload')
96
+ : lastMediaButton[0]?.shadowRoot.querySelector('a.menu-item');
97
+
98
+ if (focusElement) {
99
+ focusElement.focus();
100
+ }
101
+ } else if (this.currentTab.moveTo === 'next') {
102
+ if (this.shadowRoot.querySelector('.user-menu')) {
103
+ this.shadowRoot.querySelector('.user-menu').focus();
104
+ } else {
105
+ this.shadowRoot.querySelector('login-button').shadowRoot.querySelectorAll('span a')[0]?.focus();
106
+ }
107
+ }
108
+ }
109
+ }
110
+
82
111
  get userIcon() {
83
112
  const userMenuClass = this.openMenu === 'user' ? 'active' : '';
84
113
  const userMenuToolTip = this.openMenu === 'user' ? 'Close user menu' : 'Expand user menu';
@@ -87,7 +116,6 @@ class PrimaryNav extends TrackedElement {
87
116
  <button
88
117
  class="user-menu ${userMenuClass}"
89
118
  title="${userMenuToolTip}"
90
- tabindex="-1"
91
119
  @click="${this.toggleUserMenu}"
92
120
  data-event-click-tracking="${this.config.eventCategory}|NavUserMenu"
93
121
  >
@@ -107,7 +135,6 @@ class PrimaryNav extends TrackedElement {
107
135
  .config=${this.config}
108
136
  .dropdownOpen=${this.signedOutMenuOpen}
109
137
  .openMenu=${this.openMenu}
110
- tabindex="-1"
111
138
  @signedOutMenuToggled=${this.signedOutMenuToggled}
112
139
  ></login-button>
113
140
  `;
@@ -157,12 +184,8 @@ class PrimaryNav extends TrackedElement {
157
184
  return html`
158
185
  <a href="${formatUrl('/create', this.baseHost)}"
159
186
  class="upload"
160
- tabindex="1"
161
- @focus=${(e) => {
162
- if (e.relatedTarget !== null) {
163
- this.toggleSearchMenu(e)
164
- }
165
- }}>
187
+ @focus=${this.toggleMediaMenu}
188
+ >
166
189
  ${icons.upload}
167
190
  <span>Upload</span>
168
191
  </a>`;
@@ -191,6 +214,15 @@ class PrimaryNav extends TrackedElement {
191
214
  const mediaMenuTabIndex = this.openMenu === 'media' ? '' : '-1';
192
215
  return html`
193
216
  <nav class=${this.hideSearch ? 'hide-search' : nothing}>
217
+ <button
218
+ class="hamburger"
219
+ @click="${this.toggleMediaMenu}"
220
+ data-event-click-tracking="${this.config.eventCategory}|NavHamburger"
221
+ title="Open main menu"
222
+ >
223
+ <icon-hamburger ?active=${this.openMenu === 'media'}></icon-hamburger>
224
+ </button>
225
+
194
226
  <div class=${`branding ${this.secondLogoClass}`}>
195
227
  <a
196
228
  href=${formatUrl('/', this.baseHost)}
@@ -198,35 +230,24 @@ class PrimaryNav extends TrackedElement {
198
230
  data-event-click-tracking="${this.config.eventCategory}|NavHome"
199
231
  title="Go home"
200
232
  class="link-home"
201
- tabindex="-1"
202
233
  >${icons.iaLogo}${logoWordmarkStacked}</a
203
234
  >
204
235
  ${this.secondLogoSlot}
205
236
  </div>
206
-
207
- <div class="right-side-section">
208
- ${this.mobileDonateHeart}
209
- ${this.searchMenu}
210
- ${this.uploadButtonTemplate}
211
- ${this.userStateTemplate}
212
- </div>
213
237
  <media-menu
214
238
  .baseHost=${this.baseHost}
215
239
  .config=${this.config}
216
240
  ?mediaMenuAnimate="${this.mediaMenuAnimate}"
217
- tabindex="${mediaMenuTabIndex}"
218
241
  .selectedMenuOption=${this.selectedMenuOption}
219
242
  .openMenu=${this.openMenu}
243
+ .currentTab=${this.currentTab}
220
244
  ></media-menu>
221
- <button
222
- class="hamburger"
223
- @click="${this.toggleMediaMenu}"
224
- tabindex="1"
225
- data-event-click-tracking="${this.config.eventCategory}|NavHamburger"
226
- title="Open main menu"
227
- >
228
- <icon-hamburger ?active=${this.openMenu === 'media'}></icon-hamburger>
229
- </button>
245
+ <div class="right-side-section">
246
+ ${this.mobileDonateHeart}
247
+ ${this.userStateTemplate}
248
+ ${this.uploadButtonTemplate}
249
+ ${this.searchMenu}
250
+ </div>
230
251
  </nav>
231
252
  `;
232
253
  }
@@ -30,26 +30,33 @@ class SearchMenu extends TrackedElement {
30
30
  }
31
31
 
32
32
  firstUpdated() {
33
- this.shadowRoot.addEventListener('keyup', (e) => {
34
- const searchTypes = this.shadowRoot.querySelectorAll('.search-menu-inner label input[type=radio]');
35
- const length = searchTypes.length - 1;
36
-
37
- // early return if searchTypes not found
38
- if (!length) return;
39
-
40
- const searchTypeHandler = (index) => {
41
- const searchType = searchTypes[index];
42
- searchType.checked = true;
43
- searchType.dispatchEvent(new Event('change'));
44
- searchType.focus();
45
- };
46
-
47
- if (e.key === 'Home') {
48
- searchTypeHandler(0);
49
- } else if (e.key === 'End') {
50
- searchTypeHandler(length);
51
- }
52
- });
33
+ this.shadowRoot.addEventListener('keydown', e => this.handleKeyDownEvent(e));
34
+ }
35
+
36
+ disconnectedCallback() {
37
+ // Clean up event listener when the element is removed
38
+ this.shadowRoot.removeEventListener('keydown', e => this.handleKeyDownEvent(e));
39
+ }
40
+
41
+ handleKeyDownEvent(e) {
42
+ const searchTypes = this.shadowRoot.querySelectorAll('.search-menu-inner label input[type=radio]');
43
+
44
+ const length = searchTypes.length - 1;
45
+ if (!length) return;
46
+
47
+ const searchTypeHandler = (index) => {
48
+ e.preventDefault();
49
+ const searchType = searchTypes[index];
50
+ searchType.checked = true;
51
+ searchType.dispatchEvent(new Event('change'));
52
+ searchType.focus();
53
+ };
54
+
55
+ if (e.key === 'Home') {
56
+ searchTypeHandler(0);
57
+ } else if (e.key === 'End') {
58
+ searchTypeHandler(length);
59
+ }
53
60
  }
54
61
 
55
62
  selectSearchType(e) {
@@ -93,7 +100,7 @@ class SearchMenu extends TrackedElement {
93
100
  }
94
101
  return html`
95
102
  <label @click="${this.selectSearchType}">
96
- <input tabindex="3" form="nav-search" type="radio" name="sin" value="${value}" ?checked=${isDefault} @change=${this.searchInChanged} />
103
+ <input form="nav-search" type="radio" name="sin" value="${value}" ?checked=${isDefault} @change=${this.searchInChanged} />
97
104
  Search ${label}
98
105
  </label>
99
106
  `;
@@ -107,7 +114,7 @@ class SearchMenu extends TrackedElement {
107
114
  }
108
115
 
109
116
  render() {
110
- const searchMenuHidden = Boolean(!this.searchMenuOpen).toString();
117
+ const searchMenuHidden = Boolean(!this.openMenu).toString();
111
118
  const searchMenuExpanded = Boolean(this.searchMenuOpen).toString();
112
119
 
113
120
  if (this.hideSearch) {
@@ -127,7 +134,6 @@ class SearchMenu extends TrackedElement {
127
134
  href="${formatUrl('/advancedsearch.php', this.baseHost)}"
128
135
  @click=${this.trackClick}
129
136
  data-event-click-tracking="${this.config.eventCategory}|NavAdvancedSearch"
130
- tabindex="4"
131
137
  >Advanced Search</a
132
138
  >
133
139
  </div>
@@ -92,6 +92,7 @@ export default css`
92
92
 
93
93
  @media (min-width: 890px) {
94
94
  nav {
95
+ display: flex;
95
96
  overflow: visible;
96
97
  top: 0;
97
98
  left: auto;
@@ -146,6 +147,7 @@ export default css`
146
147
  a:focus {
147
148
  color: var(--linkHoverColor);
148
149
  background: var(--linkColor);
150
+ outline: none;
149
151
  }
150
152
 
151
153
  .initial,
@@ -42,6 +42,7 @@ export default css`
42
42
  span a {
43
43
  color: inherit;
44
44
  text-decoration: none;
45
+ outline: 0;
45
46
  }
46
47
 
47
48
  a:hover,
@@ -23,10 +23,6 @@ export default css`
23
23
 
24
24
  /* Mobile view styles */
25
25
  @media (max-width: 889px) {
26
- .media-menu-container {
27
- position: relative;
28
- }
29
-
30
26
  .media-menu-inner {
31
27
  position: absolute;
32
28
  width: 100%;
@@ -39,7 +35,7 @@ export default css`
39
35
  .overflow-clip {
40
36
  position: absolute;
41
37
  z-index: -1; /** needs to be under the navigation, otherwise it intercepts clicks */
42
- top: 0;
38
+ top: 4rem;
43
39
  left: 0;
44
40
  height: 0;
45
41
  width: 100%;
@@ -2,15 +2,13 @@ import { css } from 'https://offshoot.prod.archive.org/lit.js';
2
2
 
3
3
  export default css`
4
4
  button:focus,
5
- a:focus,
6
5
  input:focus {
7
6
  outline: none;
8
7
  }
9
8
 
10
9
  nav {
11
10
  position: relative;
12
- display: -ms-grid;
13
- display: grid;
11
+ display: flex;
14
12
  height: 4rem;
15
13
  grid-template-areas: 'hamburger empty heart search user';
16
14
  -ms-grid-columns: 4rem minmax(1rem, 100%) 4rem 4rem 4rem;
@@ -29,6 +27,7 @@ export default css`
29
27
 
30
28
  .right-side-section {
31
29
  display: flex;
30
+ margin-left: auto;
32
31
  user-select: none;
33
32
  }
34
33
  button {
@@ -86,6 +85,9 @@ export default css`
86
85
  fill: var(--activeColor);
87
86
  }
88
87
 
88
+ .mobile-donate-link {
89
+ display: inline-block;
90
+ }
89
91
  .mobile-donate-link svg {
90
92
  height: 4rem;
91
93
  width: 4rem;
@@ -151,8 +153,10 @@ export default css`
151
153
  height: 100%;
152
154
  }
153
155
 
154
- .user-menu:hover {
156
+ button.user-menu:hover,
157
+ button.user-menu:focus {
155
158
  color: var(--linkHoverColor);
159
+ outline: none;
156
160
  }
157
161
 
158
162
  .user-menu.active {
@@ -170,6 +174,10 @@ export default css`
170
174
  text-decoration: none;
171
175
  display: inline-flex;
172
176
  }
177
+ a.link-home:focus,
178
+ a.link-home:focus-visible {
179
+ outline-offset: 1px;
180
+ }
173
181
 
174
182
  @media only screen and (min-width: 890px) and (max-device-width: 905px) {
175
183
  .branding.second-logo {
@@ -187,6 +195,13 @@ export default css`
187
195
  slot[name='opt-sec-logo'] {
188
196
  display: none;
189
197
  }
198
+
199
+ .right-side-section {
200
+ display: initial;
201
+ }
202
+ .right-side-section .user-info {
203
+ float: right;
204
+ }
190
205
  }
191
206
 
192
207
  @media (min-width: 890px) {
@@ -195,12 +210,8 @@ export default css`
195
210
  --userIconHeight: 3.2rem;
196
211
  }
197
212
 
198
- .right-side-section {
199
- display: contents;
200
- }
201
-
202
213
  nav {
203
- display: block;
214
+ display: flex;
204
215
  z-index: 4;
205
216
  height: 5rem;
206
217
  padding-right: 1.5rem;
@@ -210,10 +221,6 @@ export default css`
210
221
  display: none;
211
222
  }
212
223
 
213
- .branding {
214
- margin-top: 1rem;
215
- }
216
-
217
224
  .ia-logo,
218
225
  .ia-wordmark {
219
226
  margin-right: 10px;
@@ -265,6 +272,9 @@ export default css`
265
272
  .upload:hover {
266
273
  color: var(--linkHoverColor);
267
274
  }
275
+ .upload:focus-visible {
276
+ outline: none;
277
+ }
268
278
 
269
279
  .upload svg {
270
280
  vertical-align: middle;
@@ -10,6 +10,7 @@ export default css`
10
10
  outline-color: var(--linkColor);
11
11
  outline-width: 0.16rem;
12
12
  outline-style: auto;
13
+ outline-offset: 2px !important;
13
14
  }
14
15
  .search-menu-inner {
15
16
  position: absolute;
@@ -89,6 +90,8 @@ export default css`
89
90
 
90
91
  label {
91
92
  padding: 0;
93
+ font-weight: normal;
94
+ margin: 0;
92
95
  }
93
96
 
94
97
  label + label {
package/src/user-menu.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { html } from 'https://offshoot.prod.archive.org/lit.js';
2
2
  import DropdownMenu from './dropdown-menu.js';
3
3
  import userMenuCSS from './styles/user-menu.js';
4
+ import KeyboardNavigation from './lib/keyboard-navigation.js';
4
5
 
5
6
  class UserMenu extends DropdownMenu {
6
7
  static get styles() {
@@ -21,6 +22,19 @@ class UserMenu extends DropdownMenu {
21
22
  this.username = '';
22
23
  }
23
24
 
25
+ updated(props) {
26
+ if (props.has('open') && this.open) {
27
+ const container = this.shadowRoot?.querySelector('.nav-container');
28
+
29
+ if (container) {
30
+ const keyboardNavigation = new KeyboardNavigation(container, 'usermenu');
31
+ this.addEventListener('keydown', keyboardNavigation.handleKeyDown);
32
+ this.removeEventListener('keydown', this.previousKeydownListener);
33
+ this.previousKeydownListener = keyboardNavigation.handleKeyDown;
34
+ }
35
+ }
36
+ }
37
+
24
38
  render() {
25
39
  return html`
26
40
  <div class="nav-container">