fuzzyworld

word-waltz / files / src / components / Keyboard.ts

Keyboard.ts

2.9 KB · history · blame

1/**
2 * Keyboard — on-screen keyboard
3 *
4 * Displays a QWERTY keyboard. Keys are colored to reflect the best-known
5 * state for each letter across all guesses in the current round:
6 * 'correct' > 'present' > 'absent' > (no state)
7 */
8
9export type LetterState = 'correct' | 'present' | 'absent';
10
11const ROWS = ['QWERTYUIOP', 'ASDFGHJKL', 'ENTERZCVBNM⌫'] as const;
12
13interface KeyboardProps {
14 onKey: (key: string) => void;
15 letterStates: Readonly<Record<string, LetterState>>;
16}
17
18export function mountKeyboard(container: HTMLElement, props: KeyboardProps): () => void {
19 const { onKey } = props;
20
21 const root = document.createElement('div');
22 root.className = 'keyboard';
23
24 for (const rowStr of ROWS) {
25 const rowEl = document.createElement('div');
26 rowEl.className = 'keyboard__row';
27
28 if (rowStr.startsWith('ENTER')) {
29 // Bottom row: Enter — middle letters — Backspace
30 appendKey(rowEl, 'Enter', 'Enter', onKey, true, 'Submit guess');
31 for (const ch of rowStr.slice('ENTER'.length, -1)) {
32 appendKey(rowEl, ch, ch, onKey, false);
33 }
34 appendKey(rowEl, '⌫', 'Backspace', onKey, true, 'Delete letter');
35 } else {
36 for (const ch of rowStr) {
37 appendKey(rowEl, ch, ch, onKey, false);
38 }
39 }
40
41 root.appendChild(rowEl);
42 }
43
44 applyStates(root, props.letterStates);
45 container.appendChild(root);
46
47 function handleKeydown(e: KeyboardEvent) {
48 if (e.ctrlKey || e.metaKey || e.altKey) return;
49 const k = e.key;
50 if (k === 'Enter' || k === 'Backspace') { onKey(k); return; }
51 if (/^[a-zA-Z]$/.test(k)) onKey(k.toUpperCase());
52 }
53 window.addEventListener('keydown', handleKeydown);
54
55 return function cleanup() {
56 window.removeEventListener('keydown', handleKeydown);
57 container.removeChild(root);
58 };
59}
60
61function appendKey(
62 row: HTMLElement,
63 label: string,
64 key: string,
65 onKey: (k: string) => void,
66 wide: boolean,
67 ariaLabel?: string,
68): void {
69 const btn = document.createElement('button');
70 btn.type = 'button';
71 btn.className = 'keyboard__key' + (wide ? ' keyboard__key--wide' : '');
72 btn.textContent = label;
73 btn.dataset['key'] = key;
74 if (ariaLabel) btn.setAttribute('aria-label', ariaLabel);
75 btn.addEventListener('click', () => onKey(key));
76 row.appendChild(btn);
77}
78
79export function updateKeyboard(
80 container: HTMLElement,
81 letterStates: Readonly<Record<string, LetterState>>,
82): void {
83 const keyboard = container.querySelector('.keyboard');
84 if (keyboard) applyStates(keyboard as HTMLElement, letterStates);
85}
86
87function applyStates(
88 root: HTMLElement,
89 letterStates: Readonly<Record<string, LetterState>>,
90): void {
91 root.querySelectorAll<HTMLButtonElement>('[data-key]').forEach((btn) => {
92 const key = btn.dataset['key']!;
93 const state = letterStates[key];
94 btn.classList.remove('keyboard__key--correct', 'keyboard__key--present', 'keyboard__key--absent');
95 if (state) btn.classList.add(`keyboard__key--${state}`);
96 });
97}