Step 4 - User interface

5 min czytania

User interface

A thin rendering layer over the engine. Views know how to draw a state and how to report intent; they hold no game logic and decide nothing. Each view exports a factory that receives its mount element and a callback, and returns a render(state) function.

DOM structure

<main class="app">
  <h1 class="app__title">Tic Tac Toe</h1>

  <section class="controls" aria-label="Game settings">
    <fieldset>            <!-- Mode: Two players / Play vs computer (radio group) -->
    <label>               <!-- Difficulty: select, disabled in two-player mode -->
    <fieldset>            <!-- Starting mark: X / O / Alternate (radio group) -->
    <button class="btn btn--primary" data-action="new-game">New game</button>
  </section>

  <p class="status" role="status" aria-live="polite" aria-atomic="true"></p>

  <div class="board" role="grid" aria-label="Tic tac toe board, 3 by 3">
    <div role="row">
      <div role="gridcell">
        <button class="cell" data-index="0" aria-label="Row 1, column 1, empty"></button>
      </div>
      …nine cells total…
    </div>
  </div>

  <section class="scoreboard" aria-label="Scores">…</section>
</main>

Rules for the markup:

  • Cells are real <button> elements. Never <div onclick>, never <td>.
  • The grid is built once at startup. Rendering updates existing nodes; it never re-creates the board, because replacing nodes destroys focus and breaks screen-reader position.
  • data-index on each cell is the engine index 0..8.

Rendering

createBoardView(root, onSelect) returns render(state, options) which, for each cell:

  • sets the text content to X, O or the empty string;
  • sets aria-label to Row {r}, column {c}, {empty | X | O}, one-based for humans;
  • sets disabled when the cell is occupied, the game is over, or it is the computer's turn;
  • toggles .cell--x / .cell--o for styling and .cell--winning for cells in winningLine;
  • sets aria-disabled in step with disabled.

Rendering must be idempotent: calling render twice with the same state produces identical DOM and does not restart animations. Compare before writing — only assign a property when its value changes.

Status line and announcements

createStatusView(root) renders exactly one sentence, drawn from src/ui/strings.ts:

Situation Text
In progress, human turn Your turn — X / Your turn — O (two-player mode: X to move)
In progress, computer thinking Computer is thinking…
Win, vs computer, human won You win!
Win, vs computer, computer won Computer wins.
Win, two players X wins! / O wins!
Draw It's a draw.

The status element is the single aria-live="polite" region. Do not add a second live region anywhere; competing regions produce overlapping announcements. The scoreboard updates silently (aria-live="off"), since its content is redundant with the outcome announcement.

Input

Pointer/touch. One delegated click listener on .board reads data-index from event.target.closest('.cell') and calls onSelect(index). Ignore clicks on disabled cells and on the gaps between cells. Set touch-action: manipulation on cells to remove the 300 ms tap delay.

Keyboard (src/ui/keyboard.ts), roving tabindex:

  • Exactly one cell has tabindex="0" at a time; all others have tabindex="-1". The board is a single tab stop.
  • ArrowRight / ArrowLeft / ArrowUp / ArrowDown move focus by one cell, wrapping at the edges.
  • Home moves to index 0, End to index 8.
  • Enter and Space select the focused cell. Prevent default on Space so the page does not scroll.
  • Arrow navigation moves focus over occupied and disabled cells too, so a screen-reader user can inspect the whole board. Use aria-disabled plus a click guard rather than the disabled attribute if the browser skips disabled buttons in your implementation — but then keep aria-disabled and the guard perfectly in sync.
  • After a move, focus stays on the played cell. After New game, focus moves to the centre cell (index 4) and the roving tabindex resets there.

Global shortcuts: N starts a new game, ? toggles a small keyboard-help panel. Shortcuts are ignored while focus is inside a select or a text field.

Computer turn feedback

When the computer is to move, the UI disables all cells, shows the thinking status and schedules the AI call after a delay of 300–500 ms so the move is perceptible. The delay is cosmetic only: the result must not depend on it, and it must be cancelled if a new game starts first. Never block the main thread with a synchronous loop to simulate thinking.

Strings

Every user-visible string lives in src/ui/strings.ts as a flat exported object. No string literals in view code. This keeps a later translation a single-file change without pulling in an i18n library.

Acceptance criteria

  • The board renders as nine <button> elements inside a role="grid" with rows, each carrying a correct data-index.
  • Views contain no game rules: src/ui/ never decides a winner, validates a move or picks an AI move.
  • render is idempotent — a second call with the same state changes no DOM attribute, verified by a mutation-observer test.
  • The full game is playable with the keyboard alone: arrows wrap in both axes, Home/End work, Enter and Space place a mark, and Space never scrolls the page.
  • Exactly one cell has tabindex="0" at any moment.
  • Every cell exposes an aria-label naming its one-based row, column and content, updated after every move.
  • There is exactly one aria-live region in the document, and outcomes are announced through it.
  • Occupied cells, all cells after the game ends, and all cells during the computer's turn cannot be activated by mouse, touch or keyboard.
  • Winning cells receive .cell--winning, and the win is also conveyed as text.
  • After a move focus remains on the played cell; after New game focus lands on the centre.
  • No user-visible string appears outside src/ui/strings.ts.
  • npm run verify passes.

Dyskusja

Komentarze: 0

Brak komentarzy. Rozpocznij dyskusję.