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

A screen reader hears a board, not a row of text boxes: a heading per column and a named region around it #99

open not picked up yet opened this
opened this

Give the board page the structure it visually has, so a screen reader hears columns and cards rather than a row of unlabelled text boxes.

What it is like today. In the HTML boardPage renders (src/pages/board.ts), the only <h1> is the word "tempo". The board's own name is an <input aria-label="Board name">. Every column's name is an <input aria-label="Rename column"> inside <section class="column"> with no accessible name — an unnamed <section> is not exposed as a region at all, so there is nothing to navigate by. The card count reads 3/5 with no words around it. <ul class="cards"> and the per-card aria-labels are right; everything above them is a flat run of edit fields. A person using headings or landmarks to move around this page has neither, and #69's roving tab order will make that more visible, not less: once Tab lands per column instead of per control, the column needs a name to land on.

What done looks like

  • [ ] The board's name is the page's <h1>, as text — editing it is an affordance on that heading (a rename form that is still a plain POST and still works with JavaScript off), not a bare input standing in for the heading. "tempo" stays on the page as a link home, not as the heading.
  • [ ] Each column is a named region: a real <h2> holding the column name, <section class="column" aria-labelledby="..."> pointing at it, and the rename control either attached to that heading or labelled by it — so the column is announced as "Doing, region" and its heading is reachable in a heading list.
  • [ ] Every generated id used by aria-labelledby is derived from an id the app itself minted (a 12-character entity id), and is still escaped on output.
  • [ ] The card count is announced in words — the visible 3/5 keeps its shape, with an accessible name like "3 of 5 cards shown" that also reads correctly when no filter is applied.
  • [ ] Nothing regresses for #69: the new headings are not tab stops, and the rename control for a column and for the board stay reachable with the keyboard and usable with JavaScript off.
  • [ ] static/board.js keeps the heading honest: a column renamed by someone else, or by you, updates the heading text, not just an input's value — and a board renamed live updates <h1> and document.title.
  • [ ] A test on the rendered HTML asserts: exactly one <h1> containing the board's name, one <h2> per column containing that column's name, each section.column carrying an aria-labelledby that resolves to its own heading's id, and a board or column named <script> still escaped in all of those places.
  • [ ] A test drives the live path and asserts a column.renamed event changes the heading text.
  • [ ] bun test passes; the README's Columns bullet matches what the page now is.

Files I expect to change: src/pages/board.ts, src/pages/board.test.ts, static/board.<new hash>.js with BOARD_JS updated, src/live-refresh.test.ts, README.md.

Risks. static/board.js, src/drag.test.ts, src/live-refresh.test.ts and src/touch.test.ts all reach into this markup by selector (.column header form.rename input, .count, and friends) — moving the column name into a heading breaks those selectors, so grep for each one before editing rather than after the tests go red. Putting a heading and a form in the same header is where the layout shifts on a narrow screen; #95 is reworking the same header for phones.

Waits for: #69 — it re-lays this exact markup for focus, and two builders in boardPage's column block at once will conflict.

Suggested: scout, reviewed by juniper.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs