Skip to content

feat: lvt-fx:viewport-report directive for read-progress reporting#144

Open
adnaan wants to merge 1 commit into
mainfrom
viewport-report-directive
Open

feat: lvt-fx:viewport-report directive for read-progress reporting#144
adnaan wants to merge 1 commit into
mainfrom
viewport-report-directive

Conversation

@adnaan

@adnaan adnaan commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Adds a debounced viewport-report directive so a scroll container can report which tracked descendants are visible back to the server — the primitive behind prereview's read-progress feature (livetemplate/prereview#128).

What

lvt-fx:viewport-report="<action>" on a scroll container reports, debounced, the data-key of the topmost and bottommost tracked descendant currently intersecting the container's viewport:

send({ action, data: { topKey, bottomKey } })
  • Tracked descendants are chosen by data-lvt-viewport-items (a CSS selector, default "[data-key]"); each must carry a data-key.
  • Fires on scroll (250ms debounce), once on attach (the initial viewport), and on resize.
  • Suppresses reports while the visible extremes are unchanged.

Design: deliberately a dumb reporter

It emits keys only — all logic (read-range accumulation, high-water mark, restore-target selection) lives server-side in the consumer. This keeps it a general primitive and, crucially, makes the behaviour unit-testable without a browser (the consumer feeds synthetic report actions to its controller). Armed once per container (idempotent re-arm), swept on disconnect / attribute removal — mirrors the existing text-select / area-select directive lifecycle.

Tests

tests/viewport-report.test.ts (5 cases): reports the top/bottom visible keys; suppresses unchanged repeats; reports again on a real range change; idempotent re-arm (no duplicate listeners); teardown stops reporting. Geometry is mocked (jsdom has no layout); the live scroll is covered end-to-end by the prereview chromedp test.

Full suite: 38 suites / 800 tests green.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ey41dRwDpRgLtVHZjhYLKt

Adds a debounced viewport-report directive: on a scroll container,
`lvt-fx:viewport-report="<action>"` reports the data-key of the topmost and
bottommost tracked descendant (chosen by data-lvt-viewport-items, default
"[data-key]") currently intersecting the container's viewport, via
send({action, data:{topKey, bottomKey}}).

Deliberately a dumb reporter — it emits keys only; all read-range accumulation,
high-water-mark and restore logic live server-side, which keeps it a general
primitive and makes the consuming behaviour unit-testable without a browser.
Reports on scroll (250ms debounce), once on attach (initial viewport), and on
resize; suppresses reports while the visible extremes are unchanged. Armed once
per container (idempotent re-arm), swept on disconnect / attribute removal —
mirrors the text-select / area-select directive lifecycle.

Consumed by prereview's read-progress feature (livetemplate/prereview#128).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ey41dRwDpRgLtVHZjhYLKt
@claude

claude Bot commented Jul 6, 2026

Copy link
Copy Markdown

Reviewed the viewport-report directive. Solid design (dumb reporter, logic server-side) and good debounce/idempotent-rearm test coverage, but there's one real gap:

Stale reports after DOM re-render (likely bug)

Every other stateful directive in this file re-syncs on each render even when an element is already armed — e.g. handleTextSelectDirectives/handleRegionSelectDirectives call existing.sync() to redraw/recompute after morphdom mutates the DOM (dom/directives.ts:2784, :2979). handleViewportReportDirectives doesn't do this — when an existing entry matches (existing.action === action), it only calls existing.updateSend(send) and never recomputes (dom/directives.ts:2900-2904).

Since this is explicitly the primitive behind a live-updating read-progress feature, the tracked container's content will routinely change via server-pushed re-renders (new items inserted/removed/reordered) without any scroll/resize DOM event firing. In that case lastTop/lastBottom (and the visible extremes reported to the server) go stale until the user happens to scroll or resize the window next. Worth calling compute() (or equivalent) on rearm, the same way the other directives call sync().

The test suite doesn't cover this case either (all 5 tests only vary via scroll events with fixed DOM element counts) — would be good to add a test that mutates the tracked children between two handleViewportReportDirectives calls with no scroll in between.

Everything else (cleanup/teardown on disconnect, attribute-removal sweep, debounce, idempotent re-arm) looks correct and consistent with the existing area-select/text-select directive patterns.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant