Skip to content
Coastal Healthcare Advocates

Source: style-guide.js

/**
 * @file Marks the contents link for the section being read, on the style guide
 * and the CSS reference.
 *
 * Both pages put a numbered contents list beside one long reading column. As
 * the reader scrolls, the link for the section crossing the reading line (a
 * quarter of the way down the window) gets `aria-current="true"`, which the
 * stylesheet draws as full-strength ink and the nav's Sky underline.
 *
 * ## Authored markup
 *
 * ```html
 * <nav data-toc aria-label="Contents">
 *   <ol><li><a href="#palette">Palette</a></li>…</ol>
 * </nav>
 * <section id="palette">…</section>
 * ```
 *
 * The list works as plain in-page links without this script; nothing is hidden
 * or moved. The hook is a data attribute, not a class, because Tailwind scans
 * `src/js/` for both stylesheets and a class named here would pull the guide's
 * rules into the stylesheet every visitor loads.
 *
 * ## Loading
 *
 * A module (`<script type="module">`) at the end of the two pages. The site's
 * Content-Security-Policy allows same-origin scripts only, so this can't be an
 * inline script.
 *
 * @module style-guide
 */

/**
 * The contents links that point at a section of this page.
 * @type {HTMLAnchorElement[]}
 */
const links = Array.from(document.querySelectorAll('[data-toc] a[href^="#"]'));

if (links.length && 'IntersectionObserver' in window) {
  /**
   * Contents links by the id of the section they point at, in page order.
   * @type {Map<string, HTMLAnchorElement>}
   */
  const byId = new Map(links.map((a) => [a.getAttribute('href').slice(1), a]));

  /**
   * Ids of the sections crossing the reading line right now.
   * @type {Set<string>}
   */
  const onLine = new Set();

  /**
   * Moves `aria-current` to the first section on the reading line. When the
   * line sits in the gap between two sections, the last mark stays.
   *
   * @param {IntersectionObserverEntry[]} entries Sections that crossed the line.
   * @returns {void}
   */
  const mark = (entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) onLine.add(entry.target.id);
      else onLine.delete(entry.target.id);
    });
    const current = [...byId.keys()].find((id) => onLine.has(id));
    if (!current) return;
    links.forEach((a) => a.removeAttribute('aria-current'));
    byId.get(current).setAttribute('aria-current', 'true');
  };

  // A zero-height band a quarter of the way down the window: the reading line.
  const observer = new IntersectionObserver(mark, { rootMargin: '-25% 0px -75% 0px' });
  byId.forEach((link, id) => {
    const section = document.getElementById(id);
    if (section) observer.observe(section);
  });
}