Skip to content

Commit 4825e73

Browse files
committed
Enh(viewer): Navigate in picker with keyboard shortcuts
1 parent 3063527 commit 4825e73

3 files changed

Lines changed: 237 additions & 0 deletions

File tree

codeclash/viewer/static/css/picker.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,18 @@
125125
border-left: 3px solid var(--accent-color);
126126
}
127127

128+
.game-row.keyboard-selected {
129+
background-color: var(--bg-primary);
130+
border-left: 4px solid var(--accent-color);
131+
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
132+
}
133+
134+
.game-row.keyboard-selected:hover {
135+
background-color: var(--bg-primary);
136+
border-left: 4px solid var(--accent-hover);
137+
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3);
138+
}
139+
128140
.game-folder {
129141
cursor: pointer;
130142
}

codeclash/viewer/static/js/picker.js

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,15 +591,24 @@ document
591591
}
592592
});
593593

594+
// Keyboard navigation state
595+
let currentSelectedIndex = -1;
596+
let allNavigableRows = [];
597+
594598
// Initialize functionality on page load
595599
document.addEventListener("DOMContentLoaded", function () {
596600
console.log("Game Picker initialized");
597601
console.log("Available keyboard shortcuts:");
598602
console.log(" Shift + Click: Range select checkboxes");
599603
console.log(" Escape: Close move dialog");
604+
console.log(" Arrow keys / hjkl: Navigate");
605+
console.log(" Enter: Open selected game");
600606

601607
// Collapse all folders by default
602608
collapseAllFolders();
609+
610+
// Initialize keyboard navigation
611+
initializeKeyboardNavigation();
603612
});
604613

605614
// Track individual folder states
@@ -624,3 +633,200 @@ function collapseAllFolders() {
624633

625634
console.log(`Collapsed ${folderRows.length} folders on startup`);
626635
}
636+
637+
// Keyboard Navigation Functions
638+
function initializeKeyboardNavigation() {
639+
updateNavigableRows();
640+
641+
// Add keyboard event listener
642+
document.addEventListener("keydown", handleKeyboardNavigation);
643+
644+
// Select first visible row by default
645+
if (allNavigableRows.length > 0) {
646+
setSelectedRow(0);
647+
}
648+
}
649+
650+
function updateNavigableRows() {
651+
// Get all visible rows (both games and folders)
652+
allNavigableRows = Array.from(document.querySelectorAll(".game-row")).filter(
653+
(row) => row.style.display !== "none",
654+
);
655+
}
656+
657+
function setSelectedRow(index) {
658+
// Remove previous selection
659+
if (
660+
currentSelectedIndex >= 0 &&
661+
currentSelectedIndex < allNavigableRows.length
662+
) {
663+
allNavigableRows[currentSelectedIndex].classList.remove(
664+
"keyboard-selected",
665+
);
666+
}
667+
668+
// Set new selection
669+
currentSelectedIndex = index;
670+
if (
671+
currentSelectedIndex >= 0 &&
672+
currentSelectedIndex < allNavigableRows.length
673+
) {
674+
const selectedRow = allNavigableRows[currentSelectedIndex];
675+
selectedRow.classList.add("keyboard-selected");
676+
677+
// Scroll into view if needed
678+
selectedRow.scrollIntoView({
679+
behavior: "smooth",
680+
block: "nearest",
681+
});
682+
}
683+
}
684+
685+
function handleKeyboardNavigation(event) {
686+
// Don't handle keyboard navigation if a dialog is open or input is focused
687+
if (
688+
document.getElementById("move-dialog").style.display === "flex" ||
689+
document.activeElement.tagName === "INPUT" ||
690+
document.activeElement.tagName === "SELECT"
691+
) {
692+
return;
693+
}
694+
695+
// Don't handle if no navigable rows
696+
if (allNavigableRows.length === 0) {
697+
return;
698+
}
699+
700+
let handled = false;
701+
702+
switch (event.key) {
703+
case "ArrowUp":
704+
case "k":
705+
event.preventDefault();
706+
navigateUp();
707+
handled = true;
708+
break;
709+
710+
case "ArrowDown":
711+
case "j":
712+
event.preventDefault();
713+
navigateDown();
714+
handled = true;
715+
break;
716+
717+
case "ArrowLeft":
718+
case "h":
719+
event.preventDefault();
720+
navigateLeft();
721+
handled = true;
722+
break;
723+
724+
case "ArrowRight":
725+
case "l":
726+
event.preventDefault();
727+
navigateRight();
728+
handled = true;
729+
break;
730+
731+
case "Enter":
732+
event.preventDefault();
733+
activateSelectedRow();
734+
handled = true;
735+
break;
736+
}
737+
738+
if (handled) {
739+
// Update navigable rows in case visibility changed
740+
updateNavigableRows();
741+
}
742+
}
743+
744+
function navigateUp() {
745+
if (currentSelectedIndex > 0) {
746+
setSelectedRow(currentSelectedIndex - 1);
747+
}
748+
}
749+
750+
function navigateDown() {
751+
if (currentSelectedIndex < allNavigableRows.length - 1) {
752+
setSelectedRow(currentSelectedIndex + 1);
753+
}
754+
}
755+
756+
function navigateLeft() {
757+
if (
758+
currentSelectedIndex >= 0 &&
759+
currentSelectedIndex < allNavigableRows.length
760+
) {
761+
const selectedRow = allNavigableRows[currentSelectedIndex];
762+
const folderPath = selectedRow.getAttribute("data-path");
763+
764+
// If it's a folder and it's expanded, collapse it
765+
if (selectedRow.classList.contains("intermediate-folder")) {
766+
const currentState = folderStates.get(folderPath) || "collapsed";
767+
if (currentState === "expanded") {
768+
toggleFolder(folderPath);
769+
updateNavigableRows();
770+
// Keep selection on the same row after collapse
771+
const newIndex = allNavigableRows.findIndex(
772+
(row) => row.getAttribute("data-path") === folderPath,
773+
);
774+
if (newIndex >= 0) {
775+
setSelectedRow(newIndex);
776+
}
777+
}
778+
}
779+
}
780+
}
781+
782+
function navigateRight() {
783+
if (
784+
currentSelectedIndex >= 0 &&
785+
currentSelectedIndex < allNavigableRows.length
786+
) {
787+
const selectedRow = allNavigableRows[currentSelectedIndex];
788+
const folderPath = selectedRow.getAttribute("data-path");
789+
790+
// If it's a folder and it's collapsed, expand it
791+
if (selectedRow.classList.contains("intermediate-folder")) {
792+
const currentState = folderStates.get(folderPath) || "collapsed";
793+
if (currentState === "collapsed") {
794+
toggleFolder(folderPath);
795+
updateNavigableRows();
796+
// Keep selection on the same row after expand
797+
const newIndex = allNavigableRows.findIndex(
798+
(row) => row.getAttribute("data-path") === folderPath,
799+
);
800+
if (newIndex >= 0) {
801+
setSelectedRow(newIndex);
802+
}
803+
}
804+
}
805+
}
806+
}
807+
808+
function activateSelectedRow() {
809+
if (
810+
currentSelectedIndex >= 0 &&
811+
currentSelectedIndex < allNavigableRows.length
812+
) {
813+
const selectedRow = allNavigableRows[currentSelectedIndex];
814+
const folderPath = selectedRow.getAttribute("data-path");
815+
816+
if (selectedRow.classList.contains("intermediate-folder")) {
817+
// Toggle folder
818+
toggleFolder(folderPath);
819+
updateNavigableRows();
820+
// Keep selection on the same row after toggle
821+
const newIndex = allNavigableRows.findIndex(
822+
(row) => row.getAttribute("data-path") === folderPath,
823+
);
824+
if (newIndex >= 0) {
825+
setSelectedRow(newIndex);
826+
}
827+
} else if (selectedRow.classList.contains("game-folder")) {
828+
// Open game
829+
openGame(folderPath);
830+
}
831+
}
832+
}

codeclash/viewer/templates/picker.html

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,25 @@ <h6><i class="bi bi-gear"></i> Interface</h6>
182182
</div>
183183
</div>
184184

185+
<div class="shortcut-section">
186+
<h6><i class="bi bi-keyboard"></i> Navigation</h6>
187+
<div class="shortcut-item">
188+
<kbd></kbd> / <kbd>k</kbd> <span>Navigate up</span>
189+
</div>
190+
<div class="shortcut-item">
191+
<kbd></kbd> / <kbd>j</kbd> <span>Navigate down</span>
192+
</div>
193+
<div class="shortcut-item">
194+
<kbd></kbd> / <kbd>h</kbd> <span>Collapse folder</span>
195+
</div>
196+
<div class="shortcut-item">
197+
<kbd></kbd> / <kbd>l</kbd> <span>Expand folder</span>
198+
</div>
199+
<div class="shortcut-item">
200+
<kbd>Enter</kbd> <span>Open game or toggle folder</span>
201+
</div>
202+
</div>
203+
185204
<div class="shortcut-section">
186205
<h6><i class="bi bi-mouse"></i> Mouse</h6>
187206
<div class="shortcut-item">

0 commit comments

Comments
 (0)