UNPKG

lwc-linter

Version:

A comprehensive CLI tool for linting Lightning Web Components v8.0.0+ with modern LWC patterns, decorators, lifecycle hooks, and Salesforce platform integration

252 lines 11.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.loadAccessibilityRules = loadAccessibilityRules; function loadAccessibilityRules() { return [ { name: 'aria-required', description: 'Ensure interactive elements have appropriate ARIA attributes', category: 'accessibility', severity: 'error', fixable: true, check: (content, filePath, config) => { const issues = []; if (!filePath.endsWith('.html')) return issues; const lines = content.split('\n'); // Check for buttons without aria-label or aria-labelledby lines.forEach((line, index) => { if (line.includes('<button') && !line.includes('aria-label')) { const hasTextContent = line.includes('>') && line.includes('</button>'); const textMatch = line.match(/>([^<]+)</); if (!hasTextContent || !textMatch || textMatch[1].trim().length === 0) { issues.push({ rule: 'aria-required', message: 'Button elements must have an accessible name via aria-label or text content', severity: 'error', line: index + 1, fixable: true, category: 'accessibility' }); } } // Check for input elements without labels if (line.includes('<input') && !line.includes('aria-label') && !line.includes('aria-labelledby')) { issues.push({ rule: 'aria-required', message: 'Input elements must have an accessible name via aria-label, aria-labelledby, or associated label', severity: 'error', line: index + 1, fixable: false, category: 'accessibility' }); } }); return issues; }, fix: (content, issues) => { let fixedContent = content; // Add aria-label to buttons without text content fixedContent = fixedContent.replace(/<button([^>]*?)>/g, (match, attributes) => { if (!attributes.includes('aria-label')) { return `<button${attributes} aria-label="Button">`; } return match; }); return fixedContent; } }, { name: 'alt-text-required', description: 'Ensure images have alt text for screen readers', category: 'accessibility', severity: 'error', fixable: true, check: (content, filePath, config) => { const issues = []; if (!filePath.endsWith('.html')) return issues; const lines = content.split('\n'); lines.forEach((line, index) => { if (line.includes('<img') && !line.includes('alt=')) { issues.push({ rule: 'alt-text-required', message: 'Image elements must have alt text for accessibility', severity: 'error', line: index + 1, fixable: true, category: 'accessibility' }); } // Check for empty alt text on decorative images if (line.includes('<img') && line.includes('alt=""')) { // This is actually correct for decorative images, but we'll flag for review issues.push({ rule: 'alt-text-required', message: 'Empty alt text detected. Ensure this image is purely decorative', severity: 'info', line: index + 1, fixable: false, category: 'accessibility' }); } }); return issues; }, fix: (content, issues) => { return content.replace(/<img([^>]*?)(?!alt=)([^>]*?)>/g, '<img$1 alt="Image description"$2>'); } }, { name: 'color-contrast', description: 'Ensure sufficient color contrast for text elements', category: 'accessibility', severity: 'warn', fixable: false, check: (content, filePath, config) => { const issues = []; if (!filePath.endsWith('.css')) return issues; const lines = content.split('\n'); // Check for low contrast color combinations const lowContrastColors = [ { bg: '#ffffff', text: '#cccccc' }, { bg: '#f0f0f0', text: '#888888' }, { bg: '#000000', text: '#333333' } ]; lines.forEach((line, index) => { // Simple check for color properties if (line.includes('color:') || line.includes('background-color:')) { // This is a simplified check - in a real implementation, // you'd calculate actual contrast ratios if (line.includes('#ccc') || line.includes('#ddd') || line.includes('#eee')) { issues.push({ rule: 'color-contrast', message: 'Potential low color contrast detected. Ensure text meets WCAG contrast requirements', severity: 'warn', line: index + 1, fixable: false, category: 'accessibility' }); } } }); return issues; } }, { name: 'keyboard-navigation', description: 'Ensure proper keyboard navigation support', category: 'accessibility', severity: 'warn', fixable: false, check: (content, filePath, config) => { const issues = []; if (!filePath.endsWith('.html')) return issues; const lines = content.split('\n'); lines.forEach((line, index) => { // Check for clickable elements without proper keyboard support if ((line.includes('onclick=') || line.includes('@click')) && !line.includes('tabindex') && !line.includes('<button') && !line.includes('<a ')) { issues.push({ rule: 'keyboard-navigation', message: 'Clickable elements should be keyboard accessible. Consider using button or adding tabindex', severity: 'warn', line: index + 1, fixable: false, category: 'accessibility' }); } // Check for tabindex values greater than 0 const tabindexMatch = line.match(/tabindex="([^"]+)"/); if (tabindexMatch && parseInt(tabindexMatch[1]) > 0) { issues.push({ rule: 'keyboard-navigation', message: 'Avoid positive tabindex values as they can disrupt natural tab order', severity: 'warn', line: index + 1, fixable: false, category: 'accessibility' }); } }); return issues; } }, { name: 'focus-management', description: 'Ensure proper focus management in dynamic content', category: 'accessibility', severity: 'warn', fixable: false, check: (content, filePath, config) => { const issues = []; if (!filePath.endsWith('.js')) return issues; const lines = content.split('\n'); lines.forEach((line, index) => { // Check for dynamic content changes without focus management if ((line.includes('innerHTML') || line.includes('appendChild')) && !content.includes('focus()')) { issues.push({ rule: 'focus-management', message: 'Consider managing focus when dynamically updating content', severity: 'info', line: index + 1, fixable: false, category: 'accessibility' }); } }); return issues; } }, { name: 'semantic-html', description: 'Encourage use of semantic HTML elements', category: 'accessibility', severity: 'warn', fixable: false, check: (content, filePath, config) => { const issues = []; if (!filePath.endsWith('.html')) return issues; const lines = content.split('\n'); lines.forEach((line, index) => { // Check for div elements that could be semantic elements if (line.includes('<div class="header"') || line.includes('<div class="nav"')) { issues.push({ rule: 'semantic-html', message: 'Consider using semantic HTML elements like <header>, <nav>, <main>, <section>', severity: 'info', line: index + 1, fixable: false, category: 'accessibility' }); } // Check for missing heading hierarchy const headingMatch = line.match(/<h([1-6])/); if (headingMatch) { const level = parseInt(headingMatch[1]); // This would need more sophisticated checking for proper hierarchy if (level > 3) { issues.push({ rule: 'semantic-html', message: 'Consider if this heading level maintains proper hierarchy (h1 → h2 → h3)', severity: 'info', line: index + 1, fixable: false, category: 'accessibility' }); } } }); return issues; } } ]; } //# sourceMappingURL=accessibility.js.map