# All files / src/components/settings SettingsNav.tsx

86.84% Statements 33/38
88% Branches 22/25
62.5% Functions 5/8
88.88% Lines 32/36

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

    5x

    31x
    31x
    31x

    31x
    132x
    132x

    31x

    31x

    16x
    16x

    27x

    16x
    16x

    16x
    8x
    8x
    8x
    2x
    2x
    6x
    3x
    3x
    3x
    3x
    3x

    16x
    16x
    16x

    31x

    186x
    186x

     import { useCallback, useRef, type JSX, type KeyboardEvent, type ReactNode } from "react";
    import { useLocation } from "react-router";
    import { Github, Info, Key, Keyboard, Palette, Puzzle } from "lucide-react";
    import { SETTINGS_URL, useAppNavigate } from "../../utils/navigation.js";
    import { ICON_LG } from "../../utils/iconSize.js";
    import styles from "./SettingsNav.module.scss";

    /** Tab definition for the settings navigation rail. */
    interface SettingsTab {
      /** URL path segment (appended to /settings/). */
      path: string;
      /** Display label for the tab. */
      label: string;
      /** Icon element displayed before the label. */
      icon: ReactNode;
    }

    /** Ordered list of settings tabs. */
    const TABS: SettingsTab[] = [
      { path: "credentials", label: "Credentials", icon: <Key size={ICON_LG} /> },
      { path: "github-accounts", label: "GitHub Accounts", icon: <Github size={ICON_LG} /> },
      { path: "appearance", label: "Appearance", icon: <Palette size={ICON_LG} /> },
      { path: "shortcuts", label: "Shortcuts", icon: <Keyboard size={ICON_LG} /> },
      { path: "plugins", label: "Plugins", icon: <Puzzle size={ICON_LG} /> },
      { path: "about", label: "About", icon: <Info size={ICON_LG} /> },
    ];

    /** Vertical tab navigation rail for the settings hub. */
    export function SettingsNav(): JSX.Element {
      const location = useLocation();
      const navigate = useAppNavigate();
      const tabListRef = useRef<HTMLElement>(null);

      const activeTab =
        TABS.find((tab) => {
          const tabPath = `${SETTINGS_URL}/${tab.path}`;
          return location.pathname === tabPath || location.pathname.startsWith(`${tabPath}/`);
        })?.path ?? TABS[0].path;

      const handleClick = useCallback(
        (path: string) => {
          navigate(`${SETTINGS_URL}/${path}`);
        },
        [navigate],
      );

      const handleKeyDown = useCallback(
        (e: KeyboardEvent<HTMLDivElement>) => {
          // Derive current index from the focused element rather than location
          // to avoid stale closures during rapid keyboard navigation.
          const buttons = tabListRef.current?.querySelectorAll<HTMLButtonElement>('[role="tab"]');
          Iif (!buttons) {
            return;
          }
          const focusedIndex = Array.from(buttons).findIndex((b) => b === document.activeElement);
          const currentIndex =
            focusedIndex >= 0 ? focusedIndex : TABS.findIndex((t) => t.path === activeTab);
          let nextIndex = currentIndex;

          if (e.key === "ArrowDown" || e.key === "j" || e.key === "J") {
            e.preventDefault();
            nextIndex = (currentIndex + 1) % TABS.length;
          } else if (e.key === "ArrowUp" || e.key === "k" || e.key === "K") {
            e.preventDefault();
            nextIndex = (currentIndex - 1 + TABS.length) % TABS.length;
          } else if (e.key === "Home") {
            e.preventDefault();
            nextIndex = 0;
          } else if (e.key === "End") {
            e.preventDefault();
            nextIndex = TABS.length - 1;
          } else E{
            return;
          }

          const nextPath = TABS[nextIndex].path;
          navigate(`${SETTINGS_URL}/${nextPath}`);
          buttons[nextIndex]?.focus(); // eslint-disable-line @typescript-eslint/no-unnecessary-condition -- index may be out of bounds
        },
        [activeTab, navigate],
      );

      return (
        <nav
          className={styles.nav}
          ref={tabListRef}
          role="tablist"
          aria-orientation="vertical"
          aria-label="Settings"
          onKeyDown={handleKeyDown}
        >
          {TABS.map((tab) => {
            const isActive = tab.path === activeTab;
            return (
              <button
                key={tab.path}
                role="tab"
                type="button"
                aria-selected={isActive}
                tabIndex={isActive ? 0 : -1}
                className={`${styles.tab} ${isActive ? styles.tabActive : ""}`}
                onClick={() => handleClick(tab.path)}
              >
                <span className={styles.tabIcon} aria-hidden="true">
                  {tab.icon}
                </span>
                {tab.label}
              </button>
            );
          })}
        </nav>
      );
    }