# All files / src/hooks useKeyboardNavigation.ts

94.82% Statements 55/58
85.36% Branches 35/41
93.33% Functions 14/15
95.74% Lines 45/47

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
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124

    39x

    39x

    4x
    4x

    3x
    6x
    3x
    3x

    3x
    3x

    3x
    6x

    3x

    3x
    3x
    2x

    1x

    13x
    13x
    12x
    13x
    8x
    5x

    7x

    264x
    264x
    264x

    44x
    39x

    44x
    44x
    44x
    44x
    44x
    44x

    44x
    19x
    19x
    13x
    13x
    7x
    7x
    3x

    19x
    19x

     import { useEffect, useMemo, useRef } from 'react'
    import { isTauri } from '../mock-tauri'
    import { filterEntries, sortByModified, buildRelationshipGroups } from '../utils/noteListHelpers'
    import type { VaultEntry, SidebarSelection } from '../types'

    interface Tab {
      entry: VaultEntry
      content: string
    }

    interface KeyboardNavigationOptions {
      tabs: Tab[]
      activeTabPath: string | null
      entries: VaultEntry[]
      selection: SidebarSelection
      allContent: Record<string, string>
      onSwitchTab: (path: string) => void
      onReplaceActiveTab: (entry: VaultEntry) => void
      onSelectNote: (entry: VaultEntry) => void
    }

    function computeVisibleNotes(
      entries: VaultEntry[],
      selection: SidebarSelection,
      allContent: Record<string, string>,
    ): VaultEntry[] {
      Iif (selection.kind === 'entity') {
        return buildRelationshipGroups(selection.entry, entries, allContent)
          .flatMap((g) => g.entries)
      }
      return [...filterEntries(entries, selection)].sort(sortByModified)
    }

    function navigateTab(
      tabsRef: React.RefObject<Tab[]>,
      activeTabPathRef: React.RefObject<string | null>,
      onSwitchTab: React.RefObject<(path: string) => void>,
      direction: 1 | -1,
    ) {
      const currentTabs = tabsRef.current!
      if (currentTabs.length === 0) return

      const currentPath = activeTabPathRef.current
      const currentIndex = currentTabs.findIndex((t) => t.entry.path === currentPath)
      const nextIndex = (currentIndex + direction + currentTabs.length) % currentTabs.length
      onSwitchTab.current!(currentTabs[nextIndex].entry.path)
    }

    function navigateNote(
      visibleNotesRef: React.RefObject<VaultEntry[]>,
      activeTabPathRef: React.RefObject<string | null>,
      onReplace: React.RefObject<(entry: VaultEntry) => void>,
      onSelect: React.RefObject<(entry: VaultEntry) => void>,
      direction: 1 | -1,
    ) {
      const notes = visibleNotesRef.current!
      Iif (notes.length === 0) return

      const currentPath = activeTabPathRef.current
      const currentIndex = notes.findIndex((n) => n.path === currentPath)

      const nextIndex = currentIndex === -1
        ? (direction === 1 ? 0 : notes.length - 1)
        : (currentIndex + direction + notes.length) % notes.length

      const nextNote = notes[nextIndex]
      if (currentPath) {
        onReplace.current!(nextNote)
      } else {
        onSelect.current!(nextNote)
      }
    }

    type ShortcutKind = 'tab' | 'note' | null

    function classifyShortcut(e: KeyboardEvent, inTauri: boolean): ShortcutKind {
      const mod = e.metaKey || e.ctrlKey
      if (!mod) return null
      const isTabShortcut = inTauri ? (e.altKey && !e.shiftKey) : (e.shiftKey && !e.altKey)
      if (isTabShortcut && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) return 'tab'
      if (e.altKey && !e.shiftKey && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) return 'note'
      return null
    }

    function arrowDirection(key: string): 1 | -1 {
      return (key === 'ArrowRight' || key === 'ArrowDown') ? 1 : -1
    }

    function useLatestRef<T>(value: T): React.RefObject<T> {
      const ref = useRef(value)
      useEffect(() => { ref.current = value })
      return ref
    }

    export function useKeyboardNavigation({
      tabs, activeTabPath, entries, selection, allContent,
      onSwitchTab, onReplaceActiveTab, onSelectNote,
    }: KeyboardNavigationOptions) {
      const visibleNotes = useMemo(
        () => computeVisibleNotes(entries, selection, allContent),
        [entries, selection, allContent],
      )

      const tabsRef = useLatestRef(tabs)
      const activeTabPathRef = useLatestRef(activeTabPath)
      const visibleNotesRef = useLatestRef(visibleNotes)
      const onSwitchTabRef = useLatestRef(onSwitchTab)
      const onReplaceRef = useLatestRef(onReplaceActiveTab)
      const onSelectNoteRef = useLatestRef(onSelectNote)

      useEffect(() => {
        const inTauri = isTauri()
        const handleKeyDown = (e: KeyboardEvent) => {
          const kind = classifyShortcut(e, inTauri)
          if (!kind) return
          e.preventDefault()
          if (kind === 'tab') navigateTab(tabsRef, activeTabPathRef, onSwitchTabRef, arrowDirection(e.key))
          else navigateNote(visibleNotesRef, activeTabPathRef, onReplaceRef, onSelectNoteRef, arrowDirection(e.key))
        }
        window.addEventListener('keydown', handleKeyDown)
        return () => window.removeEventListener('keydown', handleKeyDown)
      }, [tabsRef, activeTabPathRef, visibleNotesRef, onSwitchTabRef, onReplaceRef, onSelectNoteRef])
    }