fix(teleprompter): make follow-lock tolerant and its state legible

Two problems were feeding the confusion:

- Any wheel, touch, or pointerdown event broke the follow, with no tolerance.
  A stray touch or trackpad momentum after a deliberate gesture disengaged it
  as readily as a real scroll away from the read position, with no way to
  tell the two apart from the outside. The operator view solved the same
  problem with a distance check against the loaded item's expected position,
  which this now mirrors: it takes a real scroll (past 1.5 lines) to count as
  taking over, in a new hasBrokenFollow(), unit tested like the rest of the
  scroll maths.

- The runtime flag and the view option shared adjacent, opposite-valence
  names (followLocked / followLoaded) and were ANDed together in a third
  place to get the value the button actually needed. Renamed the runtime
  flag to autoScrollLocked, matching the operator's own lockAutoScroll for
  the same concept, and moved the AND into the hook so it exposes one signal
  the view no longer has to assemble itself.

Also fixes the threshold check reading a stale position: a burst of wheel
events can fire faster than the animation frame that keeps the scroll ref in
sync, so it now reads the element's scrollTop directly rather than the ref a
frame behind it. Caught by testing the tolerance in a live browser rather
than trusting the unit tests alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cb8RVPNQ2ETPJxdy4b8CHf
This commit is contained in:
Claude
2026-08-22 15:09:01 +00:00
parent 23fd2ba881
commit 57362d85de
6 changed files with 95 additions and 14 deletions
@@ -46,3 +46,27 @@ test('teleprompter renders and responds to its primary controls', async ({ page,
await expect(page).toHaveURL(/flipH=true/);
await expect(view).toHaveCSS('transform', /^matrix\(-1/);
});
test('follow tolerates a small scroll and breaks on a real one, like the operator view', async ({ page, request }) => {
const response = await request.post('/data/db/demo');
expect(response.ok()).toBe(true);
const loadResponse = await request.get('/api/load/index/5');
expect(loadResponse.ok()).toBe(true);
await page.goto('/teleprompter?script=note');
const scroller = page.getByTestId('teleprompter-scroller');
const follow = page.getByTestId('teleprompter-follow');
await expect(scroller).toBeVisible();
await expect(follow).toBeDisabled();
await page.mouse.move(960, 500);
await page.mouse.wheel(0, 15);
await expect(follow).toBeDisabled();
await page.mouse.wheel(0, 400);
await expect(follow).toBeEnabled();
await page.keyboard.press('l');
await expect(follow).toBeDisabled();
});