@@ -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
595599document . 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+ }
0 commit comments