pdf-accessibility/js/utils.js
Vadym Samoilenko 304526a8c4 Fix 13 WCAG accessibility violations in the checker UI itself
HTML:
- Move <div id="msalConfig"> out of <head> (invalid HTML)
- Add skip-to-main-content link (WCAG 2.4.1)
- Wrap content in <main id="main-content">
- Auth overlay: aria-modal, aria-describedby, aria-describedby on p
- Microsoft SVG: aria-hidden="true" (decorative)
- Tab buttons: aria-controls; panels: role=tabpanel, aria-labelledby
- Score number: <div> → <output> element
- Marker legend: role=legend (invalid) → role=region + aria-label
- Reset zoom button: aria-label added

CSS:
- input:focus outline:none → outline:2px solid accent (WCAG 2.4.7)
- --text-muted #696969 → #5a5a5a (~5.5:1 contrast, was 4.35:1)
- Skip link styles (visible on focus)
- @media (prefers-reduced-motion: reduce) disables all animations

JS:
- upload.js/batch.js: keydown Enter/Space activates upload areas (WCAG 2.1.1)
- results.js: issue cards get role=listitem inside role=list
- results.js: filterIssues() updates aria-pressed on all filter buttons
- results.js: displayResults() focuses resultsSection for screen readers
- utils.js: aria-valuenow set on role=progressbar element, not fill div

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 14:19:20 +00:00

151 lines
5.4 KiB
JavaScript

/* Utility functions — logging, progress, theme */
function addLog(message, type = 'info') {
const logContent = document.getElementById('logContent');
const entry = document.createElement('div');
entry.className = `log-entry ${type}`;
entry.setAttribute('role', type === 'error' ? 'alert' : 'status');
const timestamp = new Date().toLocaleTimeString();
entry.innerHTML = `<strong>${timestamp}</strong> ${message}`;
logContent.appendChild(entry);
logContent.scrollTop = logContent.scrollHeight;
}
function clearLog() {
const logContent = document.getElementById('logContent');
logContent.innerHTML = '<div class="log-entry" role="status">Initializing...</div>';
}
function updateProgress(percent, message) {
const fill = document.getElementById('progressFill');
const pct = document.getElementById('progressPercent');
const txt = document.getElementById('progressText');
fill.style.width = percent + '%';
const progressBar = document.getElementById('progressContainer');
if (progressBar) progressBar.setAttribute('aria-valuenow', percent);
pct.textContent = percent + '%';
txt.textContent = message;
}
/* Dark mode toggle */
function toggleDarkMode() {
const root = document.documentElement;
const isDark = root.getAttribute('data-theme') === 'dark';
root.setAttribute('data-theme', isDark ? 'light' : 'dark');
localStorage.setItem('theme', isDark ? 'light' : 'dark');
const btn = document.getElementById('themeToggle');
if (btn) btn.textContent = isDark ? 'Dark' : 'Light';
}
function loadTheme() {
const saved = localStorage.getItem('theme');
if (saved === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
const btn = document.getElementById('themeToggle');
if (btn) btn.textContent = 'Light';
}
}
/* Severity helpers */
function getSeverityColor(severity) {
const map = { CRITICAL: '#dc2626', ERROR: '#ef4444', WARNING: '#f59e0b', INFO: '#3b82f6', SUCCESS: '#10b981' };
return map[severity] || '#3b82f6';
}
function getSeverityIcon(severity) {
const map = { CRITICAL: '\u{1F6A8}', ERROR: '\u274C', WARNING: '\u26A0\uFE0F', INFO: '\u2139\uFE0F', SUCCESS: '\u2705' };
return map[severity] || '\u2022';
}
/* WCAG 2.1 criterion → Understanding page slug */
const WCAG_SLUGS = {
'1.1.1': 'non-text-content',
'1.2.1': 'audio-only-and-video-only-prerecorded',
'1.2.2': 'captions-prerecorded',
'1.2.3': 'audio-description-or-media-alternative-prerecorded',
'1.2.4': 'captions-live',
'1.2.5': 'audio-description-prerecorded',
'1.3.1': 'info-and-relationships',
'1.3.2': 'meaningful-sequence',
'1.3.3': 'sensory-characteristics',
'1.3.4': 'orientation',
'1.3.5': 'identify-input-purpose',
'1.4.1': 'use-of-color',
'1.4.2': 'audio-control',
'1.4.3': 'contrast-minimum',
'1.4.4': 'resize-text',
'1.4.5': 'images-of-text',
'1.4.6': 'contrast-enhanced',
'1.4.10': 'reflow',
'1.4.11': 'non-text-contrast',
'1.4.12': 'text-spacing',
'1.4.13': 'content-on-hover-or-focus',
'2.1.1': 'keyboard',
'2.1.2': 'no-keyboard-trap',
'2.2.1': 'timing-adjustable',
'2.2.2': 'pause-stop-hide',
'2.3.1': 'three-flashes-or-below-threshold',
'2.4.1': 'bypass-blocks',
'2.4.2': 'page-titled',
'2.4.3': 'focus-order',
'2.4.4': 'link-purpose-in-context',
'2.4.5': 'multiple-ways',
'2.4.6': 'headings-and-labels',
'2.4.7': 'focus-visible',
'2.5.3': 'label-in-name',
'3.1.1': 'language-of-page',
'3.1.2': 'language-of-parts',
'3.1.5': 'reading-level',
'3.2.1': 'on-focus',
'3.2.2': 'on-input',
'3.2.3': 'consistent-navigation',
'3.2.4': 'consistent-identification',
'3.3.1': 'error-identification',
'3.3.2': 'labels-or-instructions',
'3.3.3': 'error-suggestion',
'3.3.4': 'error-prevention-legal-financial-data',
'4.1.1': 'parsing',
'4.1.2': 'name-role-value',
'4.1.3': 'status-messages',
};
/**
* Returns an HTML string of clickable WCAG criterion links.
* Handles comma-separated criteria (e.g. "1.3.1, 4.1.2") and "PDF/UA".
*/
function wcagCriterionLinks(criterion) {
if (!criterion) return '';
if (criterion.trim().toUpperCase() === 'PDF/UA') {
return '<a href="https://www.pdfa.org/pdfua/" target="_blank" rel="noopener" class="wcag-link">PDF/UA</a>';
}
return criterion.split(',').map(part => {
const num = part.trim();
const slug = WCAG_SLUGS[num];
if (slug) {
const url = `https://www.w3.org/WAI/WCAG21/Understanding/${slug}`;
return `<a href="${url}" target="_blank" rel="noopener" class="wcag-link">WCAG ${num}</a>`;
}
return `WCAG ${num}`;
}).join(', ');
}
function escapeAttr(str) {
return String(str).replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '&quot;');
}
function getCategoryIcon(category) {
const icons = {
'Document Structure': '\u{1F3D7}\uFE0F', 'Metadata': '\u{1F4CB}', 'Language': '\u{1F310}',
'Text Accessibility': '\u{1F4DD}', 'Images': '\u{1F5BC}\uFE0F', 'Color Contrast': '\u{1F3A8}',
'Readability': '\u{1F4DA}', 'Link Text': '\u{1F517}', 'Forms': '\u{1F4C4}',
'Tables': '\u{1F4CA}', 'Headings': '\u{1F4D1}', 'Navigation': '\u{1F9ED}',
'Fonts': '\u{1F524}', 'Security': '\u{1F512}', 'OCR Quality': '\u{1F50D}'
};
const key = Object.keys(icons).find(k => category.includes(k));
return key ? icons[key] : '\u{1F4CC}';
}