Skip to content

Commit 201c502

Browse files
authored
feat(a11y): Implement keyboard navigation for Main Toolbar and Aux Navigation (Phase 1) (#5260)
This PR introduces full keyboard accessibility to the top navigation bars (Main Toolbar and Aux Nav), marking Phase 1 of the Music Blocks accessibility. The goal is to allow users to navigate and interact with the interface entirely without a mouse, while preserving existing mouse functionality. This implementation focuses on the header section, allowing traversal between buttons using arrow keys and activation via the Enter key.
1 parent d5a3120 commit 201c502

File tree

3 files changed

+686
-19
lines changed

3 files changed

+686
-19
lines changed

dist/css/style.css

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,30 @@ input[type="range"]:focus::-ms-fill-upper {
104104
position: absolute;
105105
cursor: pointer;
106106
}
107+
108+
/* Keyboard navigation focus state for toolbar buttons */
109+
.toolbar-btn-focused {
110+
background-color: rgba(0, 0, 0, 0.25) !important;
111+
border-radius: 4px;
112+
transition: background-color 0.15s ease;
113+
}
114+
115+
/* Dark mode focus - brighten the background */
116+
body.dark .toolbar-btn-focused,
117+
.dark-theme .toolbar-btn-focused {
118+
background-color: rgba(255, 255, 255, 0.3) !important;
119+
}
120+
121+
/* Keyboard navigation focus state for dropdown menu items */
122+
.dropdown-item-focused {
123+
background-color: rgba(0, 150, 136, 0.2) !important;
124+
}
125+
126+
/* Keyboard navigation focus state for modal dialog buttons */
127+
.modal-btn-focused {
128+
outline: 3px solid #009688 !important;
129+
outline-offset: 2px;
130+
box-shadow: 0 0 8px rgba(0, 150, 136, 0.5) !important;
131+
}
132+
107133
html, body { overscroll-behavior-x: none; }

js/__tests__/toolbar.test.js

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -37,30 +37,37 @@ global.window = {
3737
navigator: { language: "en-US" },
3838
document: {
3939
getElementById: jest.fn(() => ({ style: {} }))
40-
}
40+
},
41+
getComputedStyle: jest.fn(() => ({ display: "block", visibility: "visible" }))
4142
};
4243

4344
global.localStorage = window.localStorage;
4445

4546
const Toolbar = require("../toolbar");
4647

47-
global.document.getElementById = jest.fn(id => ({
48+
const createMockElement = id => ({
4849
id,
4950
style: {},
5051
setAttribute: jest.fn(),
52+
getAttribute: jest.fn(),
5153
innerHTML: "",
52-
classList: { add: jest.fn() },
53-
appendChild: jest.fn()
54-
}));
54+
classList: {
55+
add: jest.fn(),
56+
remove: jest.fn(),
57+
contains: jest.fn()
58+
},
59+
appendChild: jest.fn(),
60+
addEventListener: jest.fn(),
61+
removeEventListener: jest.fn(),
62+
querySelectorAll: jest.fn(() => []),
63+
querySelector: jest.fn(() => null),
64+
focus: jest.fn(),
65+
click: jest.fn(),
66+
getBoundingClientRect: jest.fn(() => ({ top: 0, left: 0, width: 10, height: 10 }))
67+
});
5568

56-
global.docById = jest.fn(id => ({
57-
id,
58-
setAttribute: jest.fn(),
59-
innerHTML: "",
60-
style: {},
61-
classList: { add: jest.fn() },
62-
appendChild: jest.fn()
63-
}));
69+
global.document.getElementById = jest.fn(createMockElement);
70+
global.docById = jest.fn(createMockElement);
6471

6572
global._ = jest.fn(str => str);
6673

@@ -136,7 +143,10 @@ describe("Toolbar Class", () => {
136143
return {
137144
setAttribute: jest.fn(),
138145
style: {},
139-
classList: { add: jest.fn() }
146+
classList: { add: jest.fn() },
147+
addEventListener: jest.fn(),
148+
removeEventListener: jest.fn(),
149+
querySelectorAll: jest.fn(() => [])
140150
};
141151
});
142152

@@ -179,7 +189,9 @@ describe("Toolbar Class", () => {
179189
}
180190
};
181191

182-
global.docById = jest.fn(id => elements[id]);
192+
global.docById = jest.fn(
193+
id => elements[id] || { addEventListener: jest.fn(), querySelectorAll: jest.fn() }
194+
);
183195
global.document = {
184196
body: {
185197
style: {
@@ -235,7 +247,9 @@ describe("Toolbar Class", () => {
235247
}
236248
};
237249

238-
global.docById.mockImplementation(id => elements[id] || {});
250+
global.docById.mockImplementation(
251+
id => elements[id] || { addEventListener: jest.fn(), querySelectorAll: jest.fn() }
252+
);
239253

240254
const mockActivity = {
241255
hideMsgs: jest.fn(),
@@ -268,7 +282,11 @@ describe("Toolbar Class", () => {
268282
const recordButton = { className: "recording" };
269283

270284
global.docById.mockImplementation(id =>
271-
id === "stop" ? stopIcon : id === "record" ? recordButton : {}
285+
id === "stop"
286+
? stopIcon
287+
: id === "record"
288+
? recordButton
289+
: { addEventListener: jest.fn(), querySelectorAll: jest.fn() }
272290
);
273291

274292
const mockOnClick = jest.fn();
@@ -288,14 +306,19 @@ describe("Toolbar Class", () => {
288306
test("renderNewProjectIcon displays modal and handles confirmation", () => {
289307
const elements = {
290308
"modal-container": {
291-
style: { display: "" }
309+
style: { display: "" },
310+
setAttribute: jest.fn(),
311+
addEventListener: jest.fn(),
312+
removeEventListener: jest.fn()
292313
},
293314
"newdropdown": {
294315
innerHTML: "",
295316
appendChild: jest.fn()
296317
}
297318
};
298-
global.docById = jest.fn(id => elements[id]);
319+
global.docById.mockImplementation(
320+
id => elements[id] || { addEventListener: jest.fn(), querySelectorAll: jest.fn() }
321+
);
299322
global.document = {
300323
createElement: jest.fn(tagName => ({
301324
tagName,

0 commit comments

Comments
 (0)