All files / src/components TabBar.tsx

100% Statements 13/13
100% Branches 4/4
100% Functions 4/4
100% Lines 13/13

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                                                      1x                   6x 6x   6x     1x           11x 11x 11x                         11x 11x 11x 11x 11x                                            
import { type FC } from 'react';
import * as Tabs from '@radix-ui/react-tabs';
import type { FoldrRow } from '../Foldr.ts';
import { SheetFrame } from './SheetFrame.tsx';
import styles from '../styles.module.css';
 
export interface TabBarProps {
  readonly rows: readonly FoldrRow[];
  readonly activeIndex: number;
  readonly basePath: string;
  readonly suffix: string;
  readonly index: string;
  readonly onChange: (idx: number) => void;
  readonly firstFocusUsed: boolean;
  readonly onFirstFocus: () => void;
}
 
/**
 * Renders a Radix Tabs.Root with one tab per TOC row, plus one TabsContent
 * (iframe) per row. `activeIndex` drives `value`; clicking a trigger bubbles
 * back through `onChange(nextIndex)`.
 *
 * The iframe-per-tab rendering mirrors the legacy `Nav`/`Frame` component
 * split: every tab's frame is kept mounted (Radix TabsContent has
 * `forceMount` for this so the iframe state survives tab switches), and
 * `visibility: hidden` in CSS hides non-active ones.
 */
export const TabBar: FC<TabBarProps> = ({
  rows,
  activeIndex,
  basePath,
  suffix,
  index,
  onChange,
  firstFocusUsed,
  onFirstFocus,
}) => {
  const clampedIndex = Math.max(0, Math.min(activeIndex, rows.length - 1));
  const activeValue = `tab-${clampedIndex}`;
 
  return (
    <Tabs.Root
      value={activeValue}
      onValueChange={(v) => onChange(parseInt(v.replace(/^tab-/, ''), 10))}
      orientation="horizontal"
      activationMode="manual"
    >
      <Tabs.List className={styles['tabList']}>
        {rows.map((row, i) => {
          const title = row.title;
          const value = `tab-${i}`;
          return (
            <Tabs.Trigger
              key={title}
              value={value}
              className={styles['tabTitle']}
              aria-label={title}
            >
              <span className={styles['tabTitleButton']}>{title}</span>
            </Tabs.Trigger>
          );
        })}
      </Tabs.List>
      {rows.map((row, i) => {
        const title = row.title;
        const value = `tab-${i}`;
        const link = row.link || `/${encodeURIComponent(title)}`;
        const src = `${basePath}${link}${suffix}`;
        return (
          <Tabs.Content
            key={title}
            value={value}
            forceMount
            className={`${styles['tabItem']} ${styles['wrapper']}`}
            data-state={i === clampedIndex ? 'active' : 'inactive'}
          >
            <SheetFrame
              src={src}
              rows={rows}
              index={index}
              isFirst={i === 0}
              firstFocusUsed={firstFocusUsed}
              onFirstFocus={onFirstFocus}
            />
          </Tabs.Content>
        );
      })}
    </Tabs.Root>
  );
};