It's playable
This commit is contained in:
parent
4a08467237
commit
687f2acc22
|
@ -44,6 +44,10 @@ body {
|
|||
user-select: none;
|
||||
}
|
||||
|
||||
table.Game-rows {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.Game-keyboard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -171,7 +175,8 @@ a:active {
|
|||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.Game-sr-feedback {
|
||||
.Game-sr-feedback,
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
left: -10000px;
|
||||
top: auto;
|
||||
|
|
19
src/Game.tsx
19
src/Game.tsx
|
@ -1,10 +1,10 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { Row, RowState } from "./Row";
|
||||
import dictionary from "./dictionary.json";
|
||||
import { Clue, clue } from "./clue";
|
||||
import { Clue, clue, describeClue } from "./clue";
|
||||
import { Keyboard } from "./Keyboard";
|
||||
import targetList from "./targets.json";
|
||||
import { dictionarySet, pick, resetRng, seed } from "./util";
|
||||
import { dictionarySet, pick, resetRng, seed, speak } from "./util";
|
||||
|
||||
enum GameState {
|
||||
Playing,
|
||||
|
@ -57,6 +57,7 @@ function Game(props: GameProps) {
|
|||
if (/^[a-z]$/.test(key)) {
|
||||
setCurrentGuess((guess) => (guess + key).slice(0, wordLength));
|
||||
setHint("");
|
||||
setSrStatus("");
|
||||
} else if (key === "Backspace") {
|
||||
setCurrentGuess((guess) => guess.slice(0, -1));
|
||||
setHint("");
|
||||
|
@ -81,7 +82,7 @@ function Game(props: GameProps) {
|
|||
setGameState(GameState.Lost);
|
||||
} else {
|
||||
setHint("");
|
||||
setSrStatus("Feedback goes here");
|
||||
speak(describeClue(clue(currentGuess, target)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -99,7 +100,7 @@ function Game(props: GameProps) {
|
|||
}, [currentGuess, gameState]);
|
||||
|
||||
let letterInfo = new Map<string, Clue>();
|
||||
const rowDivs = Array(props.maxGuesses)
|
||||
const tableRows = Array(props.maxGuesses)
|
||||
.fill(undefined)
|
||||
.map((_, i) => {
|
||||
const guess = [...guesses, currentGuess][i] ?? "";
|
||||
|
@ -156,7 +157,7 @@ function Game(props: GameProps) {
|
|||
}}
|
||||
></input>
|
||||
<button
|
||||
style={{ flex: "0" }}
|
||||
style={{ flex: "0 0 auto" }}
|
||||
disabled={gameState !== GameState.Playing || guesses.length === 0}
|
||||
onClick={() => {
|
||||
setHint(
|
||||
|
@ -169,11 +170,11 @@ function Game(props: GameProps) {
|
|||
Give up
|
||||
</button>
|
||||
</div>
|
||||
<div className="Game-rows">{rowDivs}</div>
|
||||
<p role="status">{hint || `\u00a0`}</p>
|
||||
<p role="status" className="Game-sr-feedback">
|
||||
<table className="Game-rows">{tableRows}</table>
|
||||
<p role="alert">{hint || `\u00a0`}</p>
|
||||
{/* <p role="alert" className="Game-sr-feedback">
|
||||
{srStatus}
|
||||
</p>
|
||||
</p> */}
|
||||
<Keyboard letterInfo={letterInfo} onKey={onKey} />
|
||||
{seed ? (
|
||||
<div className="Game-seed-info">
|
||||
|
|
|
@ -13,7 +13,7 @@ export function Keyboard(props: KeyboardProps) {
|
|||
];
|
||||
|
||||
return (
|
||||
<div className="Game-keyboard">
|
||||
<div className="Game-keyboard" aria-hidden="true">
|
||||
{keyboard.map((row, i) => (
|
||||
<div key={i} className="Game-keyboard-row">
|
||||
{row.map((label, j) => {
|
||||
|
@ -29,6 +29,7 @@ export function Keyboard(props: KeyboardProps) {
|
|||
<div
|
||||
tabIndex={-1}
|
||||
key={j}
|
||||
role="button"
|
||||
className={className}
|
||||
onClick={() => {
|
||||
props.onKey(label);
|
||||
|
|
26
src/Row.tsx
26
src/Row.tsx
|
@ -1,4 +1,4 @@
|
|||
import { Clue, clueClass, CluedLetter } from "./clue";
|
||||
import { Clue, clueClass, CluedLetter, clueWord } from "./clue";
|
||||
|
||||
export enum RowState {
|
||||
LockedIn,
|
||||
|
@ -24,20 +24,22 @@ export function Row(props: RowProps) {
|
|||
letterClass += " " + clueClass(clue);
|
||||
}
|
||||
return (
|
||||
<div key={i} className={letterClass}>
|
||||
<td
|
||||
key={i}
|
||||
className={letterClass}
|
||||
aria-live="polite"
|
||||
aria-label={
|
||||
isLockedIn
|
||||
? letter.toUpperCase() +
|
||||
(clue === undefined ? "" : ": " + clueWord(clue))
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{letter}
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
});
|
||||
let rowClass = "Row";
|
||||
if (isLockedIn) rowClass += " Row-locked-in";
|
||||
return (
|
||||
<div
|
||||
className={rowClass}
|
||||
role={isEditing ? "input" : "row"}
|
||||
tabIndex={isEditing ? 0 : undefined}
|
||||
>
|
||||
{letterDivs}
|
||||
</div>
|
||||
);
|
||||
return <tr className={rowClass}>{letterDivs}</tr>;
|
||||
}
|
||||
|
|
16
src/clue.ts
16
src/clue.ts
|
@ -39,3 +39,19 @@ export function clueClass(clue: Clue): string {
|
|||
return "letter-correct";
|
||||
}
|
||||
}
|
||||
|
||||
export function clueWord(clue: Clue): string {
|
||||
if (clue === Clue.Absent) {
|
||||
return "no";
|
||||
} else if (clue === Clue.Elsewhere) {
|
||||
return "yellow";
|
||||
} else {
|
||||
return "correct";
|
||||
}
|
||||
}
|
||||
|
||||
export function describeClue(clue: CluedLetter[]): string {
|
||||
return clue
|
||||
.map(({ letter, clue }) => letter.toUpperCase() + " " + clueWord(clue!))
|
||||
.join(", ");
|
||||
}
|
||||
|
|
21
src/util.ts
21
src/util.ts
|
@ -24,3 +24,24 @@ export function resetRng(): void {
|
|||
export function pick<T>(array: Array<T>): T {
|
||||
return array[Math.floor(array.length * random())];
|
||||
}
|
||||
|
||||
// https://a11y-guidelines.orange.com/en/web/components-examples/make-a-screen-reader-talk/
|
||||
export function speak(
|
||||
text: string,
|
||||
priority: "polite" | "assertive" = "assertive"
|
||||
) {
|
||||
var el = document.createElement("div");
|
||||
var id = "speak-" + Date.now();
|
||||
el.setAttribute("id", id);
|
||||
el.setAttribute("aria-live", priority || "polite");
|
||||
el.classList.add("sr-only");
|
||||
document.body.appendChild(el);
|
||||
|
||||
window.setTimeout(function () {
|
||||
document.getElementById(id)!.innerHTML = text;
|
||||
}, 100);
|
||||
|
||||
window.setTimeout(function () {
|
||||
document.body.removeChild(document.getElementById(id)!);
|
||||
}, 1000);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue