Skip to content

Instantly share code, notes, and snippets.

@alecslupu
Created February 10, 2026 01:15
Show Gist options
  • Select an option

  • Save alecslupu/61d6583487e75ba8e92e3a05ed7386d8 to your computer and use it in GitHub Desktop.

Select an option

Save alecslupu/61d6583487e75ba8e92e3a05ed7386d8 to your computer and use it in GitHub Desktop.
wcag toolbar
/**
* WCAG 2.1 Checker - Manual Individual Check Triggers
* Run each accessibility check independently
*/
const WCAG_CHECKS = [
{
id: 'images',
name: 'Images',
icon: '🖼️',
description: 'Alt text and decorative images',
focusArea: 'Images and visual content'
},
{
id: 'forms',
name: 'Forms',
icon: '📝',
description: 'Labels, fieldsets, and inputs',
focusArea: 'Form elements and input controls'
},
{
id: 'headings',
name: 'Headings',
icon: '📑',
description: 'Heading hierarchy and structure',
focusArea: 'Heading structure and hierarchy'
},
{
id: 'links',
name: 'Links',
icon: '🔗',
description: 'Link text and semantics',
focusArea: 'Links and navigation'
},
{
id: 'contrast',
name: 'Contrast',
icon: '🎨',
description: 'Color-only information and contrast',
focusArea: 'Color contrast and visual indicators'
},
{
id: 'aria',
name: 'ARIA',
icon: '♿',
description: 'ARIA roles and attributes',
focusArea: 'ARIA roles and attributes'
},
{
id: 'keyboard',
name: 'Keyboard',
icon: '⌨️',
description: 'Tab order and focus management',
focusArea: 'Keyboard navigation and focus'
},
{
id: 'semantic',
name: 'Semantic',
icon: '📐',
description: 'Proper use of semantic elements',
focusArea: 'Semantic HTML structure'
},
{
id: 'tables',
name: 'Tables',
icon: '📊',
description: 'Table structure and headers',
focusArea: 'Data tables'
},
{
id: 'language',
name: 'Language',
icon: '🌐',
description: 'Language attributes',
focusArea: 'Language attributes'
}
];
let fullDocumentHTML = '';
let issueCounter = 0;
let checkResults = new Map();
let runningChecks = new Set();
/**
* Initialize the toolbar
*/
function initWCAGToolbar() {
fullDocumentHTML = document.documentElement.outerHTML;
console.log(`📄 Document size: ${fullDocumentHTML.length} characters (~${Math.round(fullDocumentHTML.length / 4)} tokens)`);
showToolbar();
console.log('✅ WCAG Toolbar initialized. Click any check button to analyze that category.');
}
/**
* Run a single check
*/
async function runSingleCheck(checkId) {
if (runningChecks.has(checkId)) {
console.log(`⚠️ ${checkId} check already in progress`);
announceToScreenReader(`${checkId} check already in progress`);
return;
}
const check = WCAG_CHECKS.find(c => c.id === checkId);
if (!check) return;
runningChecks.add(checkId);
updateCheckButton(checkId, 'running');
updateTabStatus(checkId, 'loading');
// Show initial loading state in panel
const panel = document.getElementById(`wcag-panel-${checkId}`);
if (panel) {
panel.innerHTML = `
<div class="wcag-loading-state">
<div class="wcag-loading-icon" aria-hidden="true">⏳</div>
<h3 class="wcag-loading-title">Analyzing ${check.name}...</h3>
<p class="wcag-loading-description">This may take a few seconds depending on document size.</p>
</div>
`;
}
try {
const result = await analyzeSection(check);
checkResults.set(checkId, result);
updateTabStatus(checkId, 'complete');
updateCheckButton(checkId, 'complete');
updateTabContent(checkId, result);
updateSummary();
announceToScreenReader(`${check.name} check complete. Found ${result.count} issues.`);
} catch (error) {
console.error(`Error in ${check.name}:`, error);
const errorResult = { error: error.message };
checkResults.set(checkId, errorResult);
updateTabStatus(checkId, 'error');
updateCheckButton(checkId, 'error');
updateTabContent(checkId, errorResult);
announceToScreenReader(`${check.name} check failed: ${error.message}`);
} finally {
runningChecks.delete(checkId);
}
}
/**
* Run all checks
*/
async function runAllChecks() {
announceToScreenReader('Running all accessibility checks');
const promises = [];
for (const check of WCAG_CHECKS) {
promises.push(runSingleCheck(check.id));
}
await Promise.all(promises);
announceToScreenReader('All checks complete');
}
/**
* Analyze a single section with full document context
*/
async function analyzeSection(check) {
let htmlToAnalyze = fullDocumentHTML;
let note = '';
if (fullDocumentHTML.length > 200000) {
const headMatch = fullDocumentHTML.match(/<head[^>]*>[\s\S]*?<\/head>/i);
const bodyStart = fullDocumentHTML.substring(0, 150000);
const bodyEnd = fullDocumentHTML.substring(fullDocumentHTML.length - 10000);
htmlToAnalyze = headMatch ? headMatch[0] : '';
htmlToAnalyze += bodyStart + '\n<!-- ... document truncated for analysis ... -->\n' + bodyEnd;
note = `(Document truncated: ${fullDocumentHTML.length} → ${htmlToAnalyze.length} characters)`;
}
const prompt = `You are a WCAG 2.1 Level AA auditor. Analyze this COMPLETE HTML document focusing on: ${check.focusArea}
FOCUS AREAS FOR ${check.name.toUpperCase()}:
${getFocusInstructions(check.id)}
HTML DOCUMENT ${note}:
\`\`\`html
${htmlToAnalyze}
\`\`\`
OUTPUT FORMAT (one issue per line):
[SEVERITY] Element: <element_description> - Issue description
Fix: Remediation advice
SEVERITY: CRITICAL | WARNING | INFO
Element description should include: tag name, id, class, or unique identifier
Be specific and actionable. Provide 5-15 most important issues.`;
try {
const startTime = Date.now();
const response = await fetch('http://localhost:11434/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'llama3.2:1b',
prompt: prompt,
stream: false,
options: {
temperature: 0.2,
num_ctx: 32768,
num_predict: 2048,
}
}),
});
const data = await response.json();
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
console.log(`✓ ${check.name} completed in ${duration}s`);
const issues = parseIssuesFromFullDoc(data.response);
return {
checkId: check.id,
issues: issues,
count: issues.length,
rawResponse: data.response,
duration: duration
};
} catch (error) {
throw new Error(`Ollama request failed: ${error.message}`);
}
}
/**
* Get focus instructions for each check type
*/
function getFocusInstructions(checkId) {
const instructions = {
images: `- Missing alt attributes on <img> tags
- Empty alt on meaningful images
- Alt text that's just filenames (image.jpg)
- Decorative images without alt=""
- <svg> without proper labels
- Background images conveying information`,
forms: `- <input>, <textarea>, <select> without associated <label>
- Missing aria-label or aria-labelledby
- Required fields without indicators
- Radio/checkbox groups without <fieldset> and <legend>
- Submit buttons without clear labels
- Error messages not associated with fields`,
headings: `- Missing <h1> or multiple <h1> tags
- Skipped heading levels (h1 → h3)
- Empty headings
- Heading order not logical
- Headings used only for styling
- Content hierarchy unclear`,
links: `- Links with empty text content
- Generic link text ("click here", "read more")
- Links and buttons used incorrectly
- target="_blank" without warning
- Links without href
- Non-descriptive link text`,
contrast: `- Text with insufficient color contrast
- Links not distinguishable by more than color
- Form inputs relying only on color for state
- Color-coded information without text alternative
- Disabled elements with poor contrast
- Focus indicators with low contrast`,
aria: `- Invalid ARIA roles
- Required ARIA properties missing
- Redundant ARIA (role="button" on <button>)
- aria-hidden on focusable elements
- Incorrect ARIA relationships
- ARIA used instead of semantic HTML`,
keyboard: `- Missing tabindex on custom controls
- Positive tabindex values
- Elements with onclick but not keyboard accessible
- No visible focus indicators
- Focus traps
- Skip navigation links missing
- Modal dialogs that trap focus incorrectly`,
semantic: `- <div> or <span> with click handlers instead of <button>
- Missing landmark regions (<main>, <nav>, <header>, <footer>)
- Lists not using <ul>, <ol>, <li>
- <div> used instead of semantic elements
- Tables used for layout
- Improper nesting of elements`,
tables: `- Data tables without <th> elements
- Missing <caption> on tables
- <th> without scope attribute
- Complex tables without proper headers/ids
- Layout tables with table markup
- Missing summary for complex tables`,
language: `- Missing lang attribute on <html>
- Invalid language codes
- Content in different language without lang attribute
- Incorrect primary language specified`
};
return instructions[checkId] || 'Review for WCAG compliance';
}
/**
* Parse issues from AI response
*/
function parseIssuesFromFullDoc(text) {
const issues = [];
const lines = text.split('\n');
let currentIssue = null;
for (const line of lines) {
const issueMatch = line.match(/\[(CRITICAL|WARNING|INFO)\]\s*Element:\s*(.+?)\s*-\s*(.+)/i);
if (issueMatch) {
if (currentIssue) issues.push(currentIssue);
const [, severity, elementDesc, description] = issueMatch;
currentIssue = {
severity: severity.toUpperCase(),
issueId: issueCounter++,
elementDescription: elementDesc.trim(),
description: description.trim(),
fix: '',
element: findElementByDescription(elementDesc.trim())
};
} else if (currentIssue && line.trim().startsWith('Fix:')) {
currentIssue.fix = line.replace(/^Fix:\s*/i, '').trim();
} else if (currentIssue && line.trim() && !line.startsWith('[')) {
if (currentIssue.fix) {
currentIssue.fix += ' ' + line.trim();
}
}
}
if (currentIssue) issues.push(currentIssue);
return issues;
}
/**
* Try to find element by AI's description
*/
function findElementByDescription(description) {
const idMatch = description.match(/id="([^"]+)"/i) || description.match(/id:?\s*([^\s,]+)/i);
const classMatch = description.match(/class="([^"]+)"/i) || description.match(/class:?\s*([^\s,]+)/i);
const tagMatch = description.match(/<(\w+)/);
if (idMatch) {
const element = document.getElementById(idMatch[1]);
if (element) return element;
}
if (classMatch && tagMatch) {
const elements = document.querySelectorAll(`${tagMatch[1]}.${classMatch[1].split(' ')[0]}`);
if (elements.length > 0) return elements[0];
}
if (tagMatch) {
const elements = document.querySelectorAll(tagMatch[1]);
if (elements.length > 0) return elements[0];
}
return null;
}
/**
* Highlight element on page
*/
let currentHighlight = null;
function highlightElement(element) {
if (currentHighlight) {
currentHighlight.style.outline = '';
currentHighlight.style.outlineOffset = '';
}
if (element) {
element.style.outline = '3px solid #ff0000';
element.style.outlineOffset = '2px';
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
currentHighlight = element;
announceToScreenReader('Highlighting element');
} else {
announceToScreenReader('Element not found on page');
}
}
function unhighlightElement() {
if (currentHighlight) {
currentHighlight.style.outline = '';
currentHighlight.style.outlineOffset = '';
currentHighlight = null;
}
}
function announceToScreenReader(message) {
const announcement = document.getElementById('wcag-announcer');
if (announcement) {
announcement.textContent = message;
}
}
// ============= UI FUNCTIONS =============
function showToolbar() {
const existing = document.getElementById('wcag-toolbar');
if (existing) existing.remove();
const toolbar = document.createElement('aside');
toolbar.id = 'wcag-toolbar';
toolbar.setAttribute('role', 'complementary');
toolbar.setAttribute('aria-label', 'WCAG Accessibility Checker Toolbar');
toolbar.innerHTML = `
<style>
#wcag-toolbar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #1e1e1e;
color: #d4d4d4;
box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.5);
z-index: 999999;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
font-size: 14px;
border-top: 3px solid #0066cc;
}
#wcag-toolbar-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
background: #2d2d2d;
border-bottom: 1px solid #444;
}
#wcag-toolbar-title {
display: flex;
align-items: center;
gap: 12px;
margin: 0;
font-size: 16px;
font-weight: 600;
color: #ffffff;
}
#wcag-toolbar-actions {
display: flex;
gap: 10px;
align-items: center;
}
.wcag-btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 6px;
}
.wcag-btn:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.wcag-btn:focus {
outline: 3px solid #ffc107;
outline-offset: 2px;
}
.wcag-btn:active {
transform: translateY(0);
}
.wcag-btn-primary {
background: #0066cc;
color: white;
}
.wcag-btn-primary:hover {
background: #0052a3;
}
.wcag-btn-secondary {
background: #555;
color: white;
}
.wcag-btn-secondary:hover {
background: #666;
}
.wcag-btn-toggle {
background: #444;
color: white;
min-width: 120px;
}
.wcag-btn-toggle:hover {
background: #555;
}
.wcag-btn-danger {
background: #dc3545;
color: white;
}
.wcag-btn-danger:hover {
background: #c82333;
}
#wcag-toolbar-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-in-out;
}
#wcag-toolbar-content.expanded {
max-height: 70vh;
}
#wcag-tabs-wrapper {
background: #2d2d2d;
border-bottom: 2px solid #444;
overflow-x: auto;
overflow-y: hidden;
}
#wcag-tabs {
display: flex;
padding: 10px 20px 0 20px;
gap: 8px;
min-width: min-content;
}
#wcag-tabs::-webkit-scrollbar {
height: 6px;
}
#wcag-tabs::-webkit-scrollbar-track {
background: #2d2d2d;
}
#wcag-tabs::-webkit-scrollbar-thumb {
background: #555;
border-radius: 3px;
}
.wcag-tab {
padding: 10px 16px;
background: #3a3a3a;
border: 2px solid transparent;
border-radius: 6px 6px 0 0;
cursor: pointer;
color: #aaa;
font-size: 13px;
white-space: nowrap;
transition: all 0.2s;
flex-shrink: 0;
display: flex;
align-items: center;
gap: 6px;
}
.wcag-tab:hover {
background: #4a4a4a;
color: #fff;
}
.wcag-tab:focus {
outline: 3px solid #ffc107;
outline-offset: -3px;
border-color: #ffc107;
}
.wcag-tab[aria-selected="true"] {
background: #1e1e1e;
color: #fff;
border-bottom-color: #1e1e1e;
font-weight: 600;
}
.wcag-tab-status {
font-size: 16px;
line-height: 1;
}
.wcag-tab[data-status="loading"] .wcag-tab-status::after {
content: '⏳';
}
.wcag-tab[data-status="complete"] .wcag-tab-status::after {
content: '✓';
color: #28a745;
}
.wcag-tab[data-status="error"] .wcag-tab-status::after {
content: '✗';
color: #dc3545;
}
#wcag-panels {
padding: 20px;
overflow-y: auto;
max-height: calc(70vh - 120px);
background: #1e1e1e;
}
#wcag-panels::-webkit-scrollbar {
width: 10px;
}
#wcag-panels::-webkit-scrollbar-track {
background: #1e1e1e;
}
#wcag-panels::-webkit-scrollbar-thumb {
background: #555;
border-radius: 5px;
}
.wcag-panel {
display: none;
}
.wcag-panel[aria-hidden="false"] {
display: block;
animation: fadeIn 0.3s;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.wcag-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid #333;
}
.wcag-panel-info h3 {
margin: 0 0 5px 0;
color: #fff;
font-size: 20px;
}
.wcag-panel-info p {
margin: 0;
color: #888;
font-size: 13px;
}
.wcag-check-btn {
padding: 10px 20px;
background: #28a745;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 8px;
}
.wcag-check-btn:hover {
background: #218838;
transform: translateY(-1px);
}
.wcag-check-btn:focus {
outline: 3px solid #ffc107;
outline-offset: 2px;
}
.wcag-check-btn:disabled {
background: #555;
cursor: not-allowed;
opacity: 0.6;
}
.wcag-check-btn[data-status="running"] {
background: #ffc107;
color: #000;
}
.wcag-check-btn[data-status="complete"] {
background: #17a2b8;
}
.wcag-check-btn[data-status="error"] {
background: #dc3545;
}
.wcag-issue-list {
list-style: none;
padding: 0;
margin: 0;
}
.wcag-issue-item {
background: #2d2d2d;
padding: 16px;
margin-bottom: 12px;
border-radius: 8px;
border-left: 4px solid #666;
transition: all 0.2s;
}
.wcag-issue-item:hover,
.wcag-issue-item:focus-within {
background: #3a3a3a;
transform: translateX(4px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.wcag-issue-item.critical {
border-left-color: #dc3545;
}
.wcag-issue-item.warning {
border-left-color: #ffc107;
}
.wcag-issue-item.info {
border-left-color: #17a2b8;
}
.wcag-issue-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.wcag-severity-badge {
padding: 4px 10px;
border-radius: 4px;
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
}
.wcag-severity-badge.critical {
background: #dc3545;
color: white;
}
.wcag-severity-badge.warning {
background: #ffc107;
color: #000;
}
.wcag-severity-badge.info {
background: #17a2b8;
color: white;
}
.wcag-element-desc {
font-family: 'Courier New', monospace;
font-size: 11px;
color: #61dafb;
background: #1a1a1a;
padding: 3px 8px;
border-radius: 4px;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wcag-issue-description {
color: #fff;
line-height: 1.6;
margin-bottom: 10px;
}
.wcag-issue-fix {
font-size: 13px;
color: #aaa;
padding-top: 10px;
border-top: 1px solid #444;
line-height: 1.5;
}
.wcag-issue-fix::before {
content: '💡 ';
}
.wcag-highlight-btn {
background: transparent;
border: 1px solid #555;
color: #61dafb;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
margin-top: 8px;
transition: all 0.2s;
}
.wcag-highlight-btn:hover {
background: #2a2a2a;
border-color: #61dafb;
}
.wcag-highlight-btn:focus {
outline: 2px solid #ffc107;
outline-offset: 2px;
}
.wcag-highlight-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
#wcag-summary {
padding: 20px;
background: #2d2d2d;
border-radius: 8px;
margin-bottom: 20px;
}
#wcag-summary h3 {
margin: 0 0 15px 0;
font-size: 18px;
color: #fff;
}
.wcag-stats {
display: flex;
gap: 30px;
flex-wrap: wrap;
}
.wcag-stat {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.wcag-stat-value {
font-size: 24px;
font-weight: bold;
color: #fff;
}
.wcag-stat.critical .wcag-stat-value {
color: #dc3545;
}
.wcag-stat.warning .wcag-stat-value {
color: #ffc107;
}
.wcag-stat.info .wcag-stat-value {
color: #17a2b8;
}
.wcag-empty-state {
text-align: center;
padding: 60px 20px;
color: #888;
}
.wcag-empty-state-icon {
font-size: 64px;
margin-bottom: 16px;
}
.wcag-empty-state-title {
font-size: 18px;
color: #aaa;
margin-bottom: 8px;
}
.wcag-empty-state-description {
font-size: 14px;
line-height: 1.6;
}
.wcag-loading-state {
text-align: center;
padding: 60px 20px;
color: #ffc107;
}
.wcag-loading-icon {
font-size: 64px;
margin-bottom: 16px;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.wcag-loading-title {
font-size: 18px;
color: #ffc107;
margin-bottom: 8px;
}
.wcag-loading-description {
font-size: 14px;
color: #888;
}
.wcag-success-state {
text-align: center;
padding: 40px 20px;
color: #28a745;
}
.wcag-sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
#wcag-announcer {
position: absolute;
left: -10000px;
width: 1px;
height: 1px;
overflow: hidden;
}
.wcag-doc-info {
font-size: 12px;
color: #888;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #333;
}
</style>
<header id="wcag-toolbar-header">
<h2 id="wcag-toolbar-title">
<span aria-hidden="true">♿</span>
<span>WCAG 2.1 Manual Checker</span>
</h2>
<div id="wcag-toolbar-actions">
<button
id="wcag-run-all-btn"
class="wcag-btn wcag-btn-secondary"
aria-label="Run all accessibility checks"
onclick="runAllChecks()">
<span aria-hidden="true">▶▶</span>
<span>Run All Checks</span>
</button>
<button
id="wcag-toggle-btn"
class="wcag-btn wcag-btn-toggle"
aria-expanded="false"
aria-controls="wcag-toolbar-content"
aria-label="Toggle results panel">
<span id="wcag-toggle-text">Show Panel</span>
<span aria-hidden="true">▲</span>
</button>
<button
id="wcag-close-btn"
class="wcag-btn wcag-btn-danger"
aria-label="Close WCAG toolbar">
<span aria-hidden="true">✕</span>
<span class="wcag-sr-only">Close</span>
</button>
</div>
</header>
<div id="wcag-toolbar-content" aria-live="polite">
<nav id="wcag-tabs-wrapper" aria-label="Accessibility check categories">
<div id="wcag-tabs" role="tablist" aria-label="WCAG check categories">
${WCAG_CHECKS.map((check, index) => `
<button
id="wcag-tab-${check.id}"
class="wcag-tab"
role="tab"
aria-selected="${index === 0 ? 'true' : 'false'}"
aria-controls="wcag-panel-${check.id}"
tabindex="${index === 0 ? '0' : '-1'}"
data-tab="${check.id}"
data-status="">
<span class="wcag-tab-icon" aria-hidden="true">${check.icon}</span>
<span class="wcag-tab-label">${check.name}</span>
<span class="wcag-tab-status" aria-live="polite" aria-atomic="true"></span>
</button>
`).join('')}
</div>
</nav>
<div id="wcag-panels">
<div id="wcag-summary" aria-live="polite" aria-atomic="true">
<h3>Overall Summary</h3>
<p style="color: #888; margin: 10px 0;">Click individual check buttons to analyze specific categories, or use "Run All Checks" to analyze everything.</p>
<div class="wcag-stats" style="margin-top: 20px;">
<div class="wcag-stat">
<span class="wcag-stat-value" id="summary-checks-run">0</span>
<span>Checks Run</span>
</div>
</div>
<p class="wcag-doc-info">
Document size: ${fullDocumentHTML.length.toLocaleString()} characters
</p>
</div>
${WCAG_CHECKS.map((check, index) => `
<section
id="wcag-panel-${check.id}"
class="wcag-panel"
role="tabpanel"
aria-labelledby="wcag-tab-${check.id}"
aria-hidden="${index === 0 ? 'false' : 'true'}"
tabindex="0">
<div class="wcag-panel-header">
<div class="wcag-panel-info">
<h3><span aria-hidden="true">${check.icon}</span> ${check.name}</h3>
<p>${check.description}</p>
</div>
<button
id="wcag-check-btn-${check.id}"
class="wcag-check-btn"
data-check-id="${check.id}"
data-status="ready"
onclick="runSingleCheck('${check.id}')"
aria-label="Run ${check.name} check">
<span aria-hidden="true">▶</span>
<span class="wcag-check-btn-text">Run Check</span>
</button>
</div>
<div class="wcag-empty-state">
<div class="wcag-empty-state-icon" aria-hidden="true">👆</div>
<h4 class="wcag-empty-state-title">Not Analyzed Yet</h4>
<p class="wcag-empty-state-description">Click "Run Check" above to analyze ${check.name.toLowerCase()} in the document.</p>
</div>
</section>
`).join('')}
</div>
</div>
<div id="wcag-announcer" role="status" aria-live="polite" aria-atomic="true"></div>
`;
document.body.appendChild(toolbar);
setupEventListeners();
setupKeyboardNavigation();
}
function setupEventListeners() {
const toggleBtn = document.getElementById('wcag-toggle-btn');
const content = document.getElementById('wcag-toolbar-content');
toggleBtn.addEventListener('click', () => {
const isExpanded = content.classList.toggle('expanded');
toggleBtn.setAttribute('aria-expanded', isExpanded);
document.getElementById('wcag-toggle-text').textContent = isExpanded ? 'Hide Panel' : 'Show Panel';
announceToScreenReader(isExpanded ? 'Panel expanded' : 'Panel collapsed');
});
document.getElementById('wcag-close-btn').addEventListener('click', () => {
document.getElementById('wcag-toolbar').remove();
unhighlightElement();
announceToScreenReader('WCAG toolbar closed');
});
document.querySelectorAll('.wcag-tab').forEach(tab => {
tab.addEventListener('click', () => {
selectTab(tab.getAttribute('data-tab'));
});
});
}
function setupKeyboardNavigation() {
const tablist = document.getElementById('wcag-tabs');
const tabs = Array.from(document.querySelectorAll('.wcag-tab'));
tablist.addEventListener('keydown', (e) => {
const currentTab = document.activeElement;
const currentIndex = tabs.indexOf(currentTab);
let targetTab = null;
switch(e.key) {
case 'ArrowRight':
case 'ArrowDown':
e.preventDefault();
targetTab = tabs[(currentIndex + 1) % tabs.length];
break;
case 'ArrowLeft':
case 'ArrowUp':
e.preventDefault();
targetTab = tabs[(currentIndex - 1 + tabs.length) % tabs.length];
break;
case 'Home':
e.preventDefault();
targetTab = tabs[0];
break;
case 'End':
e.preventDefault();
targetTab = tabs[tabs.length - 1];
break;
}
if (targetTab) {
selectTab(targetTab.getAttribute('data-tab'));
targetTab.focus();
}
});
}
function selectTab(tabId) {
document.querySelectorAll('.wcag-tab').forEach(tab => {
const isSelected = tab.getAttribute('data-tab') === tabId;
tab.setAttribute('aria-selected', isSelected);
tab.setAttribute('tabindex', isSelected ? '0' : '-1');
});
document.querySelectorAll('.wcag-panel').forEach(panel => {
const isVisible = panel.id === `wcag-panel-${tabId}`;
panel.setAttribute('aria-hidden', !isVisible);
});
const activeTab = document.querySelector(`[data-tab="${tabId}"]`);
if (activeTab) {
activeTab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
}
// Auto-expand toolbar when tab is selected
const content = document.getElementById('wcag-toolbar-content');
const toggleBtn = document.getElementById('wcag-toggle-btn');
if (!content.classList.contains('expanded')) {
content.classList.add('expanded');
toggleBtn.setAttribute('aria-expanded', 'true');
document.getElementById('wcag-toggle-text').textContent = 'Hide Panel';
}
unhighlightElement();
}
function updateCheckButton(checkId, status) {
const btn = document.getElementById(`wcag-check-btn-${checkId}`);
if (!btn) return;
const textSpan = btn.querySelector('.wcag-check-btn-text');
btn.setAttribute('data-status', status);
switch(status) {
case 'running':
btn.disabled = true;
textSpan.textContent = 'Analyzing...';
btn.querySelector('span[aria-hidden]').textContent = '⏳';
break;
case 'complete':
btn.disabled = false;
textSpan.textContent = 'Re-run Check';
btn.querySelector('span[aria-hidden]').textContent = '🔄';
break;
case 'error':
btn.disabled = false;
textSpan.textContent = 'Retry Check';
btn.querySelector('span[aria-hidden]').textContent = '⚠️';
break;
default:
btn.disabled = false;
textSpan.textContent = 'Run Check';
btn.querySelector('span[aria-hidden]').textContent = '▶';
}
}
function updateTabStatus(checkId, status) {
const tab = document.querySelector(`[data-tab="${checkId}"]`);
if (tab) {
tab.setAttribute('data-status', status);
}
}
function updateTabContent(checkId, result) {
const panel = document.getElementById(`wcag-panel-${checkId}`);
if (!panel) return;
// Keep the header
const header = panel.querySelector('.wcag-panel-header');
const headerHTML = header ? header.outerHTML : '';
if (result.error) {
panel.innerHTML = headerHTML + `
<div class="wcag-empty-state">
<div class="wcag-empty-state-icon" aria-hidden="true">❌</div>
<h3 class="wcag-empty-state-title">Error</h3>
<p class="wcag-empty-state-description">${result.error}</p>
</div>
`;
return;
}
if (result.count === 0) {
panel.innerHTML = headerHTML + `
<div class="wcag-success-state">
<div class="wcag-empty-state-icon" aria-hidden="true">✅</div>
<h3 class="wcag-empty-state-title">No Issues Found</h3>
<p class="wcag-empty-state-description">No accessibility issues detected in this category.</p>
<p class="wcag-doc-info">Analysis completed in ${result.duration}s</p>
</div>
`;
return;
}
const issuesHTML = headerHTML + `
<h3 style="color: #fff; margin-bottom: 16px;">
Found ${result.count} Issue${result.count !== 1 ? 's' : ''}
<span style="font-size: 14px; font-weight: normal; color: #888;">(${result.duration}s)</span>
</h3>
<ul class="wcag-issue-list">
${result.issues.map(issue => `
<li class="wcag-issue-item ${issue.severity.toLowerCase()}">
<div class="wcag-issue-header">
<span class="wcag-severity-badge ${issue.severity.toLowerCase()}">${issue.severity}</span>
<code class="wcag-element-desc" title="${issue.elementDescription}">${issue.elementDescription}</code>
</div>
<div class="wcag-issue-description">${issue.description}</div>
${issue.fix ? `<div class="wcag-issue-fix">${issue.fix}</div>` : ''}
<button
class="wcag-highlight-btn"
onclick="highlightElementById(${issue.issueId})"
${!issue.element ? 'disabled title="Element not found in DOM"' : ''}
aria-label="Highlight element on page">
${issue.element ? 'Highlight Element' : 'Element Not Found'}
</button>
</li>
`).join('')}
</ul>
`;
panel.innerHTML = issuesHTML;
}
function updateSummary() {
let totalCritical = 0;
let totalWarning = 0;
let totalInfo = 0;
let checksRun = checkResults.size;
checkResults.forEach(result => {
if (result.issues) {
result.issues.forEach(issue => {
if (issue.severity === 'CRITICAL') totalCritical++;
else if (issue.severity === 'WARNING') totalWarning++;
else if (issue.severity === 'INFO') totalInfo++;
});
}
});
document.getElementById('summary-checks-run').textContent = checksRun;
const summary = document.getElementById('wcag-summary');
if (checksRun === 0) return;
summary.innerHTML = `
<h3>Overall Summary</h3>
<div class="wcag-stats">
<div class="wcag-stat">
<span class="wcag-stat-value">${checksRun}</span>
<span>Checks Run</span>
</div>
<div class="wcag-stat critical">
<span class="wcag-stat-value">${totalCritical}</span>
<span>Critical</span>
</div>
<div class="wcag-stat warning">
<span class="wcag-stat-value">${totalWarning}</span>
<span>Warnings</span>
</div>
<div class="wcag-stat info">
<span class="wcag-stat-value">${totalInfo}</span>
<span>Info</span>
</div>
</div>
<p class="wcag-doc-info">
${checksRun} of ${WCAG_CHECKS.length} checks completed • Document: ${fullDocumentHTML.length.toLocaleString()} chars
</p>
`;
}
// Store issue elements for highlighting
const issueElements = new Map();
function highlightElementById(issueId) {
let element = null;
checkResults.forEach(result => {
if (result.issues) {
const issue = result.issues.find(i => i.issueId === issueId);
if (issue && issue.element) {
element = issue.element;
}
}
});
highlightElement(element);
}
// Make functions globally accessible
window.runSingleCheck = runSingleCheck;
window.runAllChecks = runAllChecks;
window.highlightElementById = highlightElementById;
// Initialize toolbar
console.log('🚀 Initializing WCAG Manual Checker...');
initWCAGToolbar();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment