feat(trainer): UX-пасс 2 — клавиатура для вариантов + доступность (a11y)

- Клавиатура: в режиме выбора (choice/verify) клавиши 1–9 выбирают вариант;
  после ответа Enter/пробел → «Дальше» (и для текстовых задач тоже). Не мешает
  вводу в полях (input/textarea игнорируются).
- Доступность: #tr-feedback aria-live="polite" (скринридер озвучивает «Верно/
  Неверно»); кнопки-варианты получили aria-label «Вариант N: …»; #tr-choices
  role="group"; тумблеры сложности и «Текст/На чертеже» — aria-pressed; после
  ответа фокус переходит на «Дальше».

Логика проверки/ID не изменены. Inline-скрипт парсится.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-29 20:12:37 +03:00
parent a9cd5804ae
commit c164627087
+22 -7
View File
@@ -490,11 +490,11 @@
placeholder="ответ" aria-label="Ваш ответ"/> placeholder="ответ" aria-label="Ваш ответ"/>
<button class="tr-btn tr-primary" id="tr-check" type="button">Проверить</button> <button class="tr-btn tr-primary" id="tr-check" type="button">Проверить</button>
</div> </div>
<div class="tr-choices" id="tr-choices" style="display:none"></div> <div class="tr-choices" id="tr-choices" role="group" aria-label="Варианты ответа" style="display:none"></div>
<button class="tr-btn tr-primary" id="tr-choice-next" type="button" style="display:none">Дальше</button> <button class="tr-btn tr-primary" id="tr-choice-next" type="button" style="display:none">Дальше</button>
<div class="tr-keypad" id="tr-keypad"></div> <div class="tr-keypad" id="tr-keypad"></div>
<div class="tr-preview" id="tr-preview"></div> <div class="tr-preview" id="tr-preview"></div>
<div class="tr-feedback" id="tr-feedback"></div> <div class="tr-feedback" id="tr-feedback" aria-live="polite"></div>
</div> </div>
<div id="tr-stepbox" style="display:none"> <div id="tr-stepbox" style="display:none">
@@ -705,8 +705,8 @@
if (!(cur && cur.figure && cur.figurePrompt)) { box.style.display = 'none'; box.innerHTML = ''; return; } if (!(cur && cur.figure && cur.figurePrompt)) { box.style.display = 'none'; box.innerHTML = ''; return; }
box.style.display = ''; box.style.display = '';
box.innerHTML = '<span class="tr-fm-label">Условие</span>' + box.innerHTML = '<span class="tr-fm-label">Условие</span>' +
'<button type="button" class="tr-fm-btn' + (!figureMode ? ' on' : '') + '" data-fm="0">Текст</button>' + '<button type="button" class="tr-fm-btn' + (!figureMode ? ' on' : '') + '" data-fm="0" aria-pressed="' + (!figureMode) + '">Текст</button>' +
'<button type="button" class="tr-fm-btn' + (figureMode ? ' on' : '') + '" data-fm="1">На чертеже</button>'; '<button type="button" class="tr-fm-btn' + (figureMode ? ' on' : '') + '" data-fm="1" aria-pressed="' + figureMode + '">На чертеже</button>';
} }
var topics = (TG.topics ? TG.topics() : [{ key: null, label: 'Задачи' }]).concat([{ key: 'word', label: 'Текстовые задачи', word: true }]); var topics = (TG.topics ? TG.topics() : [{ key: null, label: 'Задачи' }]).concat([{ key: 'word', label: 'Текстовые задачи', word: true }]);
@@ -906,7 +906,8 @@
var autoLvl = (diffMode === 'auto') ? (' · ур.' + levelOf(curGen)) : ''; var autoLvl = (diffMode === 'auto') ? (' · ур.' + levelOf(curGen)) : '';
el.innerHTML = '<span class="tr-diff-label">Сложность</span>' + opts.map(function (o) { el.innerHTML = '<span class="tr-diff-label">Сложность</span>' + opts.map(function (o) {
var lbl = (o[0] === 'auto') ? ('Авто' + autoLvl) : o[1]; var lbl = (o[0] === 'auto') ? ('Авто' + autoLvl) : o[1];
return '<button class="tr-diff-btn' + (String(diffMode) === String(o[0]) ? ' on' : '') + '" type="button" data-d="' + o[0] + '">' + lbl + '</button>'; var on = String(diffMode) === String(o[0]);
return '<button class="tr-diff-btn' + (on ? ' on' : '') + '" type="button" data-d="' + o[0] + '" aria-pressed="' + on + '">' + lbl + '</button>';
}).join(''); }).join('');
} }
// общие эффекты «задача решена» (из обычного ответа и из пошагового режима) // общие эффекты «задача решена» (из обычного ответа и из пошагового режима)
@@ -1036,7 +1037,8 @@
var box = $('tr-choices'); if (!box) return; var box = $('tr-choices'); if (!box) return;
if (!cur || !cur.choices) { box.innerHTML = ''; return; } if (!cur || !cur.choices) { box.innerHTML = ''; return; }
box.innerHTML = cur.choices.map(function (c, i) { box.innerHTML = cur.choices.map(function (c, i) {
return '<button class="tr-choice-btn" type="button" data-ci="' + i + '">' + esc(c.label) + '</button>'; return '<button class="tr-choice-btn" type="button" data-ci="' + i +
'" aria-label="Вариант ' + (i + 1) + ': ' + esc(c.label) + '">' + esc(c.label) + '</button>';
}).join(''); }).join('');
} }
function submitChoice(idx) { function submitChoice(idx) {
@@ -1061,7 +1063,7 @@
recordAnswer(false); submitAttempt(false); recordAnswer(false); submitAttempt(false);
revealSolution(); revealSolution();
} }
var nx = $('tr-choice-next'); if (nx) nx.style.display = ''; var nx = $('tr-choice-next'); if (nx) { nx.style.display = ''; try { nx.focus(); } catch (e) {} }
updateStats(); updateStats();
} }
// Текст ответа в фидбеке/раскрытии — по типу задачи. // Текст ответа в фидбеке/раскрытии — по типу задачи.
@@ -1431,6 +1433,19 @@
}); });
$('tr-choice-next').addEventListener('click', advance); $('tr-choice-next').addEventListener('click', advance);
$('tr-skip').addEventListener('click', newProblem); $('tr-skip').addEventListener('click', newProblem);
// Клавиатура: в режиме выбора 1–9 выбирают вариант; после ответа Enter/пробел → «Дальше».
document.addEventListener('keydown', function (e) {
if (!cur) return;
var tag = (e.target && e.target.tagName || '').toLowerCase();
if (tag === 'input' || tag === 'textarea' || tag === 'select') return; // не мешаем вводу/полям
var isChoice = (cur.kind === 'choice' || cur.kind === 'verify');
if (isChoice && !answered && cur.choices && e.key >= '1' && e.key <= '9') {
var idx = +e.key - 1;
if (idx < cur.choices.length) { e.preventDefault(); submitChoice(idx); }
return;
}
if (answered && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); advance(); }
});
// ИИ-репетитор: после неверного ответа — разбор ошибки; иначе — наводящая подсказка. // ИИ-репетитор: после неверного ответа — разбор ошибки; иначе — наводящая подсказка.
// Опирается на известный ответ + шаги (движок), ИИ только ОБЪЯСНЯЕТ. Недоступен ИИ → решение. // Опирается на известный ответ + шаги (движок), ИИ только ОБЪЯСНЯЕТ. Недоступен ИИ → решение.
function aiExplain() { function aiExplain() {