Skip to content

Commit 1f5b4c2

Browse files
Merge pull request steam-bell-92#871 from nishtha-agarwal-211/fix/789-improve-a11y-keyboard-nav
fix(a11y): improve web app accessibility and keyboard navigation (steam-bell-92#789)
2 parents 16c69f2 + 7d64304 commit 1f5b4c2

3 files changed

Lines changed: 177 additions & 26 deletions

File tree

web-app/css/styles.css

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3513,6 +3513,97 @@ body.sidebar-active .hero-controls {
35133513
}
35143514
}
35153515

3516+
/* ═══════════════════════════════════════
3517+
FOCUS-VISIBLE — KEYBOARD ACCESSIBILITY
3518+
═══════════════════════════════════════ */
3519+
3520+
/* Global fallback for any interactive element */
3521+
button:focus-visible,
3522+
a:focus-visible,
3523+
input:focus-visible,
3524+
select:focus-visible,
3525+
textarea:focus-visible,
3526+
[tabindex]:focus-visible {
3527+
outline: 2px solid var(--accent, #6366f1);
3528+
outline-offset: 2px;
3529+
}
3530+
3531+
/* Search input */
3532+
#searchInput:focus-visible {
3533+
outline: 2px solid var(--accent, #6366f1);
3534+
outline-offset: 0;
3535+
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.15);
3536+
}
3537+
3538+
/* Sidebar tabs */
3539+
.sidebar-tab:focus-visible {
3540+
outline: 2px solid var(--accent, #6366f1);
3541+
outline-offset: 2px;
3542+
border-radius: 8px;
3543+
}
3544+
3545+
/* Search dropdown items — keyboard highlight */
3546+
.dropdown-item:focus-visible,
3547+
.dropdown-item.selected {
3548+
outline: 2px solid var(--accent, #6366f1);
3549+
outline-offset: -2px;
3550+
background: var(--accent-soft);
3551+
color: var(--text);
3552+
}
3553+
3554+
/* Project cards */
3555+
.project-card:focus-visible {
3556+
outline: 2px solid var(--accent, #6366f1);
3557+
outline-offset: 2px;
3558+
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.15);
3559+
}
3560+
3561+
/* Play / Try-It buttons */
3562+
.btn-play:focus-visible {
3563+
outline: 2px solid var(--accent, #6366f1);
3564+
outline-offset: 2px;
3565+
}
3566+
3567+
/* Hero CTA buttons */
3568+
.btn-primary-hero:focus-visible,
3569+
.btn-secondary-hero:focus-visible,
3570+
.btn-contribution-hero:focus-visible {
3571+
outline: 2px solid rgba(255, 255, 255, 0.85);
3572+
outline-offset: 3px;
3573+
}
3574+
3575+
/* Sound & Theme toggles */
3576+
.sound-toggle:focus-visible,
3577+
.theme-toggle:focus-visible {
3578+
outline: 2px solid var(--accent, #6366f1);
3579+
outline-offset: 2px;
3580+
}
3581+
3582+
/* Random project button */
3583+
.btn-random-project:focus-visible {
3584+
outline: 2px solid var(--accent, #6366f1);
3585+
outline-offset: 2px;
3586+
}
3587+
3588+
/* Favorite & share buttons on cards */
3589+
.btn-favorite:focus-visible,
3590+
.btn-share:focus-visible {
3591+
outline: 2px solid var(--accent, #6366f1);
3592+
outline-offset: 2px;
3593+
}
3594+
3595+
/* Modal close button */
3596+
.modal-close:focus-visible {
3597+
outline: 2px solid var(--accent, #6366f1);
3598+
outline-offset: 2px;
3599+
}
3600+
3601+
/* Mobile sidebar toggle */
3602+
.mobile-sidebar-toggle:focus-visible {
3603+
outline: 2px solid var(--accent, #6366f1);
3604+
outline-offset: 2px;
3605+
}
3606+
35163607
/* ═══════════════════════════════════════
35173608
BACK TO TOP
35183609
═══════════════════════════════════════ */

web-app/index.html

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232

3333
<!-- Mobile Sidebar Toggle -->
3434
<button type="button" class="mobile-sidebar-toggle" id="mobileSidebarToggle" aria-label="Toggle sidebar menu" aria-expanded="false">
35-
<i class="fas fa-bars"></i>
35+
<i class="fas fa-bars" aria-hidden="true"></i>
3636
</button>
3737

3838
<!-- ── PRIMARY SIDEBAR DOCK ──────────────────────────────────── -->
@@ -52,9 +52,9 @@
5252

5353
<div class="sidebar-search">
5454
<div class="search-box" style="position:relative">
55-
<i class="fas fa-search"></i>
56-
<input aria-label="Search projects" id="searchInput" placeholder="Search projects or tags..." type="text"/>
57-
<div class="search-dropdown" id="searchDropdown">
55+
<i class="fas fa-search" aria-hidden="true"></i>
56+
<input aria-label="Search projects" id="searchInput" placeholder="Search projects or tags..." type="text" role="combobox" aria-autocomplete="list" aria-controls="resultsList" aria-expanded="false"/>
57+
<div class="search-dropdown" id="searchDropdown" aria-live="polite">
5858
<div class="dropdown-content" id="searchDropdownContent">
5959
<div class="search-loader" id="searchLoader" style="display:none"><div class="spinner"></div></div>
6060
<div class="recent-searches-section" id="recentSearchesSection">
@@ -63,7 +63,7 @@
6363
</div>
6464
<div class="results-section" id="resultsSection" style="display:none">
6565
<div class="dropdown-section-title">Projects</div>
66-
<div class="dropdown-list" id="resultsList"></div>
66+
<div class="dropdown-list" id="resultsList" role="listbox"></div>
6767
</div>
6868
<div class="tips-section" id="tipsSection">
6969
<p><strong>Search tips</strong></p>
@@ -343,23 +343,23 @@ <h3>Word Scramble</h3>
343343

344344
<div class="sidebar-footer">
345345
<div class="sidebar-controls">
346-
<button aria-label="Mute sounds" class="sound-toggle" id="soundToggle" type="button">
347-
<i class="fas fa-volume-up"></i>
346+
<button aria-label="Mute sound" class="sound-toggle" id="soundToggle" type="button">
347+
<i class="fas fa-volume-up" aria-hidden="true"></i>
348348
</button>
349-
<button aria-label="Switch theme" class="theme-toggle" id="themeToggle" type="button">
350-
<i class="fas fa-sun"></i>
349+
<button aria-label="Switch to light mode" class="theme-toggle" id="themeToggle" type="button">
350+
<i class="fas fa-sun" aria-hidden="true"></i>
351351
</button>
352352
</div>
353353
</div>
354354
</aside>
355355

356356

357357
<div class="hero-controls">
358-
<button aria-label="Mute sounds" class="hero-control-btn sound-toggle" id="heroSoundToggle" type="button">
359-
<i class="fas fa-volume-up"></i>
358+
<button aria-label="Mute sound" class="hero-control-btn sound-toggle" id="heroSoundToggle" type="button">
359+
<i class="fas fa-volume-up" aria-hidden="true"></i>
360360
</button>
361-
<button aria-label="Switch theme" class="hero-control-btn theme-toggle" id="heroThemeToggle" type="button">
362-
<i class="fas fa-sun"></i>
361+
<button aria-label="Switch to light mode" class="hero-control-btn theme-toggle" id="heroThemeToggle" type="button">
362+
<i class="fas fa-sun" aria-hidden="true"></i>
363363
</button>
364364
</div>
365365

@@ -408,15 +408,15 @@ <h1 class="hero-title">
408408
<!-- ── Hero CTA Action Row ──────────────────────────────────────── -->
409409
<div class="hero-cta-row">
410410
<button class="btn-primary-hero" id="exploreBtn">
411-
<i class="fas fa-rocket"></i>
411+
<i class="fas fa-rocket" aria-hidden="true"></i>
412412
Explore Projects
413413
</button>
414414
<button class="btn-secondary-hero" id="randomProjectBtn">
415-
<i class="fas fa-shuffle"></i>
415+
<i class="fas fa-shuffle" aria-hidden="true"></i>
416416
Surprise Me
417417
</button>
418418
<a class="btn-contribution-hero" id="contributionBtn" href="https://github.com/steam-bell-92/python-mini-project/blob/main/CONTRIBUTING.md" target="_blank" rel="noopener noreferrer">
419-
<i class="fas fa-code-branch"></i>
419+
<i class="fas fa-code-branch" aria-hidden="true"></i>
420420
Open Contribution
421421
</a>
422422
</div>
@@ -523,7 +523,7 @@ <h3 class="timeline-title">Share</h3>
523523
<div class="section-header">
524524
<h2 class="section-heading">Projects <span id="projectCountBadge"></span></h2>
525525
<button class="btn-random-project" id="randomProjectBtnSidebar" type="button">
526-
<i class="fas fa-shuffle"></i>
526+
<i class="fas fa-shuffle" aria-hidden="true"></i>
527527
Surprise Me
528528
</button>
529529
</div>
@@ -613,7 +613,7 @@ <h2 class="playground-title">🐍 Python Playground</h2>
613613

614614
<!-- ── Back to Top ───────────────────────────────────────────── -->
615615
<button aria-label="Back to top" class="back-to-top" id="backToTop" title="Back to top" type="button">
616-
<i class="fas fa-arrow-up"></i>
616+
<i class="fas fa-arrow-up" aria-hidden="true"></i>
617617
</button>
618618

619619
<!-- ── Footer ────────────────────────────────────────────────── -->

web-app/js/main.js

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -490,8 +490,8 @@ document.addEventListener("DOMContentLoaded", function () {
490490
// Show sun for dark theme, moon for light theme so reload displays sun by default.
491491
themeToggle.innerHTML =
492492
savedTheme === "dark"
493-
? '<i class="fas fa-sun"></i>'
494-
: '<i class="fas fa-moon"></i>';
493+
? '<i class="fas fa-sun" aria-hidden="true"></i>'
494+
: '<i class="fas fa-moon" aria-hidden="true"></i>';
495495
updateThemeToggleAria(savedTheme === "light");
496496

497497
themeToggle.addEventListener("click", function () {
@@ -503,18 +503,23 @@ document.addEventListener("DOMContentLoaded", function () {
503503
// After toggling, show sun when the new theme is dark, moon when it's light.
504504
themeToggle.innerHTML =
505505
next === "dark"
506-
? '<i class="fas fa-sun"></i>'
507-
: '<i class="fas fa-moon"></i>';
506+
? '<i class="fas fa-sun" aria-hidden="true"></i>'
507+
: '<i class="fas fa-moon" aria-hidden="true"></i>';
508508
updateThemeToggleAria(next === "light");
509509
});
510510
}
511511

512512
/* ── Sound Toggle ─────────────────────────────────────────── */
513513
if (soundToggle && window.audioController) {
514514
function updateSoundIcon() {
515-
soundToggle.innerHTML = window.audioController.isMuted
516-
? '<i class="fas fa-volume-mute"></i>'
517-
: '<i class="fas fa-volume-up"></i>';
515+
var isMuted = window.audioController.isMuted;
516+
soundToggle.innerHTML = isMuted
517+
? '<i class="fas fa-volume-mute" aria-hidden="true"></i>'
518+
: '<i class="fas fa-volume-up" aria-hidden="true"></i>';
519+
soundToggle.setAttribute(
520+
"aria-label",
521+
isMuted ? "Unmute sound" : "Mute sound"
522+
);
518523
}
519524
updateSoundIcon();
520525
soundToggle.addEventListener("click", function () {
@@ -1210,6 +1215,7 @@ document.addEventListener('DOMContentLoaded', () => {
12101215

12111216
function closeDropdown() {
12121217
if (searchDropdown) searchDropdown.classList.remove("active");
1218+
if (searchInput) searchInput.setAttribute("aria-expanded", "false");
12131219
}
12141220

12151221
function renderRecentSearches() {
@@ -1302,6 +1308,11 @@ document.addEventListener('DOMContentLoaded', () => {
13021308
item.className =
13031309
"dropdown-item" +
13041310
(index === selectedSuggestionIndex ? " selected" : "");
1311+
item.setAttribute("role", "option");
1312+
item.id = "search-option-" + index;
1313+
if (index === selectedSuggestionIndex) {
1314+
item.setAttribute("aria-selected", "true");
1315+
}
13051316

13061317
var iconBox = document.createElement("div");
13071318
iconBox.className = "dropdown-item-icon";
@@ -1343,8 +1354,18 @@ document.addEventListener('DOMContentLoaded', () => {
13431354
if (!resultsList) return;
13441355
var items = resultsList.querySelectorAll(".dropdown-item");
13451356
items.forEach(function (item, i) {
1346-
item.classList.toggle("selected", i === selectedSuggestionIndex);
1357+
var isSelected = i === selectedSuggestionIndex;
1358+
item.classList.toggle("selected", isSelected);
1359+
item.setAttribute("aria-selected", isSelected ? "true" : "false");
13471360
});
1361+
// Update aria-activedescendant on the search input
1362+
if (searchInput) {
1363+
if (selectedSuggestionIndex >= 0 && selectedSuggestionIndex < items.length) {
1364+
searchInput.setAttribute("aria-activedescendant", "search-option-" + selectedSuggestionIndex);
1365+
} else {
1366+
searchInput.removeAttribute("aria-activedescendant");
1367+
}
1368+
}
13481369
}
13491370

13501371
function selectSuggestion(title) {
@@ -1437,13 +1458,52 @@ document.addEventListener('DOMContentLoaded', () => {
14371458

14381459
searchInput.addEventListener("focus", function () {
14391460
if (searchDropdown) searchDropdown.classList.add("active");
1461+
searchInput.setAttribute("aria-expanded", "true");
14401462
if (!currentSearchQuery) renderRecentSearches();
14411463
});
14421464

14431465
searchInput.addEventListener("keydown", function (e) {
14441466
if (e.key === "Escape") {
14451467
closeDropdown();
14461468
searchInput.blur();
1469+
return;
1470+
}
1471+
1472+
// Arrow key navigation within search suggestions
1473+
var items = resultsList
1474+
? resultsList.querySelectorAll(".dropdown-item")
1475+
: [];
1476+
if (items.length === 0) return;
1477+
1478+
if (e.key === "ArrowDown") {
1479+
e.preventDefault();
1480+
selectedSuggestionIndex =
1481+
selectedSuggestionIndex < items.length - 1
1482+
? selectedSuggestionIndex + 1
1483+
: 0;
1484+
updateSuggestionHighlight();
1485+
// Scroll the selected item into view within the dropdown
1486+
if (items[selectedSuggestionIndex]) {
1487+
items[selectedSuggestionIndex].scrollIntoView({ block: "nearest" });
1488+
}
1489+
} else if (e.key === "ArrowUp") {
1490+
e.preventDefault();
1491+
selectedSuggestionIndex =
1492+
selectedSuggestionIndex > 0
1493+
? selectedSuggestionIndex - 1
1494+
: items.length - 1;
1495+
updateSuggestionHighlight();
1496+
if (items[selectedSuggestionIndex]) {
1497+
items[selectedSuggestionIndex].scrollIntoView({ block: "nearest" });
1498+
}
1499+
} else if (e.key === "Enter") {
1500+
e.preventDefault();
1501+
if (
1502+
selectedSuggestionIndex >= 0 &&
1503+
selectedSuggestionIndex < items.length
1504+
) {
1505+
items[selectedSuggestionIndex].click();
1506+
}
14471507
}
14481508
});
14491509
}

0 commit comments

Comments
 (0)