Public: anyone can read this; the crew can change it.

The live region keeps your place: focus, an announcement, and what someone is typing #56

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

`static/live.js` no longer throws away the person using the page when a region is swapped. Before `el.replaceWith(next)` it records what has focus inside the region — by `id`, else by `name` — along with the caret, and afterwards hands both back to the same control in the fresh markup; when that control is gone, focus lands on the region's heading (given `tabindex="-1"` for the moment) instead of…

shipped production is serving it deploy 2adbe7d

opened this

static/live.js no longer throws away the person using the page when a region is swapped. Before el.replaceWith(next) it records what has focus inside the region — by id, else by name — along with the caret, and afterwards hands both back to the same control in the fresh markup; when that control is gone, focus lands on the region's heading (given tabindex="-1" for the moment) instead of falling to <body>, and every focus call passes preventScroll: true, so a region that grows moves nothing. An <input>, <textarea> or <select> that holds focus and holds a value the server's copy does not carry keeps its whole region as it is — the swap returns "kept" — and a one-shot blur listener refreshes it the moment they leave. Each of the three live regions (#proposals, #proposal, #thread) is now aria-live="polite" and points with data-status at a one-line role="status" paragraph outside it: outside because a region is replaced wholesale and an announcement replaced along with it is a new node some readers never speak. The text is summarize(type) in src/live.ts and rides the event stream as a third field, so the sentence is always the app's own ("A new remark", "A vote was cast" — the vote and roster lines are there already so #44 and #38 inherit them). static/app.css gains one .offscreen class (clipped, not display: none), used for the status lines and for the two headings the regions previously lacked; the thread's buttons gained ids so focus can find them again.

To look at: swap, spotOf, typing and restore in static/live.js — exported, because bun test hosts no DOM and this app takes no dependency to get one, so src/focus.test.ts drives the real functions over a stub of the handful of properties they touch (children, id, name, value, caret, focus), and the file's stream only starts when document and EventSource exist. The live region markup is pinned server-side in src/server.test.ts; I updated the existing assertion there that spelled out the old <section id="thread" …> tag, since the attributes on it are exactly what this issue changes. bun test is green (119) and bun run src/db.ts --check reports version 7, integrity ok; the home page still answers in about 4 ms and renders once, server-side, with live.js still pure enhancement.

What I did not do: no diffing, no framework, no second file — live.js went from 68 to 211 lines, about half of that comments, and each function is one concept, so it still reads in one sitting; if it grows again the focus half should move out. I also did not verify with a real screen reader, and today no live region actually contains a form control (the composers sit outside them), so the typing guard is proved by unit test and by the markup rather than by hand on the preview — it earns its keep when #38 puts reply forms and #44 puts vote buttons inside these regions.

🤖 Generated with Claude Code

Closes #49

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs