Skip to content

Commit 471659a

Browse files
committed
Logged-in keyboard navigation fixes
1 parent 0c2f6ae commit 471659a

File tree

9 files changed

+73
-54
lines changed

9 files changed

+73
-54
lines changed

packages/ia-topnav/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@internetarchive/ia-topnav",
3-
"version": "1.3.22",
3+
"version": "1.3.23-alpha1",
44
"description": "Top nav for Internet Archive",
55
"license": "AGPL-3.0-only",
66
"main": "dist/index.js",

packages/ia-topnav/src/dropdown-menu.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CSSResult, html, nothing, TemplateResult } from 'lit';
1+
import { CSSResult, html, nothing, PropertyValues, TemplateResult } from 'lit';
22
import { property } from 'lit/decorators.js';
33

44
import icons from './assets/img/icons';
@@ -9,6 +9,7 @@ import { IATopNavConfig, IATopNavLink } from './models';
99
import dropdownMenuCSS from './styles/dropdown-menu';
1010
import TrackedElement from './tracked-element';
1111
import { ifDefined } from 'lit/directives/if-defined.js';
12+
import KeyboardNavigation from './lib/keyboard-navigation';
1213

1314
export default class DropdownMenu extends TrackedElement {
1415
@property({ type: String }) baseHost = '';
@@ -18,10 +19,33 @@ export default class DropdownMenu extends TrackedElement {
1819
@property({ type: Boolean }) animated = false;
1920
@property({ type: Boolean }) open = false;
2021

22+
private previousKeydownListener?: // eslint-disable-next-line @typescript-eslint/no-explicit-any
23+
(this: HTMLElement, ev: KeyboardEvent) => any;
24+
2125
static get styles(): CSSResult[] {
2226
return [dropdownMenuCSS];
2327
}
2428

29+
updated(props: PropertyValues) {
30+
if (props.has('open') && this.open) {
31+
const container = this.shadowRoot?.querySelector(
32+
'.nav-container',
33+
) as HTMLElement;
34+
35+
if (container) {
36+
const keyboardNavigation = new KeyboardNavigation(
37+
container,
38+
'usermenu',
39+
);
40+
this.addEventListener('keydown', keyboardNavigation.handleKeyDown);
41+
if (this.previousKeydownListener) {
42+
this.removeEventListener('keydown', this.previousKeydownListener);
43+
}
44+
this.previousKeydownListener = keyboardNavigation.handleKeyDown;
45+
}
46+
}
47+
}
48+
2549
get dropdownItems() {
2650
if (!this.menuItems) return nothing;
2751

@@ -56,17 +80,19 @@ export default class DropdownMenu extends TrackedElement {
5680

5781
dropdownLink(link: IATopNavLink): TemplateResult {
5882
const calloutText = this.config?.callouts?.[link.title];
83+
const isMobileUpload = link.class === 'mobile-upload';
84+
const isTabbable = this.open && !isMobileUpload;
85+
5986
return html`<a
6087
href="${formatUrl(link.url, this.baseHost)}"
6188
class=${ifDefined(link.class)}
62-
tabindex="${this.open ? '' : '-1'}"
89+
tabindex="${isTabbable ? '' : '-1'}"
6390
@click=${this.trackClick}
6491
data-event-click-tracking="${this.config
6592
?.eventCategory}|Nav${link.analyticsEvent}"
6693
aria-label=${calloutText ? `New feature: ${link.title}` : nothing}
6794
>
68-
${link.class === 'mobile-upload' ? icons.uploadUnpadded : nothing}
69-
${link.title}
95+
${isMobileUpload ? icons.uploadUnpadded : nothing} ${link.title}
7096
${calloutText
7197
? html`<span class="callout" aria-hidden="true">${calloutText}</span>`
7298
: nothing}

packages/ia-topnav/src/ia-topnav.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,9 @@ export class IATopNav extends LitElement {
258258
?hideSearch=${this.hideSearch}
259259
tabindex="${this.signedOutTabIndex}"
260260
.menuItems=${this.signedOutMenuItems}
261+
@focusToOtherMenuItem=${(e: CustomEvent) => {
262+
this.currentTab = e.detail;
263+
}}
261264
></signed-out-dropdown>
262265
`;
263266
}

packages/ia-topnav/src/lib/keyboard-navigation.ts

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,31 +13,25 @@ export default class KeyboardNavigation {
1313
this.elementsContainer = elementsContainer;
1414
this.menuOption = menuOption;
1515
this.focusableElements = this.getFocusableElements();
16-
this.focusedIndex = this.getInitialFocusedIndex();
16+
this.focusedIndex = 0; // always start from first element
1717

1818
if (menuOption !== 'search') {
1919
this.focusableElements[this.focusedIndex]?.focus();
2020
}
2121
this.handleKeyDown = this.handleKeyDown.bind(this);
2222
}
2323

24-
/**
25-
* Returns the initial focused index based on the menu option.
26-
* @returns {number} The initial focused index (0 for 'web', 1 for 'usermenu').
27-
*/
28-
getInitialFocusedIndex(): number {
29-
return this.menuOption === 'usermenu' ? 1 : 0;
30-
}
31-
3224
/**
3325
* Gets an array of focusable elements within the container.
3426
* @returns {HTMLElement[]} An array of focusable elements.
3527
*/
3628
getFocusableElements(): HTMLElement[] {
37-
const focusableTagSelectors =
38-
'a[href], button, input, [tabindex]:not([tabindex="-1"])';
39-
const isDisabledOrHidden = (el: Element) =>
40-
!el.hasAttribute('disabled') && !el.getAttribute('aria-hidden');
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';
4135

4236
let elements;
4337
if (this.menuOption === 'web') {
@@ -69,9 +63,7 @@ export default class KeyboardNavigation {
6963
elements = this.elementsContainer.querySelectorAll(focusableTagSelectors);
7064
}
7165

72-
return Array.from(elements ?? []).filter(
73-
isDisabledOrHidden,
74-
) as HTMLElement[];
66+
return Array.from(elements ?? []).filter(isFocusable) as HTMLElement[];
7567
}
7668

7769
/**

packages/ia-topnav/src/login-button.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import TrackedElement from './tracked-element';
33
import icons from './assets/img/icons';
44
import loginButtonCSS from './styles/login-button';
55
import formatUrl from './lib/format-url';
6+
import { makeBooleanString } from './lib/make-boolean-string';
67
import { customElement, property, state } from 'lit/decorators.js';
78
import { IATopNavConfig } from './models';
89
import { defaultTopNavConfig } from './data/menus';
@@ -31,7 +32,7 @@ export class LoginButton extends TrackedElement {
3132
return `${this.config?.eventCategory}|NavLoginIcon`;
3233
}
3334

34-
get menuOpened() {
35+
get menuOpened(): boolean {
3536
return this.openMenu === 'login';
3637
}
3738

@@ -57,13 +58,15 @@ export class LoginButton extends TrackedElement {
5758
render() {
5859
return html`
5960
<div class="logged-out-toolbar">
60-
<a
61-
class="${this.avatarClass}"
61+
<button
62+
class="logged-out-menu ${this.avatarClass}"
6263
@click=${this.toggleDropdown}
6364
data-event-click-tracking="${this.analyticsEvent}"
65+
aria-label="Toggle login menu"
66+
aria-expanded="${makeBooleanString(this.menuOpened)}"
6467
>
6568
${icons.user}
66-
</a>
69+
</button>
6770
<span>
6871
<a href="${this.signupPath}">Sign up</a>
6972
|

packages/ia-topnav/src/primary-nav.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,20 @@ export class PrimaryNav extends TrackedElement {
9393
?.classList.contains('images');
9494
});
9595

96+
let nextElement;
97+
if (this.username) {
98+
nextElement = this.shadowRoot?.querySelector('a.upload');
99+
} else {
100+
nextElement = this.shadowRoot
101+
?.querySelector('login-button')
102+
?.shadowRoot?.querySelector('span a');
103+
}
104+
105+
const menuItemElement =
106+
lastMediaButton[0]?.shadowRoot?.querySelector('a.menu-item');
107+
96108
const focusElement =
97-
this.currentTab.moveTo === 'next'
98-
? this.shadowRoot?.querySelector('a.upload')
99-
: lastMediaButton[0]?.shadowRoot?.querySelector('a.menu-item');
109+
this.currentTab.moveTo === 'next' ? nextElement : menuItemElement;
100110

101111
if (focusElement) {
102112
(focusElement as HTMLElement).focus();

packages/ia-topnav/src/styles/login-button.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import { css } from 'lit';
22

33
export default css`
4+
.logged-out-menu {
5+
background: inherit;
6+
border: none;
7+
}
8+
.logged-out-menu:focus-visible {
9+
outline: none;
10+
border: none;
11+
}
412
.dropdown-toggle {
513
display: block;
614
text-transform: uppercase;

packages/ia-topnav/src/styles/primary-nav.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,8 @@ export default css`
304304
}
305305
306306
.upload span {
307-
display: inline;
307+
display: inline-block;
308+
vertical-align: middle;
308309
}
309310
}
310311
`;

packages/ia-topnav/src/user-menu.ts

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,18 @@
1-
import { CSSResult, html, PropertyValues } from 'lit';
1+
import { CSSResult, html } from 'lit';
22
import DropdownMenu from './dropdown-menu';
33
import userMenuCSS from './styles/user-menu';
44
import dropdownStyles from './styles/dropdown-menu';
5-
import KeyboardNavigation from './lib/keyboard-navigation';
65
import { customElement, property } from 'lit/decorators.js';
76

87
@customElement('user-menu')
98
export default class UserMenu extends DropdownMenu {
109
@property({ type: String }) username = '';
1110
@property({ type: String }) screenName = '';
1211

13-
private previousKeydownListener?: // eslint-disable-next-line @typescript-eslint/no-explicit-any
14-
(this: HTMLElement, ev: KeyboardEvent) => any;
15-
1612
static get styles(): CSSResult[] {
1713
return [dropdownStyles, userMenuCSS];
1814
}
1915

20-
updated(props: PropertyValues) {
21-
if (props.has('open') && this.open) {
22-
const container = this.shadowRoot?.querySelector(
23-
'.nav-container',
24-
) as HTMLElement;
25-
26-
if (container) {
27-
const keyboardNavigation = new KeyboardNavigation(
28-
container,
29-
'usermenu',
30-
);
31-
this.addEventListener('keydown', keyboardNavigation.handleKeyDown);
32-
if (this.previousKeydownListener) {
33-
this.removeEventListener('keydown', this.previousKeydownListener);
34-
}
35-
this.previousKeydownListener = keyboardNavigation.handleKeyDown;
36-
}
37-
}
38-
}
39-
4016
render() {
4117
return html`
4218
<div class="nav-container">

0 commit comments

Comments
 (0)