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
JavaScript
;
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