clone

a browser grid puzzle game where you push words around to rewrite the rules of each level

Expose pack notes to screen readers

Dami committed f92b270
commit
f92b270685d13109ab1bc38f2360b03c50ee6aba
parent
d99d0b0d494bf2d1f3c3a47d134d87fdf482c3c3
author
Dami <dami.etoile@gmail.com>
6 files changed, +47 -6
Msrc/main.js6+++++-
Msrc/scenes/gift.js10+++++++++-
Msrc/scenes/workshop.js13++++++++++++-
Msrc/stage.js22+++++++++++++++++++---
Mtests/gift.test.js1+
Mtests/workshop.test.js1+
M src/main.js +5 -1
M src/scenes/gift.js +9 -1
@@ -88,6 +88,14 @@ export function createGiftScene({ pack = null, collision = false, warnings: init
8888 const result = copyToClipboard(textToCopy);8989 return { ok: true, action: 'copy', text: textToCopy, copied: result };9090 }91+ function accessibilityText() {92+ if (!open || !preview) return 'Gift closed.';93+ const levels = levelList(preview);94+ const intro = `A pack for you. ${packName(preview)}. ${levels.length} ${levels.length === 1 ? 'proof' : 'proofs'}.`;95+ const notes = warnings.length ? ` ${warnings.length} ${warnings.length === 1 ? 'note' : 'notes'}: ${[...new Set(warnings.map(warning => warning.word))].join(', ')}. Press I for details or C to copy.` : '';96+ const details = detailsOpen ? ` ${warnings.slice(0, 5).map(warning => `${warning.levelTitle || warning.levelId || 'Proof'}: ${warning.message}`).join(' ')}${warnings.length > 5 ? ` And ${warnings.length - 5} more.` : ''}` : '';97+ return `${intro}${notes}${details} ${choices()[selected] === 'keep' ? 'Keep and play selected.' : `${choices()[selected]} selected.`}`;98+ }9199 function move(direction) { const values = choices(); if (direction === 'up' || direction === 'left') selected = (selected - 1 + values.length) % values.length; if (direction === 'down' || direction === 'right') selected = (selected + 1) % values.length; return values[selected]; }92100 function input(action) {93101 if (!open) return { ok: false, action: null };
@@ -232,7 +240,7 @@ export function createGiftScene({ pack = null, collision = false, warnings: init
232240 });233241 return { x: left, y: top, width: cardWidth, height: cardHeight, narrow, preview: { x: previewX, y: previewY, width: previewWidth, height: previewHeight }, actions: buttons.map(button => ({ ...button })), hintActions: hintButtons.map(button => ({ ...button })), hints: { x: hintStart, width: hintTotal }, hintY: hintBottom - rowHeight / 2, hintRows: hintRows.length, selected: selectedAction, now };234242 }235- const api = { show, hide, setPack, choose, input, hit, draw, get open() { return open; }, get mode() { return mode; }, get selected() { return choices()[selected]; }, get pack() { return preview ? cloneData(preview) : null; }, get collision() { return collides ? cloneData(collides) : null; }, get warnings() { return cloneData(warnings); }, get detailsOpen() { return detailsOpen; } };243+ const api = { show, hide, setPack, choose, input, hit, draw, accessibilityText, get open() { return open; }, get mode() { return mode; }, get selected() { return choices()[selected]; }, get pack() { return preview ? cloneData(preview) : null; }, get collision() { return collides ? cloneData(collides) : null; }, get warnings() { return cloneData(warnings); }, get detailsOpen() { return detailsOpen; } };236244 return Object.freeze(api);237245 }238246
M src/scenes/workshop.js +12 -1
@@ -80,6 +80,17 @@ export function createWorkshopScene(options = {}) {
8080 confirm: confirm ? { ...confirm, choices: [...confirm.choices] } : null,8181 };8282 }83+ function accessibilityText() {84+ const row = selectedRow();85+ const pack = row?.pack;86+ const selection = pack87+ ? `${pack.name || pack.manifest?.name || pack.id}. ${pack.levelCount} ${pack.levelCount === 1 ? 'proof' : 'proofs'}. ${pack.warnings.length} ${pack.warnings.length === 1 ? 'note' : 'notes'}. ${row.actions[actionIndex]} selected.`88+ : 'Import pack selected.';89+ const details = note90+ ? ` ${note.kind === 'warning' ? 'Pack notes' : 'Pack errors'}. ${note.visible.join(' ')}${note.remaining ? ` And ${note.remaining} more.` : ''} Press C to copy details or Escape to close.`91+ : '';92+ return `Workshop. ${selection}${details}`;93+ }8394 function setFeedback(message, kind = 'info') { feedback = { message, kind }; note = null; return view(); }8495 function showErrors(errors) {8596 const entries = Array.isArray(errors) ? errors : [errors];
@@ -308,7 +319,7 @@ export function createWorkshopScene(options = {}) {
308319 309320 return Object.freeze({310321 id: 'workshop', eyebrow: 'WORKSHOP', title: "Packs of proofs you've added", prompt: 'IMPORT | PLAY | SHARE | EXPORT', help: 'ARROWS CHOOSE ENTER SELECT ESC BACK',311- draw, view, refresh, move, handleInput, hit, activate, copyDetails, showErrors, showWarnings,322+ draw, view, accessibilityText, refresh, move, handleInput, hit, activate, copyDetails, showErrors, showWarnings,312323 get selectedRow() { return rowIndex; }, get selectedAction() { return actionIndex; },313324 get hitLayout() { return hitLayout ? cloneData(hitLayout) : null; },314325 });
M src/stage.js +19 -3
@@ -167,6 +167,8 @@ export function createStage(options = {}) {
167167 h = innerHeight;168168 ctx.fillStyle = "#0F0E0D";169169 ctx.fillRect(0, 0, w, h);170+ if (scene !== "proof" && scene !== "gift" && scene !== "workshop")171+ canvas.setAttribute("aria-label", `Miau Is True ${scene}`);170172 if (scene === "title" && title) {171173 const drawn = title.draw(ctx, {172174 width: w,
@@ -186,10 +188,12 @@ export function createStage(options = {}) {
186188 }187189 if (scene === "gift" && gift) {188190 gift.draw(ctx, performance.now(), { width: w, height: h });191+ canvas.setAttribute("aria-label", gift.accessibilityText?.() || "Miau Is True Gift");189192 return;190193 }191194 if (scene === "workshop" && workshop) {192195 workshop.draw(ctx, { width: w, height: h });196+ canvas.setAttribute("aria-label", workshop.accessibilityText?.() || "Miau Is True Workshop");193197 return;194198 }195199 if (scene === "proof" && boardState) {
@@ -291,7 +295,9 @@ export function createStage(options = {}) {
291295 choices.hidden = true;292296 root.hidden = true;293297 root.dataset.scene = id;294- announce(`${spec.eyebrow || ""} ${titleEl.textContent}`.trim());298+ announce(id === "gift" && gift?.accessibilityText ? gift.accessibilityText()299+ : id === "workshop" && workshop?.accessibilityText ? workshop.accessibilityText()300+ : `${spec.eyebrow || ""} ${titleEl.textContent}`.trim());295301 render();296302 if (!reducedMotion && (id === "title" || id === "win" || id === "index"))297303 requestAnimationFrame(animateVisuals);
@@ -401,9 +407,12 @@ export function createStage(options = {}) {
401407 return false;402408 }403409 if (scene === "workshop" && workshop) {404- workshop.handleInput(410+ const outcome = workshop.handleInput(405411 type === "confirm" ? "enter" : type === "menu" ? "escape" : intent.dir || type,406412 );413+ Promise.resolve(outcome).then(result => {414+ if (result?.event?.type === "notes") announce(workshop.accessibilityText?.());415+ });407416 render();408417 return true;409418 }
@@ -412,6 +421,7 @@ export function createStage(options = {}) {
412421 type === "confirm" ? "confirm" : type === "menu" ? "escape" : intent.dir || type,413422 );414423 if (result.action === "later") closeGift();424+ if (result.action === "details") announce(gift.accessibilityText?.());415425 render();416426 return true;417427 }
@@ -603,8 +613,14 @@ export function createStage(options = {}) {
603613 if (scene === "gift") {604614 const result = gift?.hit(x, y);605615 if (result?.action === "later") closeGift();616+ if (result?.action === "details") announce(gift.accessibilityText?.());617+ }618+ if (scene === "workshop") {619+ const outcome = workshop?.hit?.(x, y);620+ Promise.resolve(outcome).then(result => {621+ if (result?.event?.type === "notes") announce(workshop.accessibilityText?.());622+ });606623 }607- if (scene === "workshop") workshop?.hit?.(x, y);608624 if (609625 scene === "pause" &&610626 x >= innerWidth / 2 - 160 &&
M tests/gift.test.js +1 -0
M tests/workshop.test.js +1 -0