Skip to content

Accessibility: keyboard nav, landmarks, screen-reader board summary, offers, clocks and chat - #2377

Draft
nnickoloff1234 wants to merge 26 commits into
pychess:masterfrom
nnickoloff1234:accessibility-semantics
Draft

nnickoloff1234 wants to merge 26 commits into
pychess:masterfrom
nnickoloff1234:accessibility-semantics

Conversation

@nnickoloff1234

@nnickoloff1234 nnickoloff1234 commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

Accessibility work prompted by a blind user who wrote in. Two parts: ordinary HTML semantics on the site chrome, and a screen-reader summary of the board on every game page. No chessgroundx changes, no layout changes, and no special mode — everything is always on, and the parts sighted users can see benefit keyboard users as much as screen-reader users.

Everything was measured with Chrome's computed accessibility tree over CDP — the artifact a screen reader is actually handed, rather than the DOM — and listened to with Orca.

Navigation was unreachable without a mouse

The six header menus opened on :hover only, so 15 destinations — Forum, Teams, Tournaments, Studies, Import game and 10 more — could not be reached by keyboard at all, and never by touch. Measured on the lobby: 8 of 31 focusable elements reachable, now 31 of 31. WCAG 2.1.1 Level A.

  • A submenu opens when its section button's aria-expanded is true (:hover stays as the pointer preview), so the screen and the screen reader cannot disagree.
  • The six section titles become real disclosure buttons carrying aria-expanded. They were links, and a browse-mode screen-reader user has exactly one verb — activation — so a single control must either navigate or toggle, and aria-expanded on something that navigates is a lie. No destination is lost: each section's URL is already its submenu's first item (Play → /?any is also "Create a game"). client/topNav.ts adds one delegated listener, Escape to close with focus returned to the button, and mouseleave so a click followed by a mouse-out behaves the way hovering always did.
  • The three header panel buttons (settings, notifications, challenges) gain aria-expanded/aria-controls, routed through a single helper so the attribute cannot drift from the .shown class.

Landmarks

lichess ships banner + navigation + main on every page in both normal and blind mode — landmarks are not a blind-mode feature there. We had no navigation landmark anywhere, and no main on the board pages.

  • <div class="topnav"> → <nav>. It is the only navigation landmark on every page, so it needs no name. The one CSS selector that qualified it with div is un-qualified in the same commit, or the section hover background dies silently.
  • <main> on the analysis, editor and puzzle views. These are client-rendered, so they never inherited the template's <main>: a screen-reader user had nothing to jump to and had to arrow past the header on every page load. The round view already emitted one, so this is copying what existed.
  • The lobby's and the players page's two <aside>s are named, so they no longer announce as "complementary, complementary". The names reuse existing translations ("Create a game", "Online", "Leaderboard"); only "What's up" is new.

Two header bugs found while measuring

  • The Board Settings variant select had no accessible name. Its <label> was the only one in client/ written props: { for: ... } instead of attrs:. Snabbdom's props module assigns elm[key] = value, and a label's reflecting DOM property is htmlFor, not for — so no for attribute was ever emitted and label.control was null. A 59-option combobox announced as nothing, while looking perfectly labelled on screen. WCAG 4.1.2.
  • Tabbing to the header search input broke the header until reload. It read SearchTest-Soldier…, magnifier gone. overflow: hidden still creates a scroll container: #search-input is focusable but clipped outside the collapsed 50px, so focus scrolls it into view and leaves scrollLeft at 36–60, pushing .search-icon out of the aperture. Nothing ever reset it. :focus-within now expands the bar exactly as .active does, so the content fits and there is nothing to scroll. Mouse paths measured identical before and after.

The board, for screen readers

The board itself is pixels: chessgroundx exposes no text, so a screen reader found nothing on a game page and an opponent's move was never announced. Every board page now carries a visually hidden summary in the shape of lichess's blind mode, rendered beside the board rather than inside it:

  • Pieces — per colour, one line per piece type: "rook: a1, h1". Squares follow the variant's own notation for shogi (76) and janggi (83), matching what the move list already says.
  • Pockets — counts, in pocket variants.
  • Last move and Status — live regions, so they are spoken when they change: assertive on round pages, polite on analysis and puzzle pages.

BoardSummaryView (client/accessibility/boardSummary.ts) is owned by every GameController, so a single-board page gets one and each board of a two-board page gets its own, headed "Board A" / "Board B", with the game status on the viewer's own board only so a result is announced once. Colour names are the variant's own (Red, Gold, Blue).

One source for piece names

client/pieceNames.ts — pieceName(variant, role, color) tries the variant's own table, then its piece family's (chess, xiangqi), then falls back to the letter. Table entries use _() like everywhere else and are translated again with _() when shown, because the tables are built before translations load. That also fixes the old pocket.pieceNames, which were translated only at load and so stayed English. Filled from the rules pages for shogi, shogun, seirawan, capablanca, capahouse, grand and orda; community variants keep their authors' names. The bughouse chat presets use the same function.

Game controls, clocks and chat

  • <html lang> from the session locale (zh_CN becomes zh-CN), so a translated page is read by a synthesiser in the right language.
  • Offers you could hear but not answer. The draw, takeback, rematch and correspondence-confirm offers answered through bare <div>s, unreachable by keyboard. They are buttons now, named Accept / Decline / Cancel / Confirm, styled exactly as before (measured). The offer area is a polite live region, so an arriving offer is read out.
  • A takeback back to the start left the taken-back moves in the move list; it is rebuilt now.
  • Move-list buttons were named by their icon font's glyph: the accessibility tree read "p", "l", "n", "o", "m", "s", because the glyph beats title in the name computation. The icons are hidden from readers and the four arrows get titles (First / Previous / Next / Last move).
  • Clocks on request, as lichess does. A Time section in the board summary says each clock in words ("White: 2 minutes 57 seconds") under role="timer", so it is read when reached and never spoken as it ticks. A Clock.onRender hook feeds it. The visible clock, which read as "05", ":", "23", is hidden from readers.
  • Chat, everywhere, like lichess's: a real list (an extra wrapper div made it none), new messages announced politely, the input named "Chat" instead of the hardcoded English "Chat input". Chat history is resent and appended on every load and reconnect, so the list is set aria-live="off" while history refills it and back to polite by the next live message (no timer). Bughouse preset messages, previously an empty CSS icon to a reader, carry the preset's label from one presetLabel() shared with the preset buttons.

Headings, the lobby, tabs, menus, focus rings, settings and tournaments

  • Game-page headings. A hidden h1 (the page title) and h2s for Moves, Actions and Chat, so a screen reader can jump straight to them. Lichess's normal game page has none; its blind mode is organised by them.
  • Joining a seek by keyboard. Seek rows were click-only (lichess's are too). The colour icon is now a button named "Join: Alice, 5+3, Crazyhouse" (or "Cancel: …" for your own); its click reaches the row's existing handler, so mouse and keyboard share one path.
  • Tabs are buttons, as on lichess. The lobby, analysis and two-board tabs are <button role="tab">: every tab is a Tab stop, Enter/Space work natively, no key handling. The lobby tabs were unreachable by Tab, and the two-board tabs (and panels) carried positive tabindex values that hoisted them ahead of the whole page.
  • Mobile menu. The hamburger was a <div>; it is a named button reporting aria-expanded. Its closed links were only slid off-screen, so Tab walked through 25 invisible links before reaching the page; they are hidden while closed now, still animating out. The analysis "Menu" button reports its state too.
  • Focus rings. Several rules removed the focus outline for everyone — including on Draw/Resign. They now remove it only after a mouse click (:focus:not(:focus-visible)), the pattern study.css already uses.
  • Settings. The 323 board-theme and piece-set radios had no name (their labels are pictures); they now carry the style's raw file or CSS name ("8x8brown.svg", "cburnett"), which at least tells the options apart. Header and page aria-labels that were hard-coded English are translatable.
  • One dismissal for the header dropdowns. The nav menus, the login chooser and the Settings/Notifications/Challenges panels each had their own click-outside and Escape code; registerHeaderDropdown() in headerPanel.ts replaces the three copies. The panels gain Escape (closing and returning focus), which they never had.
  • Tournaments. Standings rows and the player panel's game rows get buttons (same pattern as the seeks), the "×" closes are named buttons, the top game is a real link instead of a click handler, and the pager buttons are named.

Labels, action links, dialogs and engine lines

  • Links that were not links. 19 h('a') with a click handler and no href (the board editor's whole control strip, analysis actions, "Continue training" and the puzzle votes, two-board and zen controls) could not be reached by keyboard. They get href="#" and preventDefault(), the minimal change that leaves their look untouched (measured).
  • Puzzle feedback is spoken through the board summary's status ("Best move!", "Try something else.", "Puzzle complete!").
  • Native dialogs. The forum relocate/delete modals and the round-robin scheduling modal are <dialog>s opened with showModal() through one shared modalDialogHooks: focus moves in and stays, Escape closes, and focus returns to the opener. alertDialog became a native modal exactly as confirmDialog did (7c6b214), so an alert raised from inside a modal shows above it instead of behind it. Visuals unchanged (measured).
  • Forum post buttons (Edit, Delete, Quote…) were visibility: hidden until hover, so Tab skipped them; opacity + :focus-within keeps them identical for the mouse and reachable by keyboard.
  • Engine lines were click-only custom elements; they are keyboard buttons now (pvLine()), one Tab stop per line, named by the line itself ("1. e4 c5 2. Nf3 …"). The "-" placeholder between positions is the same element, so Enter, Enter, Enter walks the engine's line while the board summary announces each move. Not a <button>, which would clip the line's ellipsis.
  • Names for unnamed fields. The forum's edit box had none, and its reply box lost its "Message" label to display: none (now .sr-only); placeholder-only fields (forum search, inbox, tournament/team forms) carry their text as a label too; the editor FEN and lobby fields are labelled.
  • Tournaments. Opening a player's panel is announced (a polite live region on its stats). The "Top games" links are named "Open game: white – black": their 2x2 layout read each rating next to the wrong player.
  • Images. Decorative images get alt="" (readers announced their file names); profile trophies get their name as alt.

Every converted element was measured in place against its old form (swapped back and compared) and came out identical; the header controls, tabs, seeks and tournament rows were driven with real keys.

Skip link and an accessibility guide

  • A skip link, the first thing Tab reaches on every page and invisible until focused: "Skip to main content" jumps past the header to a #main-content target placed once in base.html, so every page has it without editing each template.
  • Beside it, a link to a new guide at /page/accessibility: an ordinary translatable page (the same shell as the forum etiquette page, no JavaScript) describing the landmarks, the board summary section by section, moving through a game with the arrow keys, offers, engine lines and chat, and saying plainly that keyboard moves are not available yet. The two links are a list, so a reader announces "list, 2 items". lichess does the same with its blind-mode tutorial page.

Headings and image labels

  • One h1 per page. The terminology page (7 languages) made every section an h1, so pressing 1 cycled through six "page titles"; its sections are h2 now. The Reports and Public chats admin pages had no h1; they get a hidden one named as in the admin menu (so does closed.html, which no route currently renders).
  • No skipped levels. The patron page's tagline was an h6 (now a paragraph) and its questions h3s under the h1 (now h2); the features table's "All registered accounts" column label was an h3 inside a <th> (now plain text); Dragon Chess's piece heading and the create-game dialog's "A.I. Level" each sat two levels below their parent. CSS keeps every converted element's look (measured against the old tags in the same page).
  • Rules-page titles read once. Every rules page opens with the variant icon inside its h1, and the icon's alt repeated the title: Chrome named the heading "Crazyhouse960 Crazyhouse 960". The icon is decoration beside the name, so its alt is empty (296 markdown files).
  • Diagram labels read as words. "HorseDiagram", "KvsKO", "image" and the like (154 labels) become "Horse movement", "King versus king and offering", "Gold cannon movement" — the form the xiangqi pages already use.

Page headings, game cards, and names found by verification

  • Every main page opens with an h1 and is organised by h2s, hidden where the page has no visible title: Lobby (What's up › Timeline, Challenges, Variants, Create a game; the Leaderboard, Tournament winners and Puzzle of the day titles become level-2 headings in place), Players (Online, Leaderboard, one level-3 heading per variant card), Games, Editor, Puzzle and Stats (which also gains its <main>). All strings reused.
  • Games page cards were click-only; each is now a keyboard link named "Open game: white – black, Variant, 5+3".
  • Editor: the side-to-move select and the two queenside castling boxes are named.
  • Icon glyphs read as names. An accessibility-tree sweep of 16 pages for controls named by one character or nothing found: Abort, Resign and Draw on the game page reading "j", "d", "½"; "Give 15 seconds" reading "W"; the crosstable's result links reading "1" / "0"; the FEN field and the two-board engine switch with no name; and the two-board move list and game controls with the same glyph problem the single-board list had. Icons are hidden from readers, and the rest get names from existing strings ("Open game: 1–0", "Computer analysis", "FEN"). The single-board engine switch stays unnamed for now: it is the only switch without visible text. Attributes only; the sweep is clean now apart from the puzzle votes' 👍/👎.

Verification

Board summary: verified live on crazyhouse round (between two windows: opponent moves, a drop, resignation) and analysis, a live four-player bughouse game, finished bughouse games and the bughouse analysis board, plus every filled variant including promoted pieces. Gates on this branch: yarn lint, yarn typecheck, yarn md, yarn test (101 suites, 832 tests).

Game controls, clocks and chat: verified live with real keys (Enter declined a draw, Space accepted a takeback, Enter accepted a rematch), a 3-minute game with both windows' clocks agreeing through a flip, a chat message and three bughouse presets arriving as named list items in a polite live list, and lang served as en, de, zh-CN, gl-ES.

Labels, dialogs and engine lines: verified live with real keys (forum delete modal on a live post, engine lines over three moves, puzzle vote by keyboard) and from Chrome's accessibility tree; the round-robin modal through test_gui.py::test_round_robin_full_browser_flow. Gates: yarn lint, yarn typecheck, yarn md, yarn test (830 tests).

Page headings and the verification fixes: a live crazyhouse game between two windows, followed over AT-SPI with Orca's event log (joined by Enter on the seek, the opponent's moves and the result announced, a draw offer declined by Enter, chat, the Draw confirmation focused); engine lines walked by Enter on the two-board analysis page too; the layout matrix (286 rows) against the same commit with this PR reversed shows no difference; yarn lint, yarn typecheck, yarn md, yarn test (836 tests, after merging master).

Headings and image labels: heading outlines and names read from Chrome's accessibility tree on the patron, features, terminology, Dragon, Crazyhouse 960, shogi, chak and cannon shogi pages; yarn lint, yarn typecheck, yarn md, yarn test (830 tests).

Skip link and guide: driven with real keys on the lobby, analysis, forum, players and guide pages (Tab shows the skip link, Enter then Tab lands on the page's first control); header and content positions measured unchanged; Python unittest 1485 OK, pytest tests/test_simul.py 40 passed, ruff and pyrefly clean.

Header work:

yarn lint, yarn typecheck, yarn md, yarn test (94 suites, 775 tests); Python unittest 1312 OK; pytest tests/test_simul.py 40 passed. Landmark and focus counts re-measured from the accessibility tree after each change, and the nav menus heard in Orca ("Play collapsed button" → Enter → "expanded").

🤖 Generated with Claude Code

Ordinary HTML semantics on the site chrome. No board code, no chessgroundx, no
layout CSS, and nothing behind a special mode -- every change benefits sighted
keyboard users as much as screen-reader users.

Measured throughout with Chrome's computed accessibility tree over CDP (the
artifact a screen reader is actually handed), and heard with Orca.

NAVIGATION WAS UNREACHABLE WITHOUT A MOUSE
The six header menus opened on :hover only, so 15 destinations -- Forum, Teams,
Tournaments, Studies, Import game and 10 more -- could not be reached by
keyboard at all, and never by touch. Measured on the lobby: 8 of 31 focusable
elements reachable, now 31 of 31. WCAG 2.1.1 Level A.

  - :focus-within beside :hover on the submenus.
  - The six section titles become real disclosure buttons carrying
    aria-expanded, so a screen reader is told whether a menu is open. They were
    links, and a browse-mode user has exactly one verb -- activation -- so a
    single control must either navigate or toggle; aria-expanded on something
    that navigates is a lie. No destination is lost: each section's URL is also
    its submenu's first item. topNav.ts adds one delegated listener, Escape to
    close with focus returned, and mouseleave to match hover's old behaviour.
  - The three header panel buttons (settings, notifications, challenges) gain
    aria-expanded/aria-controls, routed through one helper so the attribute
    cannot drift from the .shown class.

LANDMARKS
lichess ships banner + navigation + main on every page in both normal and blind
mode; we had no navigation landmark anywhere, and no main on the board pages.

  - <div class="topnav"> becomes <nav aria-label="Site">. The one selector that
    qualified it with `div` is un-qualified, or the section hover background
    dies silently.
  - <main> on the analysis, editor and puzzle views. These are client-rendered,
    so they never inherited the template's <main>: a screen-reader user had
    nothing to jump to and had to arrow past the header on every page load.
  - The lobby's and the players page's two <aside>s are named, so they no longer
    announce as "complementary, complementary". lichess names none of theirs.

HEADER FIXES FOUND WHILE MEASURING
  - The Board Settings variant select had NO accessible name: its <label> was
    the only one in the client written `props: { for: ... }` instead of
    `attrs:`. Snabbdom's props module assigns elm[key] = value, and a label's
    reflecting property is htmlFor, not for -- so no `for` attribute was ever
    emitted and label.control was null. A 59-option combobox announced as
    nothing. WCAG 4.1.2.
  - Tabbing to the header search input left the header reading
    "SearchTest-Soldier..." until a page reload. overflow:hidden still creates a
    scroll container: the input is focusable but clipped outside the collapsed
    50px, so focus scrolls it into view and leaves scrollLeft at 36-60, pushing
    the magnifier out of the aperture. Nothing reset it. :focus-within now
    expands the bar as .active does, so the content fits and there is nothing to
    scroll. Mouse behaviour is unchanged.

Gates: yarn lint, typecheck, md, test (94 suites, 775 tests); Python
unittest 1312 OK and pytest tests/test_simul.py 40 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@nnickoloff1234
nnickoloff1234 marked this pull request as draft September 28, 2026 08:25
nnickoloff1234 and others added 3 commits October 10, 2026 15:51
Drop the untranslated "Site" label from the header nav: it is the only
navigation landmark on every page, so "navigation" is unambiguous. Rename
the side-panel labels to strings that already exist in the catalogs:
players "Online" and "Leaderboard", lobby "Create a game". The lobby's
"What's up" stays, as there is no existing string for that panel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every board page now carries a visually hidden summary in the shape of
lichess's blind mode: the position as text (one line per piece type,
per colour), the pockets, the last move and the game status. The last
move and the status are live regions, assertive on round pages and
polite on analysis and puzzle pages, so an opponent's move is spoken
without the player looking for it.

BoardSummaryView (client/accessibility/boardSummary.ts) is owned by
every GameController, so single-board pages get one and each board of a
two-board page gets its own, headed "Board A"/"Board B". On two-board
pages the status is reported on the viewer's own board only. Squares
follow the variant's notation for shogi and janggi.

Piece names now come from client/pieceNames.ts: the variant's own
table, then its piece family's (chess, xiangqi), then the letter.
Tables hold English marked with the new N_() and are translated when
shown; the old pocket.pieceNames ran _() before translations loaded.
Names are filled for shogi, shogun, seirawan, capablanca, capahouse,
grand and orda, and community variants keep their authors' names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nnickoloff1234 nnickoloff1234 changed the title Accessibility: keyboard-reachable nav, landmarks, and header fixes Accessibility: keyboard-reachable nav, landmarks, and a screen-reader board summary Oct 10, 2026
nnickoloff1234 and others added 2 commits October 10, 2026 18:27
…chat

- <html lang> from the session locale, with the underscore of codes such
  as zh_CN turned into the hyphen the attribute expects.
- The draw, takeback, rematch and correspondence-confirm offers answer
  through real buttons named Accept/Decline/Cancel/Confirm, and the offer
  area is a polite live region, so an arriving offer is read out.
- A takeback back to the start now rebuilds the move list; it kept
  showing the taken-back moves.
- The move-list buttons were named by their icon font's glyph ("p",
  "l", "n"...); the icons are hidden from readers and the buttons are
  titled First/Previous/Next/Last move.
- The board summary gains a Time section: each clock in words, read on
  request (role="timer"), fed by a new Clock.onRender hook. The visible
  clock, which read as "05", ":", "23", is hidden from readers.
- Chat, everywhere: a real list whose new messages are announced
  politely, as on lichess; the input is named "Chat"; history resent on
  load or reconnect is filled with the list silenced. Bughouse preset
  messages carry the preset's label, from one presetLabel() shared with
  the preset buttons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nnickoloff1234 nnickoloff1234 changed the title Accessibility: keyboard-reachable nav, landmarks, and a screen-reader board summary Accessibility: keyboard nav, landmarks, screen-reader board summary, offers, clocks and chat Oct 10, 2026
nnickoloff1234 and others added 6 commits October 10, 2026 21:22
…, tournaments

- Game pages get a hidden h1 (the tab title) and h2s for Moves, Actions
  and Chat, so a screen reader can jump to them.
- Lobby seek rows: the colour icon is a button ("Join: Alice, 5+3,
  Crazyhouse" / "Cancel: …"); its click reaches the row's own handler.
- Tabs on the lobby, analysis and two-board pages are <button role=tab>,
  as on lichess: every tab a Tab stop, Enter/Space native, no key
  handling. The two-board tabs lose their positive tabindex values.
- The mobile menu's hamburger is a named button with aria-expanded, and
  its closed links are hidden rather than only moved off-screen, so Tab
  no longer walks through them. The analysis Menu reports its state.
- Focus rings removed for everyone now go only after a mouse click
  (:focus:not(:focus-visible)): game controls, icon buttons, sliders,
  tab-panel text areas.
- Settings: the 323 board-theme and piece-set radios are named; header
  and page aria-labels are translatable.
- One dismissal for all header dropdowns (nav menus, login, Settings,
  Notifications, Challenges): click outside closes, Escape closes and
  returns focus. Replaces three copies; the panels gain Escape.
- Tournaments: standings and player-game rows get buttons, the "×"
  closes are named buttons, the top game is a real link, the pager
  buttons are named.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Labels: the arena form's "Byoyomi periods" points at its select; the
  memory game's background radios are named (they used label=, which
  is no attribute on <input>); the lobby's rating-range sliders are
  named; the editor's and the lobby's FEN fields are named "FEN".
- The editor's control strip, the analysis actions, the engine's "Go
  deeper", the puzzle's Hint / View the solution / Continue training
  and the zen-mode exit were <a> without href: not focusable, deaf to
  Enter. They get href="#" and preventDefault() in their handlers (as
  the forum's cancel link does); measured, buttons would have changed
  their look and href="#" does not. A keyboard user can now leave zen
  mode.
- Puzzle feedback goes to the board summary's polite status line, so a
  screen reader hears "Best move!", "That's not the move!" and the
  result; puzzle moves now update the summary's last move too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Forum relocate/delete and round-robin modals are native <dialog>s via
  shared modalDialogHooks: focus moves in, Escape closes, focus returns.
- alertDialog is a native modal like confirmDialog, so it shows above
  another modal.
- Forum post buttons stay reachable by Tab (opacity, not visibility).
- Engine lines are keyboard buttons (pvLine); focus survives the
  placeholder between positions.
- Tournament player panel stats are a polite live region; top-games links
  are named "Open game: white – black".
- Names for the forum edit and reply boxes, placeholder-only fields,
  tournament/team form fields, puzzle vote links, decorative images and
  profile trophies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every page starts with a two-item list, visible only on focus: "Skip to
main content" (to a #main-content target in base.html) and a link to the
new /page/accessibility guide, a plain translatable page describing the
board summary, move navigation and what is not available yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nnickoloff1234
nnickoloff1234 force-pushed the accessibility-semantics branch from 381de12 to 0fb38b2 Compare October 11, 2026 09:56
nnickoloff1234 and others added 14 commits October 11, 2026 14:20
- Docs: one h1 per terminology and Battle of Ideologies page (sections
  move down a level); Dragon's piece heading no longer skips a level.
- Heading levels follow the content on the patron and features pages
  and in the create-game dialog; CSS keeps every converted element's
  look. The closed-account page and two admin pages get an h1.
- Rules pages: the variant icon at the start of each title gets an
  empty alt (the heading was read as "Crazyhouse960 Crazyhouse 960"),
  and run-together diagram labels ("HorseDiagram", "image") read as
  words ("Horse movement").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	client/analysis/analysisPage.ts
#	static/docs/battleofideologies.md
#	static/docs/melonvariant.md
- Headings where a screen-reader user lands: a hidden h1 on the puzzle,
  editor, stats, games and players pages; the lobby gets a heading per
  section (What's up, Timeline, Challenges, Variants, Create a game,
  Leaderboard, Tournament winners, Puzzle of the day) and the players
  page one per variant card. Visible titles are marked with
  role="heading", so nothing moves.
- The games page's live-game cards could only be clicked: they are
  keyboard links now ("Open game: white – black, variant, 5+0"). The
  game grid is no longer in an unnamed <aside>.
- The stats page gets a <main> landmark.
- Editor: the side-to-move select is named, and the two queenside
  castling boxes are "White O-O-O" / "Black O-O-O".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round Abort/Draw/Resign and two-board Draw/Resign hide their icon glyphs;
Give 15 seconds, the engine switch, the FEN fields and the crosstable
result links get names; the two-board move list matches the single-board
one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
replayingChat() turns the list's aria-live off before a history refill;
history lines are added with a flag, and every other message turns it back
to polite. No timer, so a message arriving right after a reconnect is no
longer lost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A history line sets the list's aria-live off itself before it is added, so
the separate call before each refill and its test mock are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
srHeading/pageHeading go to accessibility/headings.ts, pvLine/pvLinePending
to a new common/engineView.ts shared by both analysis pages. The ariaLabel
parameter on toggleSwitch is reverted: one caller did not justify it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The local hidden() helpers in the single- and two-board move lists become
ariaHiddenAttr() in accessibility/aria.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The heuristic that turned file names into words is gone; each radio's
aria-label is its board file or piece CSS name as it stands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tables are still translated again when shown, so one marker convention
is enough; i18n.ts is back to upstream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
clock.ts's hiddenFromReaders() and the hand-written aria-hidden attrs in the
round controls, tournament pager and two-board round controls now use the
shared helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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