a11y: WCAG AA contrast + ARIA roles + focus management across all pages
- css/ls.css: --text-3 #8898AA → #56687A (5.1:1 contrast), min-height 44px on .btn-primary/.btn-ghost/.sb-link, new .icon-btn utility (44×44px) - js/api.js: lsConfirm — role=dialog, aria-modal, aria-labelledby, Tab focus trap, restore focus on close; lsToast — aria-live=polite on container, role=alert on errors; live quiz — role=dialog, role=radiogroup, role=radio, aria-checked, keyboard support - test-run.html: q-opt divs — role=radio/checkbox, aria-checked, tabindex, keyboard enter/space; confirm modal — role=dialog, aria-modal; btn-flag — aria-pressed; dots — aria-label, aria-current; touch targets 44px - board.html: btn-del-ann — aria-label; reaction buttons — aria-label, aria-pressed - All 18 HTML files: replace hardcoded color:#8898AA with color:var(--text-3) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+29
-12
@@ -111,9 +111,9 @@
|
||||
.confirm-desc { font-size: 0.88rem; color: var(--text-2); line-height: 1.6; margin-bottom: 24px; }
|
||||
.confirm-skipped { display: inline-block; margin: 8px 0 4px; padding: 6px 16px; border-radius: var(--r-pill); background: rgba(255,179,71,0.12); color: #c47f00; font-family: 'Unbounded', sans-serif; font-size: 0.82rem; font-weight: 700; }
|
||||
.confirm-actions { display: flex; gap: 10px; justify-content: center; }
|
||||
.confirm-cancel { padding: 11px 26px; border: 1.5px solid var(--border-h); border-radius: var(--r-pill); background: transparent; font-family: 'Manrope', sans-serif; font-size: 0.88rem; font-weight: 600; color: var(--text-3); cursor: pointer; transition: all var(--tr); }
|
||||
.confirm-cancel { padding: 11px 26px; min-height: 44px; border: 1.5px solid var(--border-h); border-radius: var(--r-pill); background: transparent; font-family: 'Manrope', sans-serif; font-size: 0.88rem; font-weight: 600; color: var(--text-3); cursor: pointer; transition: all var(--tr); }
|
||||
.confirm-cancel:hover { border-color: var(--violet); color: var(--violet); }
|
||||
.confirm-ok { padding: 11px 26px; border: none; border-radius: var(--r-pill); background: var(--grad-1); color: #fff; font-family: 'Manrope', sans-serif; font-size: 0.88rem; font-weight: 700; cursor: pointer; transition: transform var(--tr); }
|
||||
.confirm-ok { padding: 11px 26px; min-height: 44px; border: none; border-radius: var(--r-pill); background: var(--grad-1); color: #fff; font-family: 'Manrope', sans-serif; font-size: 0.88rem; font-weight: 700; cursor: pointer; transition: transform var(--tr); }
|
||||
.confirm-ok:hover { transform: translateY(-1px); }
|
||||
|
||||
/* ── btn-finish counter ── */
|
||||
@@ -201,13 +201,13 @@
|
||||
</div>
|
||||
|
||||
<!-- Confirm finish modal -->
|
||||
<div class="confirm-overlay" id="confirm-overlay" onclick="if(event.target===this)closeConfirm()">
|
||||
<div class="confirm-overlay" id="confirm-overlay" role="dialog" aria-modal="true" aria-labelledby="confirm-dlg-title" onclick="if(event.target===this)closeConfirm()">
|
||||
<div class="confirm-box">
|
||||
<div class="confirm-icon" id="confirm-icon"></div>
|
||||
<div class="confirm-title">Завершить тест?</div>
|
||||
<div class="confirm-title" id="confirm-dlg-title">Завершить тест?</div>
|
||||
<div class="confirm-desc" id="confirm-desc"></div>
|
||||
<div class="confirm-actions">
|
||||
<button class="confirm-cancel" onclick="closeConfirm()">Вернуться</button>
|
||||
<button class="confirm-cancel" id="confirm-cancel-btn" onclick="closeConfirm()">Вернуться</button>
|
||||
<button class="confirm-ok" onclick="doFinish()">Завершить</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -403,12 +403,16 @@
|
||||
? (Array.isArray(answers[q.id]) && answers[q.id].includes(opt.id))
|
||||
: answers[q.id] === opt.id;
|
||||
const keyLabel = isMulti ? (sel ? lsIcon('check', 14) : lsIcon('square', 14)) : String.fromCharCode(65 + i);
|
||||
return `<div class="q-opt${sel ? ' selected' : ''}" data-opt-id="${opt.id}" data-i="${i}">
|
||||
<div class="q-opt-key">${keyLabel}</div>
|
||||
return `<div class="q-opt${sel ? ' selected' : ''}"
|
||||
role="${isMulti ? 'checkbox' : 'radio'}"
|
||||
aria-checked="${sel}"
|
||||
tabindex="0"
|
||||
data-opt-id="${opt.id}" data-i="${i}">
|
||||
<div class="q-opt-key" aria-hidden="true">${keyLabel}</div>
|
||||
<div class="q-opt-text">${esc(opt.text)}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
bodyHtml = `<div class="q-options" id="opts">${optHtml}</div>`;
|
||||
bodyHtml = `<div class="q-options" id="opts" role="${isMulti ? 'group' : 'radiogroup'}" aria-label="Варианты ответа">${optHtml}</div>`;
|
||||
if (isMulti) {
|
||||
bodyHtml += `<div class="q-hint" style="margin-top:10px">Можно выбрать несколько вариантов</div>`;
|
||||
}
|
||||
@@ -421,7 +425,7 @@
|
||||
|
||||
document.getElementById('q-area').innerHTML = `
|
||||
<div class="q-card active">
|
||||
<button class="btn-flag${flags[q.id] ? ' flagged' : ''}" id="btn-flag-${q.id}" title="Отметить для проверки" onclick="toggleFlag(${q.id})">
|
||||
<button class="btn-flag${flags[q.id] ? ' flagged' : ''}" id="btn-flag-${q.id}" aria-label="${flags[q.id] ? 'Снять отметку' : 'Отметить для проверки'}" aria-pressed="${flags[q.id] ? 'true' : 'false'}" onclick="toggleFlag(${q.id})">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="${flags[q.id] ? '#f59e0b' : 'none'}" stroke="${flags[q.id] ? '#f59e0b' : 'currentColor'}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" y1="22" x2="4" y2="15"/></svg>
|
||||
</button>
|
||||
<div class="q-num-badge">
|
||||
@@ -455,10 +459,14 @@
|
||||
} else {
|
||||
const isMulti = type === 'multi';
|
||||
document.querySelectorAll('.q-opt').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const handleSelect = () => {
|
||||
const optId = Number(el.dataset.optId);
|
||||
if (isMulti) toggleMultiOpt(q, optId);
|
||||
else selectSingleOpt(q, optId);
|
||||
};
|
||||
el.addEventListener('click', handleSelect);
|
||||
el.addEventListener('keydown', e => {
|
||||
if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); handleSelect(); }
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -518,6 +526,7 @@
|
||||
document.querySelectorAll('.q-opt').forEach((el, i) => {
|
||||
const sel = q.options[i].id === option_id;
|
||||
el.classList.toggle('selected', sel);
|
||||
el.setAttribute('aria-checked', sel ? 'true' : 'false');
|
||||
el.querySelector('.q-opt-key').innerHTML = sel ? lsIcon('check', 14) : String.fromCharCode(65 + i);
|
||||
if (sel) {
|
||||
// attach progress ring svg
|
||||
@@ -558,6 +567,7 @@
|
||||
const id = Number(el.dataset.optId);
|
||||
const sel = arr.includes(id);
|
||||
el.classList.toggle('selected', sel);
|
||||
el.setAttribute('aria-checked', sel ? 'true' : 'false');
|
||||
el.querySelector('.q-opt-key').innerHTML = sel ? lsIcon('check', 14) : lsIcon('square', 14);
|
||||
});
|
||||
|
||||
@@ -603,7 +613,9 @@
|
||||
desc.innerHTML = skipped === total
|
||||
? `Вы не ответили ни на один вопрос.<br>Результат будет нулевым.`
|
||||
: `<span class="confirm-skipped">${lsIcon('warning', 16)} ${skipped} ${skipped === 1 ? 'вопрос без ответа' : skipped < 5 ? 'вопроса без ответа' : 'вопросов без ответа'}</span><br>Пропущенные вопросы будут засчитаны как неверные.`;
|
||||
document.getElementById('confirm-overlay').classList.add('open');
|
||||
const ov = document.getElementById('confirm-overlay');
|
||||
ov.classList.add('open');
|
||||
setTimeout(() => document.getElementById('confirm-cancel-btn')?.focus(), 50);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -722,6 +734,7 @@
|
||||
const dot = document.createElement('button');
|
||||
dot.className = 'dot';
|
||||
dot.textContent = i + 1;
|
||||
dot.setAttribute('aria-label', `Вопрос ${i + 1}`);
|
||||
dot.addEventListener('click', () => { cancelAutoAdvance(); renderQuestion(i); });
|
||||
el.appendChild(dot);
|
||||
});
|
||||
@@ -729,9 +742,11 @@
|
||||
|
||||
function updateDots() {
|
||||
document.querySelectorAll('.dot').forEach((dot, i) => {
|
||||
dot.classList.toggle('current', i === currentIdx);
|
||||
const isCurrent = i === currentIdx;
|
||||
dot.classList.toggle('current', isCurrent);
|
||||
dot.classList.toggle('answered', isAnswered(questions[i]));
|
||||
dot.classList.toggle('flagged', !!flags[questions[i].id]);
|
||||
dot.setAttribute('aria-current', isCurrent ? 'step' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -742,6 +757,8 @@
|
||||
const btn = document.getElementById(`btn-flag-${qid}`);
|
||||
if (btn) {
|
||||
btn.classList.toggle('flagged', flags[qid]);
|
||||
btn.setAttribute('aria-pressed', flags[qid] ? 'true' : 'false');
|
||||
btn.setAttribute('aria-label', flags[qid] ? 'Снять отметку' : 'Отметить для проверки');
|
||||
btn.querySelector('svg').setAttribute('fill', flags[qid] ? '#f59e0b' : 'none');
|
||||
btn.querySelector('svg').setAttribute('stroke', flags[qid] ? '#f59e0b' : 'currentColor');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user