All files / src/hooks useKeyboardNavigation.ts

89.28% Statements 25/28
83.87% Branches 26/31
100% Functions 4/4
89.28% Lines 25/28

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96                                                        22x     16x 16x 2x     14x           6x 6x 4x   6x           4x 4x 2x   4x       2x 2x         1x 1x       1x     1x 1x       1x                 22x 22x 22x 22x        
/**
 * useKeyboardNavigation Hook
 * Story 10.8a: Moderator Presentation Page — Functional
 *
 * Handles keyboard/remote navigation for the moderator presentation page.
 *
 * ACs: #2–6, #23–24
 */
 
import { useEffect, useCallback } from 'react';
 
interface UseKeyboardNavigationOptions {
  sectionCount: number;
  currentIndex: number;
  isBlankActive: boolean;
  onNext: () => void;
  onPrev: () => void;
  onToggleBlank: () => void;
}
 
export function useKeyboardNavigation({
  sectionCount,
  currentIndex,
  isBlankActive,
  onNext,
  onPrev,
  onToggleBlank,
}: UseKeyboardNavigationOptions): void {
  const handleKeyDown = useCallback(
    (e: KeyboardEvent) => {
      // Guard: ignore key events from input/textarea elements
      const target = e.target as HTMLElement;
      if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
        return;
      }
 
      switch (e.key) {
        case 'ArrowRight':
        case 'ArrowDown':
        case 'PageDown':
        case ' ':
          // Prevent default scroll for these keys
          e.preventDefault();
          if (!isBlankActive && currentIndex < sectionCount - 1) {
            onNext();
          }
          break;
 
        case 'ArrowLeft':
        case 'ArrowUp':
        case 'PageUp':
          // Prevent default scroll for these keys
          e.preventDefault();
          if (!isBlankActive && currentIndex > 0) {
            onPrev();
          }
          break;
 
        case 'b':
        case 'B':
          onToggleBlank();
          break;
 
        case 'f':
        case 'F':
          // AC #5: F toggles fullscreen
          if (!document.fullscreenElement) {
            void document.documentElement.requestFullscreen();
          } else E{
            void document.exitFullscreen();
          }
          break;
 
        case 'Escape':
          if (isBlankActive) {
            onToggleBlank();
          } else if (Edocument.fullscreenElement) {
            // Escape exits fullscreen natively; no extra handling needed
          }
          break;
 
        default:
          break;
      }
    },
    [sectionCount, currentIndex, isBlankActive, onNext, onPrev, onToggleBlank]
  );
 
  useEffect(() => {
    window.addEventListener('keydown', handleKeyDown);
    return () => {
      window.removeEventListener('keydown', handleKeyDown);
    };
  }, [handleKeyDown]);
}