@internetarchive/ia-topnav 1.3.5 → 1.3.6-alpha1
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 +1 -1
- package/src/dropdown-menu.js +1 -0
- package/src/ia-topnav.js +12 -2
- package/src/lib/keyboard-navigation.js +128 -0
- package/src/login-button.js +3 -3
- package/src/media-menu.js +22 -1
- package/src/media-slider.js +21 -7
- package/src/nav-search.js +4 -18
- package/src/primary-nav.js +52 -18
- package/src/search-menu.js +31 -1
- package/src/styles/dropdown-menu.js +2 -0
- package/src/styles/login-button.js +4 -1
- package/src/styles/media-menu.js +1 -5
- package/src/styles/primary-nav.js +24 -13
- package/src/styles/search-menu.js +6 -0
- package/src/user-menu.js +14 -0
package/package.json
CHANGED
package/src/dropdown-menu.js
CHANGED
|
@@ -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
|
-
|
|
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')
|
|
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/login-button.js
CHANGED
|
@@ -67,9 +67,9 @@ class LoginButton extends TrackedElement {
|
|
|
67
67
|
${icons.user}
|
|
68
68
|
</a>
|
|
69
69
|
<span>
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
<a href="${this.signupPath}">Sign up</a>
|
|
71
|
+
|
|
|
72
|
+
<a href="${this.loginPath}">Log in</a>
|
|
73
73
|
</span>
|
|
74
74
|
</div>
|
|
75
75
|
`;
|
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">
|
package/src/media-slider.js
CHANGED
|
@@ -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
|
@@ -25,25 +25,10 @@ class NavSearch extends TrackedElement {
|
|
|
25
25
|
constructor() {
|
|
26
26
|
super();
|
|
27
27
|
this.config = {};
|
|
28
|
-
this.locationHandler = () => {};
|
|
28
|
+
this.locationHandler = () => { };
|
|
29
29
|
this.open = false;
|
|
30
30
|
this.openMenu = '';
|
|
31
31
|
this.searchIn = '';
|
|
32
|
-
this.inSearchBeta = false;
|
|
33
|
-
|
|
34
|
-
this.initSearchBetaOptIn();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
updated() {
|
|
38
|
-
if (this.open) {
|
|
39
|
-
this.shadowRoot.querySelector('[name=query]').focus();
|
|
40
|
-
}
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
initSearchBetaOptIn() {
|
|
45
|
-
this.inSearchBeta = !!window.localStorage?.getItem('SearchBeta-opt-in') ||
|
|
46
|
-
!!window.localStorage?.getItem('SearchBeta-launched');
|
|
47
32
|
}
|
|
48
33
|
|
|
49
34
|
search(e) {
|
|
@@ -83,7 +68,7 @@ class NavSearch extends TrackedElement {
|
|
|
83
68
|
}
|
|
84
69
|
|
|
85
70
|
get searchEndpoint() {
|
|
86
|
-
return
|
|
71
|
+
return '/search';
|
|
87
72
|
}
|
|
88
73
|
|
|
89
74
|
render() {
|
|
@@ -105,13 +90,14 @@ class NavSearch extends TrackedElement {
|
|
|
105
90
|
class="search-field"
|
|
106
91
|
placeholder="Search"
|
|
107
92
|
autocomplete="off"
|
|
108
|
-
@focus=${this.toggleSearchMenu}
|
|
109
93
|
value=${this.searchQuery || ''}
|
|
94
|
+
@focus=${this.toggleSearchMenu}
|
|
110
95
|
/>
|
|
111
96
|
${this.searchInsideInput}
|
|
112
97
|
<button
|
|
113
98
|
type="submit"
|
|
114
99
|
class="search"
|
|
100
|
+
tabindex="-1"
|
|
115
101
|
data-event-click-tracking="${this.config.eventCategory}|NavSearchClose"
|
|
116
102
|
>
|
|
117
103
|
${icons.search}
|
package/src/primary-nav.js
CHANGED
|
@@ -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,35 @@ class PrimaryNav extends TrackedElement {
|
|
|
79
81
|
);
|
|
80
82
|
}
|
|
81
83
|
|
|
84
|
+
updated(props) {
|
|
85
|
+
if (props.has('currentTab')) {
|
|
86
|
+
// early return
|
|
87
|
+
if (Object.keys(this.currentTab).length === 0) return nothing;
|
|
88
|
+
|
|
89
|
+
const isUserMenuTab = this.currentTab && this.currentTab.mediatype === 'usermenu';
|
|
90
|
+
if (isUserMenuTab) {
|
|
91
|
+
const mediaButtons = Array.from(this.shadowRoot.querySelector('media-menu').shadowRoot.querySelectorAll('media-button'));
|
|
92
|
+
const lastMediaButton = mediaButtons.filter(element => {
|
|
93
|
+
return element.shadowRoot.querySelector('a').classList.contains('images')
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const focusElement = this.currentTab.moveTo === 'next'
|
|
97
|
+
? this.shadowRoot.querySelector('a.upload')
|
|
98
|
+
: lastMediaButton[0]?.shadowRoot.querySelector('a.menu-item');
|
|
99
|
+
|
|
100
|
+
if (focusElement) {
|
|
101
|
+
focusElement.focus();
|
|
102
|
+
}
|
|
103
|
+
} else if (this.currentTab.moveTo === 'next') {
|
|
104
|
+
if (this.shadowRoot.querySelector('.user-menu')) {
|
|
105
|
+
this.shadowRoot.querySelector('.user-menu').focus();
|
|
106
|
+
} else {
|
|
107
|
+
this.shadowRoot.querySelector('login-button').shadowRoot.querySelectorAll('span a')[0]?.focus();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
82
113
|
get userIcon() {
|
|
83
114
|
const userMenuClass = this.openMenu === 'user' ? 'active' : '';
|
|
84
115
|
const userMenuToolTip = this.openMenu === 'user' ? 'Close user menu' : 'Expand user menu';
|
|
@@ -152,7 +183,11 @@ class PrimaryNav extends TrackedElement {
|
|
|
152
183
|
}
|
|
153
184
|
|
|
154
185
|
get uploadButtonTemplate() {
|
|
155
|
-
return html
|
|
186
|
+
return html`
|
|
187
|
+
<a href="${formatUrl('/create', this.baseHost)}"
|
|
188
|
+
class="upload"
|
|
189
|
+
@focus=${this.toggleMediaMenu}
|
|
190
|
+
>
|
|
156
191
|
${icons.upload}
|
|
157
192
|
<span>Upload</span>
|
|
158
193
|
</a>`;
|
|
@@ -181,6 +216,15 @@ class PrimaryNav extends TrackedElement {
|
|
|
181
216
|
const mediaMenuTabIndex = this.openMenu === 'media' ? '' : '-1';
|
|
182
217
|
return html`
|
|
183
218
|
<nav class=${this.hideSearch ? 'hide-search' : nothing}>
|
|
219
|
+
<button
|
|
220
|
+
class="hamburger"
|
|
221
|
+
@click="${this.toggleMediaMenu}"
|
|
222
|
+
data-event-click-tracking="${this.config.eventCategory}|NavHamburger"
|
|
223
|
+
title="Open main menu"
|
|
224
|
+
>
|
|
225
|
+
<icon-hamburger ?active=${this.openMenu === 'media'}></icon-hamburger>
|
|
226
|
+
</button>
|
|
227
|
+
|
|
184
228
|
<div class=${`branding ${this.secondLogoClass}`}>
|
|
185
229
|
<a
|
|
186
230
|
href=${formatUrl('/', this.baseHost)}
|
|
@@ -192,30 +236,20 @@ class PrimaryNav extends TrackedElement {
|
|
|
192
236
|
>
|
|
193
237
|
${this.secondLogoSlot}
|
|
194
238
|
</div>
|
|
195
|
-
|
|
196
|
-
<div class="right-side-section">
|
|
197
|
-
${this.mobileDonateHeart}
|
|
198
|
-
${this.searchMenu}
|
|
199
|
-
${this.uploadButtonTemplate}
|
|
200
|
-
${this.userStateTemplate}
|
|
201
|
-
</div>
|
|
202
239
|
<media-menu
|
|
203
240
|
.baseHost=${this.baseHost}
|
|
204
241
|
.config=${this.config}
|
|
205
242
|
?mediaMenuAnimate="${this.mediaMenuAnimate}"
|
|
206
|
-
tabindex="${mediaMenuTabIndex}"
|
|
207
243
|
.selectedMenuOption=${this.selectedMenuOption}
|
|
208
244
|
.openMenu=${this.openMenu}
|
|
245
|
+
.currentTab=${this.currentTab}
|
|
209
246
|
></media-menu>
|
|
210
|
-
<
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
>
|
|
217
|
-
<icon-hamburger ?active=${this.openMenu === 'media'}></icon-hamburger>
|
|
218
|
-
</button>
|
|
247
|
+
<div class="right-side-section">
|
|
248
|
+
${this.mobileDonateHeart}
|
|
249
|
+
${this.userStateTemplate}
|
|
250
|
+
${this.uploadButtonTemplate}
|
|
251
|
+
${this.searchMenu}
|
|
252
|
+
</div>
|
|
219
253
|
</nav>
|
|
220
254
|
`;
|
|
221
255
|
}
|
package/src/search-menu.js
CHANGED
|
@@ -29,6 +29,36 @@ class SearchMenu extends TrackedElement {
|
|
|
29
29
|
this.selectedSearchType = '';
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
firstUpdated() {
|
|
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
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
32
62
|
selectSearchType(e) {
|
|
33
63
|
this.selectedSearchType = e.target.value;
|
|
34
64
|
}
|
|
@@ -84,7 +114,7 @@ class SearchMenu extends TrackedElement {
|
|
|
84
114
|
}
|
|
85
115
|
|
|
86
116
|
render() {
|
|
87
|
-
const searchMenuHidden = Boolean(!this.
|
|
117
|
+
const searchMenuHidden = Boolean(!this.openMenu).toString();
|
|
88
118
|
const searchMenuExpanded = Boolean(this.searchMenuOpen).toString();
|
|
89
119
|
|
|
90
120
|
if (this.hideSearch) {
|
|
@@ -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,12 +42,15 @@ 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,
|
|
48
49
|
a:active,
|
|
49
50
|
a:focus {
|
|
50
|
-
color: var(--linkHoverColor);
|
|
51
|
+
color: var(--linkHoverColor) !important;
|
|
52
|
+
outline: none !important;
|
|
53
|
+
outline-offset: inherit !important;
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
@media (min-width: 890px) {
|
package/src/styles/media-menu.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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 {
|
|
@@ -42,6 +41,7 @@ export default css`
|
|
|
42
41
|
.branding {
|
|
43
42
|
position: static;
|
|
44
43
|
float: left;
|
|
44
|
+
margin: 0 !important;
|
|
45
45
|
padding: 0 5px 0 10px;
|
|
46
46
|
-webkit-transform: translate(0, 0);
|
|
47
47
|
-ms-transform: translate(0, 0);
|
|
@@ -86,6 +86,9 @@ export default css`
|
|
|
86
86
|
fill: var(--activeColor);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
.mobile-donate-link {
|
|
90
|
+
display: inline-block;
|
|
91
|
+
}
|
|
89
92
|
.mobile-donate-link svg {
|
|
90
93
|
height: 4rem;
|
|
91
94
|
width: 4rem;
|
|
@@ -151,8 +154,10 @@ export default css`
|
|
|
151
154
|
height: 100%;
|
|
152
155
|
}
|
|
153
156
|
|
|
154
|
-
.user-menu:hover
|
|
157
|
+
button.user-menu:hover,
|
|
158
|
+
button.user-menu:focus {
|
|
155
159
|
color: var(--linkHoverColor);
|
|
160
|
+
outline: none;
|
|
156
161
|
}
|
|
157
162
|
|
|
158
163
|
.user-menu.active {
|
|
@@ -170,6 +175,10 @@ export default css`
|
|
|
170
175
|
text-decoration: none;
|
|
171
176
|
display: inline-flex;
|
|
172
177
|
}
|
|
178
|
+
a.link-home:focus,
|
|
179
|
+
a.link-home:focus-visible {
|
|
180
|
+
outline-offset: 1px;
|
|
181
|
+
}
|
|
173
182
|
|
|
174
183
|
@media only screen and (min-width: 890px) and (max-device-width: 905px) {
|
|
175
184
|
.branding.second-logo {
|
|
@@ -187,6 +196,13 @@ export default css`
|
|
|
187
196
|
slot[name='opt-sec-logo'] {
|
|
188
197
|
display: none;
|
|
189
198
|
}
|
|
199
|
+
|
|
200
|
+
.right-side-section {
|
|
201
|
+
display: initial;
|
|
202
|
+
}
|
|
203
|
+
.right-side-section .user-info {
|
|
204
|
+
float: right;
|
|
205
|
+
}
|
|
190
206
|
}
|
|
191
207
|
|
|
192
208
|
@media (min-width: 890px) {
|
|
@@ -195,12 +211,8 @@ export default css`
|
|
|
195
211
|
--userIconHeight: 3.2rem;
|
|
196
212
|
}
|
|
197
213
|
|
|
198
|
-
.right-side-section {
|
|
199
|
-
display: contents;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
214
|
nav {
|
|
203
|
-
display:
|
|
215
|
+
display: flex;
|
|
204
216
|
z-index: 4;
|
|
205
217
|
height: 5rem;
|
|
206
218
|
padding-right: 1.5rem;
|
|
@@ -210,10 +222,6 @@ export default css`
|
|
|
210
222
|
display: none;
|
|
211
223
|
}
|
|
212
224
|
|
|
213
|
-
.branding {
|
|
214
|
-
margin-top: 1rem;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
225
|
.ia-logo,
|
|
218
226
|
.ia-wordmark {
|
|
219
227
|
margin-right: 10px;
|
|
@@ -265,6 +273,9 @@ export default css`
|
|
|
265
273
|
.upload:hover {
|
|
266
274
|
color: var(--linkHoverColor);
|
|
267
275
|
}
|
|
276
|
+
.upload:focus-visible {
|
|
277
|
+
outline: none;
|
|
278
|
+
}
|
|
268
279
|
|
|
269
280
|
.upload svg {
|
|
270
281
|
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;
|
|
@@ -43,6 +44,9 @@ export default css`
|
|
|
43
44
|
.advanced-search {
|
|
44
45
|
text-decoration: none;
|
|
45
46
|
color: var(--linkColor);
|
|
47
|
+
line-height: normal;
|
|
48
|
+
padding: 0.5rem;
|
|
49
|
+
margin-top: 5px;
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
@media (min-width: 890px) {
|
|
@@ -86,6 +90,8 @@ export default css`
|
|
|
86
90
|
|
|
87
91
|
label {
|
|
88
92
|
padding: 0;
|
|
93
|
+
font-weight: normal;
|
|
94
|
+
margin: 0;
|
|
89
95
|
}
|
|
90
96
|
|
|
91
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">
|