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>
  );
}