All files / src/components/layout Sidebar.tsx

100% Statements 32/32
100% Branches 11/11
100% Functions 6/6
100% Lines 32/32

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        2x   2x   2x   2x       3x 3x 3x 1x 1x 1x           2x         2x 2x                           3x 3x     3x 3x 3x 1x     2x 2x 2x   2x 2x 2x 2x           2x 2x 1x       3x 1x     2x            
import { useState, useRef, useEffect, type JSX, type ReactNode } from "react";
import styles from "./Sidebar.module.scss";
 
/** Default sidebar width in pixels. */
const DEFAULT_SIDEBAR_WIDTH: number = 320;
/** Minimum sidebar width in pixels. */
const MIN_SIDEBAR_WIDTH: number = 220;
/** Maximum sidebar width in pixels. */
const MAX_SIDEBAR_WIDTH: number = 600;
/** localStorage key for persisted sidebar width. */
const STORAGE_KEY: string = "grackle-sidebar-width";
 
/** Read persisted sidebar width from localStorage, falling back to the default. */
function loadWidth(): number {
  try {
    const stored = localStorage.getItem(STORAGE_KEY);
    if (stored !== null) {
      const parsed = Number(stored);
      if (Number.isFinite(parsed) && parsed >= MIN_SIDEBAR_WIDTH && parsed <= MAX_SIDEBAR_WIDTH) {
        return parsed;
      }
    }
  } catch {
    // localStorage unavailable
  }
  return DEFAULT_SIDEBAR_WIDTH;
}
 
/** Persist sidebar width to localStorage. */
function saveWidth(width: number): void {
  try {
    localStorage.setItem(STORAGE_KEY, String(width));
  } catch {
    // localStorage unavailable
  }
}
 
/** Props for the Sidebar component. */
export interface SidebarProps {
  /** Content to render inside the sidebar slot. When undefined, the sidebar is hidden. */
  content: ReactNode | undefined;
}
 
/** Left sidebar rendering slot content passed via props. */
export function Sidebar({ content }: SidebarProps): JSX.Element | undefined {
  const [width] = useState<number>(loadWidth);
  const containerRef = useRef<HTMLDivElement>(null);
 
  /** Observe container resizes and persist width to localStorage. */
  useEffect(() => {
    const element = containerRef.current;
    if (!element) {
      return;
    }
 
    const observer = new ResizeObserver((entries) => {
      for (const entry of entries) {
        const borderBox = entry.borderBoxSize[0];
        // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- borderBoxSize[0] may be undefined in some browsers
        if (borderBox) {
          const boxWidth = Math.round(borderBox.inlineSize);
          if (boxWidth >= MIN_SIDEBAR_WIDTH && boxWidth <= MAX_SIDEBAR_WIDTH) {
            saveWidth(boxWidth);
          }
        }
      }
    });
 
    observer.observe(element);
    return () => {
      observer.disconnect();
    };
  }, []);
 
  if (content === undefined) {
    return undefined;
  }
 
  return (
    <div className={styles.container} ref={containerRef} data-testid="sidebar" style={{ width }}>
      <div className={styles.content}>{content}</div>
    </div>
  );
}