{
  "tag": "ed-scrollspy",
  "kind": "component",
  "summary": "Tracks the user's scroll position through a long page and highlights the corresponding entry in a vertical \"on this page\" rail. Two-level hierarchy with declarative or auto-discovered items.",
  "status": "experimental",
  "level": "molecule",
  "since": "1.0",
  "docs": "https://test-apps.eurodata.de/a/eurodata-ui/docs/components/scrollspy/",
  "usage": "https://test-apps.eurodata.de/a/eurodata-ui/docs/components/scrollspy/usage/",
  "playwright": "getByTestId('…')",
  "dependencies": [
    "ed-scrollspy-item"
  ],
  "use": [
    "Long, scrollable single pages — long-form articles, documentation, reports, or a long detail view with several distinct sections",
    "Readers who need to skip to a specific section and to always know which section they are currently in",
    "A sticky \"on this page\" outline that follows reading position, so a long document never leaves the reader lost"
  ],
  "notFor": [
    {
      "when": "Switching between separate views or panels that are not all on one scrolling page",
      "instead": "[Tabs](https://test-apps.eurodata.de/a/eurodata-ui/docs/components/tab-group)"
    },
    {
      "when": "Primary application navigation between areas, modules, or records",
      "instead": "[Side Menu](https://test-apps.eurodata.de/a/eurodata-ui/docs/components/side-menu)"
    },
    {
      "when": "A short page that fits on a screen or two and needs no on-page index",
      "instead": "plain headings with no scrollspy"
    }
  ],
  "rules": [
    {
      "id": "scrollspy/secondary-aid",
      "rule": "Use it as a secondary, in-page orientation aid for a single long page only — never as product navigation, and never in place of the [Side Menu](https://test-apps.eurodata.de/a/eurodata-ui/docs/components/side-menu). Show it only on pages long enough that a reader genuinely needs help finding and tracking sections."
    },
    {
      "id": "scrollspy/sticky",
      "rule": "Keep the index visible (sticky) beside the content, so the reader can see their position at any scroll depth — never let it scroll away with the content. Inside the app shell, the aside slot is made for exactly this and sticks on its own.",
      "why": "A reader who has scrolled deep must still see it.",
      "check": "error"
    },
    {
      "id": "scrollspy/bounded-height",
      "rule": "Bound the index's height and let it scroll on its own when the outline can grow longer than the viewport — on the index itself, on a box around it, or on the app shell's aside slot. Don't add your own scrolling to keep the current entry in view: the index keeps it inside the visible slice as the reader moves down the page, wherever the bound sits.",
      "why": "A sticky index stays put, so entries past the bottom edge are unreachable otherwise.",
      "check": "error"
    },
    {
      "id": "scrollspy/headings",
      "rule": "Give every section a clear, descriptive heading, stable and linkable — never vague or duplicated headings that make index entries indistinguishable.",
      "why": "The index is only as useful as the headings it mirrors, and it does not work against sections that have no stable, linkable heading."
    },
    {
      "id": "scrollspy/two-levels",
      "rule": "Keep the structure to two levels, sections and their sub-sections — never nest the index more than two levels deep.",
      "check": "error"
    },
    {
      "id": "scrollspy/outline",
      "rule": "Keep the built-in default expand behavior, which reveals a section's sub-sections as the reader reaches that section, to keep a long index compact. Show the full outline up front instead when the page is shorter and the reader benefits from seeing everything at once, and offer a top-level-only index when sub-sections would make the rail too busy."
    },
    {
      "id": "scrollspy/offset",
      "rule": "Adjust the point at which a section counts as \"current\" when a sticky page header overlaps the top of the content."
    },
    {
      "id": "scrollspy/side-menu-spacing",
      "rule": "Match the index's row spacing to an adjacent [Side Menu](https://test-apps.eurodata.de/a/eurodata-ui/docs/components/side-menu), so the two line up visually.",
      "check": "warning"
    }
  ],
  "api": {
    "attributes": [
      {
        "name": "for",
        "property": "for",
        "type": "string",
        "default": "''",
        "description": "CSS selector for the article whose headings should be auto-discovered.\nSlot children take precedence — when both are present, auto-discovery is silently skipped."
      },
      {
        "name": "headings",
        "property": "headings",
        "type": "string",
        "default": "'h2, h3'",
        "description": "CSS selector for the headings to scan inside the `for=` target. The first match becomes a level-1 item, the second a level-2 child."
      },
      {
        "name": "expand",
        "property": "expand",
        "type": "'active' | 'all'",
        "default": "'active'",
        "description": "Children-display mode.\n- `'active'` — level-2 children visible only under the active level-1 parent.\n- `'all'` — all level-2 children always visible.\n\nTo get an L1-only rail, narrow the scan with `headings=\"h2\"` instead — that way\nthe spy never tracks h3s, so scrolling past one can't steal the active state from\nits parent."
      },
      {
        "name": "show-counts",
        "property": "showCounts",
        "type": "boolean",
        "default": "false",
        "description": "When true, parent items render a child-count badge with their level-2 child count."
      },
      {
        "name": "label",
        "property": "label",
        "type": "string",
        "default": "''",
        "description": "Sets `aria-label` on the inner `<nav>`. Defaults to a localized \"On this page\"."
      },
      {
        "name": "density",
        "property": "density",
        "type": "'compact' | 'default' | 'spacious'",
        "default": "'default'",
        "description": "Density of the rail items. Match this to an adjacent [ed-side-menu](https://test-apps.eurodata.de/a/eurodata-ui/docs/components/side-menu)\nso the two line up — a compact side-menu next to a default scrollspy reads as mismatched.\n- `spacious` — relaxed rows (~52px).\n- `default` — condensed rows (~40px).\n- `compact` — minimal rows (`--ed-form-control-height`, ~32px)."
      },
      {
        "name": "active-id",
        "property": "activeId",
        "type": "string",
        "default": "''",
        "description": "Currently active heading id. Reflected for CSS hooks; consumers should treat as\nread-only outside tests."
      },
      {
        "name": "offset",
        "property": "offset",
        "type": "string",
        "default": "'25%'",
        "description": "Probe-line position from the scroll region's top (the article's scroll container when\nthe article is `overflow:auto`/`scroll`, otherwise the viewport). CSS length string\n(default `'25%'` — a quarter into the scroll region).\n\n`%` is interpreted relative to the scroll region's height; `vh`/`vw` always refer to\nthe viewport per CSS convention; `px`/`rem`/`em` are absolute. Override when a sticky\npage header consumes part of the scroll region."
      },
      {
        "name": "dir",
        "property": "dir",
        "type": "string"
      },
      {
        "name": "lang",
        "property": "lang",
        "type": "string"
      }
    ],
    "properties": [],
    "methods": [
      {
        "name": "recomputeActive",
        "description": "Public test hook: synchronously recompute active item from current scroll position.\nSide-menu's `setCurrent()` is the precedent for an externally-callable sync escape hatch."
      },
      {
        "name": "scrollToId",
        "parameters": [
          {
            "name": "id",
            "type": "string"
          }
        ],
        "description": "Smoothly scroll the named heading to the top of the scroll region and sync the URL hash.\n\nThe heading lands at the region's top: for the window (the real page case) that means\nscroll-margin-top below the viewport top — i.e. just under any sticky header — and for a\nnested overflow container (e.g. the docs demo) the container's top edge.\n\nThe clicked heading is set active only when the scroll can actually bring it to the top.\nAt the document end, where it can't, we leave the active state to the probe so the heading\nactually shown at the top stays highlighted — clicking the last item repeatedly no longer\nforces it active."
      }
    ],
    "events": [
      {
        "name": "ed-scrollspy-change",
        "react": "onEdScrollspyChange",
        "type": "{ activeId: string, previousId: string | null }",
        "description": "Fired when the active heading changes."
      }
    ],
    "slots": [
      {
        "name": "",
        "description": "The default slot for `<ed-scrollspy-item>` elements when authoring declaratively."
      }
    ],
    "cssProperties": [],
    "parts": [
      {
        "name": "base",
        "description": "The inner `<nav>` element."
      },
      {
        "name": "list",
        "description": "The top-level `<ul>` rendered for auto-discovered items."
      }
    ],
    "states": []
  },
  "import": "import '@eurodata/eurodata-ui/dist/components/scrollspy/scrollspy.js';",
  "react": {
    "component": "EdScrollspy",
    "import": "import EdScrollspy from '@eurodata/eurodata-ui/dist/react/scrollspy/index.js';"
  }
}
