Skip to the content

Capsid menu

components/anchor-bar/anchor-bar.react.tsx

77 lines and 15 functions in DrDustinEdwards/capsomer, at commit d3966cc on 2026-10-24. This file on GitHub.

Functions

The functions of anchor-bar.react.tsx
FunctionLinesComplexityCoveredCRAPMutation
AnchorBar12 to 821No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:16>16 to 161No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:20>20 to 394No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
headings24 to 241No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:24>24 to 241No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:24>24 to 241No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
apply26 to 262No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:34>34 to 371No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:42>42 to 553No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:46>46 to 461No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:46>46 to 461No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:49>49 to 491No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:51>51 to 541No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:63>63 to 782No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:68>68 to 736No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet

Imports

It imports components/anchor-bar/anchor-bar.ts. No module of this repo imports it.

Source

import { useEffect, useRef, useState } from "react";
import { goToSection, markSections, measureBar, meetsRule, scrollParent, watchSections } from "./anchor-bar.ts";

export interface AnchorBarProps {
  // The view's sections, in order; each id is the id of the block or h2 it links to.
  sections: Array<{ id: string; label: string }>;
  // Show the bar even when the view is short or has fewer than three sections.
  always?: boolean;
  label?: string;
}

export function AnchorBar({ sections, always = false, label = "On this page" }: AnchorBarProps) {
  const nav = useRef<HTMLElement>(null);
  const [shown, setShown] = useState(always);
  const [current, setCurrent] = useState<string | null>(null);
  const ids = sections.map((s) => s.id).join(" ");

  // The rule, measured now and again whenever the window or the content changes size, so a
  // view that fills in after it loads gets its bar when it earns one (the Portal's Anchors).
  useEffect(() => {
    const el = nav.current;
    if (!el) return;
    const scroller = scrollParent(el);
    const headings = () => sections.map((s) => document.getElementById(s.id)).filter((h): h is HTMLElement => h !== null);
    // The bar's own height does not count, or showing it could tip the rule.
    const apply = () => setShown(always || meetsRule(headings(), scroller, el.offsetHeight));
    apply();
    const ro = new ResizeObserver(apply);
    ro.observe(scroller ?? document.documentElement);
    ro.observe(el);
    const scope = el.closest("main") ?? document.body;
    const mo = new MutationObserver(apply);
    mo.observe(scope, { childList: true, subtree: true, attributes: true, attributeFilter: ["data-section", "id", "hidden"] });
    return () => {
      ro.disconnect();
      mo.disconnect();
    };
    // The section ids identify what to watch.
  }, [ids, always]);

  // While the bar shows: stop each section below it, and mark the current one.
  useEffect(() => {
    const el = nav.current;
    if (!el || !shown) return;
    const scroller = scrollParent(el);
    const headings = sections.map((s) => document.getElementById(s.id)).filter((h): h is HTMLElement => h !== null);
    markSections(headings);
    // After the next frame, once the bar is shown and has a height.
    const frame = requestAnimationFrame(() => measureBar(el, scroller));
    const stop = watchSections(headings, setCurrent, scroller);
    return () => {
      stop();
      cancelAnimationFrame(frame);
    };
  }, [ids, shown]);

  return (
    <nav ref={nav} className="cap-anchors" aria-label={label} data-cap="anchor-bar" hidden={!shown}>
      <span className="cap-anchors-label" aria-hidden="true">
        {label}
      </span>
      <ul className="cap-anchors-list">
        {sections.map((s) => (
          <li key={s.id}>
            <a
              href={`#${s.id}`}
              aria-current={current === s.id ? "location" : undefined}
              onClick={(e) => {
                const h = document.getElementById(s.id);
                if (!h || e.ctrlKey || e.metaKey || e.shiftKey || e.button !== 0) return;
                e.preventDefault();
                goToSection(h);
              }}
            >
              {s.label}
            </a>
          </li>
        ))}
      </ul>
    </nav>
  );
}