gbu-accessibility-package 3.3.0 โ 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.js +23 -1
- package/demo/nested-controls-test.html +92 -0
- package/lib/fixer.js +361 -0
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -23,6 +23,7 @@ const options = {
|
|
|
23
23
|
langOnly: false,
|
|
24
24
|
roleOnly: false,
|
|
25
25
|
formsOnly: false,
|
|
26
|
+
nestedOnly: false,
|
|
26
27
|
buttonsOnly: false,
|
|
27
28
|
linksOnly: false,
|
|
28
29
|
landmarksOnly: false,
|
|
@@ -80,6 +81,9 @@ for (let i = 0; i < args.length; i++) {
|
|
|
80
81
|
case '--forms-only':
|
|
81
82
|
options.formsOnly = true;
|
|
82
83
|
break;
|
|
84
|
+
case '--nested-only':
|
|
85
|
+
options.nestedOnly = true;
|
|
86
|
+
break;
|
|
83
87
|
case '--buttons-only':
|
|
84
88
|
options.buttonsOnly = true;
|
|
85
89
|
break;
|
|
@@ -227,7 +231,7 @@ async function main() {
|
|
|
227
231
|
try {
|
|
228
232
|
// Handle different modes - All modes now include cleanup
|
|
229
233
|
if (options.cleanupOnly || options.altOnly || options.langOnly || options.roleOnly ||
|
|
230
|
-
options.formsOnly || options.buttonsOnly || options.linksOnly || options.landmarksOnly ||
|
|
234
|
+
options.formsOnly || options.nestedOnly || options.buttonsOnly || options.linksOnly || options.landmarksOnly ||
|
|
231
235
|
options.headingsOnly || options.brokenLinksOnly) {
|
|
232
236
|
// Individual modes - handle each separately, then run cleanup
|
|
233
237
|
} else {
|
|
@@ -333,6 +337,24 @@ async function main() {
|
|
|
333
337
|
showCompletionMessage(options, 'Form label fixes + cleanup');
|
|
334
338
|
return;
|
|
335
339
|
|
|
340
|
+
} else if (options.nestedOnly) {
|
|
341
|
+
// Fix nested interactive controls + cleanup
|
|
342
|
+
console.log(chalk.blue('๐ฏ Running nested interactive controls fixes + cleanup...'));
|
|
343
|
+
const nestedResults = await fixer.fixNestedInteractiveControls(options.directory);
|
|
344
|
+
const nestedFixed = nestedResults.filter(r => r.status === 'fixed').length;
|
|
345
|
+
const totalNestedIssues = nestedResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
346
|
+
|
|
347
|
+
console.log(chalk.green(`\nโ
Fixed nested interactive controls in ${nestedFixed} files (${totalNestedIssues} issues)`));
|
|
348
|
+
|
|
349
|
+
// Run cleanup
|
|
350
|
+
console.log(chalk.blue('\n๐งน Running cleanup for duplicate role attributes...'));
|
|
351
|
+
const cleanupResults = await fixer.cleanupDuplicateRoles(options.directory);
|
|
352
|
+
const cleanupFixed = cleanupResults.filter(r => r.status === 'fixed').length;
|
|
353
|
+
console.log(chalk.green(`โ
Cleaned duplicate roles in ${cleanupFixed} files`));
|
|
354
|
+
|
|
355
|
+
showCompletionMessage(options, 'Nested interactive controls fixes + cleanup');
|
|
356
|
+
return;
|
|
357
|
+
|
|
336
358
|
} else if (options.buttonsOnly) {
|
|
337
359
|
// Fix button names + cleanup
|
|
338
360
|
console.log(chalk.blue('๐ Running button name fixes + cleanup...'));
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="ja">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Nested Interactive Controls Test - Accessibility Issues</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<h1>Nested Interactive Controls Test Cases</h1>
|
|
10
|
+
|
|
11
|
+
<!-- Test Case 1: div[role="button"] containing links (like in the axe error) -->
|
|
12
|
+
<div class="card-buttons" role="button">
|
|
13
|
+
<a href="https://business.mobile.rakuten.co.jp/solution/service/rakuten-ai-for-business/?scid=we_solution09_2504" class="btn btn-secondary" target="_blank" role="link">่ฉณ็ดฐใ่ฆใ</a>
|
|
14
|
+
<a href="https://business.mobile.rakuten.co.jp/solution/ai/inquiry/?l=id=solution_ai_inquiry1&scid=we_solution10_2504" class="btn btn-primary" target="_blank" role="link">ใๅใๅใใ</a>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<!-- Test Case 2: Button containing links -->
|
|
18
|
+
<button type="button" onclick="handleClick()">
|
|
19
|
+
<a href="/page1">Link inside button</a>
|
|
20
|
+
<span>Click me</span>
|
|
21
|
+
</button>
|
|
22
|
+
|
|
23
|
+
<!-- Test Case 3: Link containing button -->
|
|
24
|
+
<a href="/page2">
|
|
25
|
+
<button type="button">Button inside link</button>
|
|
26
|
+
</a>
|
|
27
|
+
|
|
28
|
+
<!-- Test Case 4: div[role="button"] containing input -->
|
|
29
|
+
<div role="button" tabindex="0" onclick="submit()">
|
|
30
|
+
<input type="text" placeholder="Search...">
|
|
31
|
+
<span>Submit</span>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<!-- Test Case 5: Link containing select -->
|
|
35
|
+
<a href="/settings">
|
|
36
|
+
<select name="language">
|
|
37
|
+
<option value="ja">Japanese</option>
|
|
38
|
+
<option value="en">English</option>
|
|
39
|
+
</select>
|
|
40
|
+
Settings
|
|
41
|
+
</a>
|
|
42
|
+
|
|
43
|
+
<!-- Test Case 6: Button containing textarea -->
|
|
44
|
+
<button type="submit">
|
|
45
|
+
<textarea name="comment" placeholder="Enter comment"></textarea>
|
|
46
|
+
<span>Send</span>
|
|
47
|
+
</button>
|
|
48
|
+
|
|
49
|
+
<!-- Test Case 7: Multiple levels of nesting -->
|
|
50
|
+
<div role="button" tabindex="0">
|
|
51
|
+
<div class="container">
|
|
52
|
+
<a href="/nested">
|
|
53
|
+
<button type="button">Deeply nested</button>
|
|
54
|
+
</a>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<!-- Test Case 8: div[role="button"] with tabindex containing interactive elements -->
|
|
59
|
+
<div role="button" tabindex="0" onclick="handleAction()">
|
|
60
|
+
<input type="checkbox" id="agree">
|
|
61
|
+
<label for="agree">I agree</label>
|
|
62
|
+
<a href="/terms">Terms</a>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<!-- Test Case 9: Link containing details/summary -->
|
|
66
|
+
<a href="/info">
|
|
67
|
+
<details>
|
|
68
|
+
<summary>More info</summary>
|
|
69
|
+
<p>Details content</p>
|
|
70
|
+
</details>
|
|
71
|
+
</a>
|
|
72
|
+
|
|
73
|
+
<!-- Test Case 10: Form elements nested in buttons -->
|
|
74
|
+
<button type="button" class="form-button">
|
|
75
|
+
<input type="radio" name="choice" value="1">
|
|
76
|
+
<input type="radio" name="choice" value="2">
|
|
77
|
+
<span>Choose option</span>
|
|
78
|
+
</button>
|
|
79
|
+
|
|
80
|
+
<!-- Test Case 11: Correct structure (should not be flagged) -->
|
|
81
|
+
<div class="card-buttons">
|
|
82
|
+
<a href="/page1" class="btn btn-secondary">่ฉณ็ดฐใ่ฆใ</a>
|
|
83
|
+
<a href="/page2" class="btn btn-primary">ใๅใๅใใ</a>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<!-- Test Case 12: Another correct structure -->
|
|
87
|
+
<button type="button" onclick="handleClick()">
|
|
88
|
+
<span>Click me</span>
|
|
89
|
+
<i class="icon"></i>
|
|
90
|
+
</button>
|
|
91
|
+
</body>
|
|
92
|
+
</html>
|
package/lib/fixer.js
CHANGED
|
@@ -4093,6 +4093,367 @@ class AccessibilityFixer {
|
|
|
4093
4093
|
return fixed;
|
|
4094
4094
|
}
|
|
4095
4095
|
|
|
4096
|
+
async fixNestedInteractiveControls(directory = '.') {
|
|
4097
|
+
console.log(chalk.blue('๐ฏ Fixing nested interactive controls...'));
|
|
4098
|
+
|
|
4099
|
+
const htmlFiles = await this.findHtmlFiles(directory);
|
|
4100
|
+
const results = [];
|
|
4101
|
+
let totalIssuesFound = 0;
|
|
4102
|
+
|
|
4103
|
+
for (const file of htmlFiles) {
|
|
4104
|
+
try {
|
|
4105
|
+
const content = await fs.readFile(file, 'utf8');
|
|
4106
|
+
const issues = this.analyzeNestedInteractiveControls(content);
|
|
4107
|
+
|
|
4108
|
+
if (issues.length > 0) {
|
|
4109
|
+
console.log(chalk.cyan(`\n๐ ${file}:`));
|
|
4110
|
+
issues.forEach(issue => {
|
|
4111
|
+
console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
|
|
4112
|
+
if (issue.suggestion) {
|
|
4113
|
+
console.log(chalk.gray(` ๐ก ${issue.suggestion}`));
|
|
4114
|
+
}
|
|
4115
|
+
totalIssuesFound++;
|
|
4116
|
+
});
|
|
4117
|
+
}
|
|
4118
|
+
|
|
4119
|
+
const fixed = this.fixNestedInteractiveControlsInContent(content);
|
|
4120
|
+
|
|
4121
|
+
if (fixed !== content) {
|
|
4122
|
+
if (this.config.backupFiles) {
|
|
4123
|
+
await fs.writeFile(`${file}.backup`, content);
|
|
4124
|
+
}
|
|
4125
|
+
|
|
4126
|
+
if (!this.config.dryRun) {
|
|
4127
|
+
await fs.writeFile(file, fixed);
|
|
4128
|
+
}
|
|
4129
|
+
|
|
4130
|
+
console.log(chalk.green(`โ
Fixed nested interactive controls in: ${file}`));
|
|
4131
|
+
results.push({ file, status: 'fixed', issues: issues.length });
|
|
4132
|
+
} else {
|
|
4133
|
+
results.push({ file, status: 'no-change', issues: issues.length });
|
|
4134
|
+
}
|
|
4135
|
+
} catch (error) {
|
|
4136
|
+
console.error(chalk.red(`โ Error processing ${file}: ${error.message}`));
|
|
4137
|
+
results.push({ file, status: 'error', error: error.message });
|
|
4138
|
+
}
|
|
4139
|
+
}
|
|
4140
|
+
|
|
4141
|
+
console.log(chalk.blue(`\n๐ Summary: Found ${totalIssuesFound} nested interactive control issues across ${results.length} files`));
|
|
4142
|
+
return results;
|
|
4143
|
+
}
|
|
4144
|
+
|
|
4145
|
+
analyzeNestedInteractiveControls(content) {
|
|
4146
|
+
const issues = [];
|
|
4147
|
+
|
|
4148
|
+
// Define interactive elements and their roles
|
|
4149
|
+
const interactiveElements = [
|
|
4150
|
+
{ tag: 'button', role: 'button' },
|
|
4151
|
+
{ tag: 'a', role: 'link', requiresHref: true },
|
|
4152
|
+
{ tag: 'input', role: 'textbox|button|checkbox|radio|slider|spinbutton' },
|
|
4153
|
+
{ tag: 'textarea', role: 'textbox' },
|
|
4154
|
+
{ tag: 'select', role: 'combobox|listbox' },
|
|
4155
|
+
{ tag: 'details', role: 'group' },
|
|
4156
|
+
{ tag: 'summary', role: 'button' }
|
|
4157
|
+
];
|
|
4158
|
+
|
|
4159
|
+
// Also check for elements with interactive roles
|
|
4160
|
+
const interactiveRoles = [
|
|
4161
|
+
'button', 'link', 'textbox', 'checkbox', 'radio', 'slider',
|
|
4162
|
+
'spinbutton', 'combobox', 'listbox', 'menuitem', 'tab',
|
|
4163
|
+
'treeitem', 'gridcell', 'option'
|
|
4164
|
+
];
|
|
4165
|
+
|
|
4166
|
+
// Find all interactive elements
|
|
4167
|
+
const interactiveSelectors = [];
|
|
4168
|
+
|
|
4169
|
+
// Add tag-based selectors
|
|
4170
|
+
interactiveElements.forEach(element => {
|
|
4171
|
+
if (element.requiresHref) {
|
|
4172
|
+
interactiveSelectors.push(`<${element.tag}[^>]*href[^>]*>`);
|
|
4173
|
+
} else {
|
|
4174
|
+
interactiveSelectors.push(`<${element.tag}[^>]*>`);
|
|
4175
|
+
}
|
|
4176
|
+
});
|
|
4177
|
+
|
|
4178
|
+
// Add role-based selectors
|
|
4179
|
+
interactiveRoles.forEach(role => {
|
|
4180
|
+
interactiveSelectors.push(`<[^>]*role\\s*=\\s*["']${role}["'][^>]*>`);
|
|
4181
|
+
});
|
|
4182
|
+
|
|
4183
|
+
// Create combined regex pattern
|
|
4184
|
+
const interactivePattern = new RegExp(interactiveSelectors.join('|'), 'gi');
|
|
4185
|
+
|
|
4186
|
+
// Find all interactive elements with their positions
|
|
4187
|
+
const interactiveMatches = [];
|
|
4188
|
+
let match;
|
|
4189
|
+
|
|
4190
|
+
while ((match = interactivePattern.exec(content)) !== null) {
|
|
4191
|
+
const element = match[0];
|
|
4192
|
+
const startPos = match.index;
|
|
4193
|
+
const endPos = this.findElementEndPosition(content, element, startPos);
|
|
4194
|
+
|
|
4195
|
+
if (endPos > startPos) {
|
|
4196
|
+
interactiveMatches.push({
|
|
4197
|
+
element: element,
|
|
4198
|
+
startPos: startPos,
|
|
4199
|
+
endPos: endPos,
|
|
4200
|
+
fullElement: content.substring(startPos, endPos)
|
|
4201
|
+
});
|
|
4202
|
+
}
|
|
4203
|
+
}
|
|
4204
|
+
|
|
4205
|
+
// Check for nesting
|
|
4206
|
+
for (let i = 0; i < interactiveMatches.length; i++) {
|
|
4207
|
+
const parent = interactiveMatches[i];
|
|
4208
|
+
|
|
4209
|
+
for (let j = 0; j < interactiveMatches.length; j++) {
|
|
4210
|
+
if (i === j) continue;
|
|
4211
|
+
|
|
4212
|
+
const child = interactiveMatches[j];
|
|
4213
|
+
|
|
4214
|
+
// Check if child is nested inside parent
|
|
4215
|
+
if (child.startPos > parent.startPos && child.endPos < parent.endPos) {
|
|
4216
|
+
const parentType = this.getInteractiveElementType(parent.element);
|
|
4217
|
+
const childType = this.getInteractiveElementType(child.element);
|
|
4218
|
+
|
|
4219
|
+
issues.push({
|
|
4220
|
+
type: '๐ฏ Nested interactive controls',
|
|
4221
|
+
description: `${childType} is nested inside ${parentType}`,
|
|
4222
|
+
parentElement: parent.element.substring(0, 100) + '...',
|
|
4223
|
+
childElement: child.element.substring(0, 100) + '...',
|
|
4224
|
+
suggestion: `Remove interactive role from parent or child, or restructure HTML to avoid nesting`
|
|
4225
|
+
});
|
|
4226
|
+
}
|
|
4227
|
+
}
|
|
4228
|
+
}
|
|
4229
|
+
|
|
4230
|
+
return issues;
|
|
4231
|
+
}
|
|
4232
|
+
|
|
4233
|
+
findElementEndPosition(content, startTag, startPos) {
|
|
4234
|
+
// Extract tag name from start tag
|
|
4235
|
+
const tagMatch = startTag.match(/<(\w+)/);
|
|
4236
|
+
if (!tagMatch) return startPos + startTag.length;
|
|
4237
|
+
|
|
4238
|
+
const tagName = tagMatch[1].toLowerCase();
|
|
4239
|
+
|
|
4240
|
+
// Self-closing tags
|
|
4241
|
+
if (startTag.endsWith('/>') || ['input', 'img', 'br', 'hr', 'meta', 'link'].includes(tagName)) {
|
|
4242
|
+
return startPos + startTag.length;
|
|
4243
|
+
}
|
|
4244
|
+
|
|
4245
|
+
// Find matching closing tag
|
|
4246
|
+
const closeTagPattern = new RegExp(`</${tagName}>`, 'i');
|
|
4247
|
+
const remainingContent = content.substring(startPos + startTag.length);
|
|
4248
|
+
const closeMatch = remainingContent.match(closeTagPattern);
|
|
4249
|
+
|
|
4250
|
+
if (closeMatch) {
|
|
4251
|
+
return startPos + startTag.length + closeMatch.index + closeMatch[0].length;
|
|
4252
|
+
}
|
|
4253
|
+
|
|
4254
|
+
// If no closing tag found, assume it ends at the start tag
|
|
4255
|
+
return startPos + startTag.length;
|
|
4256
|
+
}
|
|
4257
|
+
|
|
4258
|
+
getInteractiveElementType(element) {
|
|
4259
|
+
// Extract tag name
|
|
4260
|
+
const tagMatch = element.match(/<(\w+)/);
|
|
4261
|
+
const tagName = tagMatch ? tagMatch[1].toLowerCase() : 'element';
|
|
4262
|
+
|
|
4263
|
+
// Extract role if present
|
|
4264
|
+
const roleMatch = element.match(/role\s*=\s*["']([^"']+)["']/i);
|
|
4265
|
+
const role = roleMatch ? roleMatch[1] : null;
|
|
4266
|
+
|
|
4267
|
+
if (role) {
|
|
4268
|
+
return `${tagName}[role="${role}"]`;
|
|
4269
|
+
}
|
|
4270
|
+
|
|
4271
|
+
// Special cases
|
|
4272
|
+
if (tagName === 'a' && /href\s*=/i.test(element)) {
|
|
4273
|
+
return 'link';
|
|
4274
|
+
}
|
|
4275
|
+
|
|
4276
|
+
if (tagName === 'input') {
|
|
4277
|
+
const typeMatch = element.match(/type\s*=\s*["']([^"']+)["']/i);
|
|
4278
|
+
const inputType = typeMatch ? typeMatch[1] : 'text';
|
|
4279
|
+
return `input[type="${inputType}"]`;
|
|
4280
|
+
}
|
|
4281
|
+
|
|
4282
|
+
return tagName;
|
|
4283
|
+
}
|
|
4284
|
+
|
|
4285
|
+
fixNestedInteractiveControlsInContent(content) {
|
|
4286
|
+
let fixed = content;
|
|
4287
|
+
|
|
4288
|
+
// Strategy 1: Remove role attributes from parent containers that have interactive children
|
|
4289
|
+
const issues = this.analyzeNestedInteractiveControls(content);
|
|
4290
|
+
|
|
4291
|
+
issues.forEach(issue => {
|
|
4292
|
+
// Try to fix by removing role from parent element
|
|
4293
|
+
const parentRoleMatch = issue.parentElement.match(/role\s*=\s*["'][^"']*["']/i);
|
|
4294
|
+
if (parentRoleMatch) {
|
|
4295
|
+
const parentWithoutRole = issue.parentElement.replace(/\s*role\s*=\s*["'][^"']*["']/i, '');
|
|
4296
|
+
fixed = fixed.replace(issue.parentElement, parentWithoutRole);
|
|
4297
|
+
console.log(chalk.yellow(` ๐ฏ Removed role attribute from parent element to fix nesting`));
|
|
4298
|
+
}
|
|
4299
|
+
});
|
|
4300
|
+
|
|
4301
|
+
// Strategy 2: Convert div[role="button"] containing links to regular div
|
|
4302
|
+
fixed = fixed.replace(/<div([^>]*role\s*=\s*["']button["'][^>]*)>([\s\S]*?)<\/div>/gi, (match, attributes, content) => {
|
|
4303
|
+
// Check if content contains interactive elements
|
|
4304
|
+
const hasInteractiveChildren = /<(?:a\s[^>]*href|button|input|select|textarea)[^>]*>/i.test(content);
|
|
4305
|
+
|
|
4306
|
+
if (hasInteractiveChildren) {
|
|
4307
|
+
// Remove role="button" and any button-related attributes
|
|
4308
|
+
const cleanAttributes = attributes
|
|
4309
|
+
.replace(/\s*role\s*=\s*["']button["']/i, '')
|
|
4310
|
+
.replace(/\s*tabindex\s*=\s*["'][^"']*["']/i, '')
|
|
4311
|
+
.replace(/\s*onclick\s*=\s*["'][^"']*["']/i, '');
|
|
4312
|
+
|
|
4313
|
+
console.log(chalk.yellow(` ๐ฏ Converted div[role="button"] to regular div due to interactive children`));
|
|
4314
|
+
return `<div${cleanAttributes}>${content}</div>`;
|
|
4315
|
+
}
|
|
4316
|
+
|
|
4317
|
+
return match;
|
|
4318
|
+
});
|
|
4319
|
+
|
|
4320
|
+
// Strategy 3: Remove tabindex from parent containers with interactive children
|
|
4321
|
+
fixed = fixed.replace(/(<[^>]+)(\s+tabindex\s*=\s*["'][^"']*["'])([^>]*>[\s\S]*?<(?:a\s[^>]*href|button|input|select|textarea)[^>]*>[\s\S]*?<\/[^>]+>)/gi, (match, beforeTabindex, tabindexAttr, afterTabindex) => {
|
|
4322
|
+
console.log(chalk.yellow(` ๐ฏ Removed tabindex from parent element with interactive children`));
|
|
4323
|
+
return beforeTabindex + afterTabindex;
|
|
4324
|
+
});
|
|
4325
|
+
|
|
4326
|
+
return fixed;
|
|
4327
|
+
}
|
|
4328
|
+
|
|
4329
|
+
async fixAllAccessibilityIssues(directory = '.') {
|
|
4330
|
+
console.log(chalk.blue('๐ Starting comprehensive accessibility fixes...'));
|
|
4331
|
+
console.log('');
|
|
4332
|
+
|
|
4333
|
+
const results = {
|
|
4334
|
+
totalFiles: 0,
|
|
4335
|
+
fixedFiles: 0,
|
|
4336
|
+
totalIssues: 0,
|
|
4337
|
+
steps: []
|
|
4338
|
+
};
|
|
4339
|
+
|
|
4340
|
+
try {
|
|
4341
|
+
// Step 1: HTML lang attributes
|
|
4342
|
+
console.log(chalk.blue('๐ Step 1: HTML lang attributes...'));
|
|
4343
|
+
const langResults = await this.fixHtmlLang(directory);
|
|
4344
|
+
const langFixed = langResults.filter(r => r.status === 'fixed').length;
|
|
4345
|
+
results.steps.push({ step: 1, name: 'HTML lang attributes', fixed: langFixed });
|
|
4346
|
+
|
|
4347
|
+
// Step 2: Alt attributes
|
|
4348
|
+
console.log(chalk.blue('๐ผ๏ธ Step 2: Alt attributes...'));
|
|
4349
|
+
const altResults = await this.fixEmptyAltAttributes(directory);
|
|
4350
|
+
const altFixed = altResults.filter(r => r.status === 'fixed').length;
|
|
4351
|
+
const totalAltIssues = altResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4352
|
+
results.steps.push({ step: 2, name: 'Alt attributes', fixed: altFixed, issues: totalAltIssues });
|
|
4353
|
+
|
|
4354
|
+
// Step 3: Role attributes
|
|
4355
|
+
console.log(chalk.blue('๐ญ Step 3: Role attributes...'));
|
|
4356
|
+
const roleResults = await this.fixRoleAttributes(directory);
|
|
4357
|
+
const roleFixed = roleResults.filter(r => r.status === 'fixed').length;
|
|
4358
|
+
const totalRoleIssues = roleResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4359
|
+
results.steps.push({ step: 3, name: 'Role attributes', fixed: roleFixed, issues: totalRoleIssues });
|
|
4360
|
+
|
|
4361
|
+
// Step 4: Form labels
|
|
4362
|
+
console.log(chalk.blue('๐ Step 4: Form labels...'));
|
|
4363
|
+
const formResults = await this.fixFormLabels(directory);
|
|
4364
|
+
const formFixed = formResults.filter(r => r.status === 'fixed').length;
|
|
4365
|
+
const totalFormIssues = formResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4366
|
+
results.steps.push({ step: 4, name: 'Form labels', fixed: formFixed, issues: totalFormIssues });
|
|
4367
|
+
|
|
4368
|
+
// Step 5: Nested interactive controls (NEW!)
|
|
4369
|
+
console.log(chalk.blue('๐ฏ Step 5: Nested interactive controls...'));
|
|
4370
|
+
const nestedResults = await this.fixNestedInteractiveControls(directory);
|
|
4371
|
+
const nestedFixed = nestedResults.filter(r => r.status === 'fixed').length;
|
|
4372
|
+
const totalNestedIssues = nestedResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4373
|
+
results.steps.push({ step: 5, name: 'Nested interactive controls', fixed: nestedFixed, issues: totalNestedIssues });
|
|
4374
|
+
|
|
4375
|
+
// Step 6: Button names
|
|
4376
|
+
console.log(chalk.blue('๐ Step 6: Button names...'));
|
|
4377
|
+
const buttonResults = await this.fixButtonNames(directory);
|
|
4378
|
+
const buttonFixed = buttonResults.filter(r => r.status === 'fixed').length;
|
|
4379
|
+
const totalButtonIssues = buttonResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4380
|
+
results.steps.push({ step: 6, name: 'Button names', fixed: buttonFixed, issues: totalButtonIssues });
|
|
4381
|
+
|
|
4382
|
+
// Step 7: Link names
|
|
4383
|
+
console.log(chalk.blue('๐ Step 7: Link names...'));
|
|
4384
|
+
const linkResults = await this.fixLinkNames(directory);
|
|
4385
|
+
const linkFixed = linkResults.filter(r => r.status === 'fixed').length;
|
|
4386
|
+
const totalLinkIssues = linkResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4387
|
+
results.steps.push({ step: 7, name: 'Link names', fixed: linkFixed, issues: totalLinkIssues });
|
|
4388
|
+
|
|
4389
|
+
// Step 8: Landmarks
|
|
4390
|
+
console.log(chalk.blue('๐๏ธ Step 8: Landmarks...'));
|
|
4391
|
+
const landmarkResults = await this.fixLandmarks(directory);
|
|
4392
|
+
const landmarkFixed = landmarkResults.filter(r => r.status === 'fixed').length;
|
|
4393
|
+
const totalLandmarkIssues = landmarkResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4394
|
+
results.steps.push({ step: 8, name: 'Landmarks', fixed: landmarkFixed, issues: totalLandmarkIssues });
|
|
4395
|
+
|
|
4396
|
+
// Step 9: Heading analysis
|
|
4397
|
+
console.log(chalk.blue('๐ Step 9: Heading analysis...'));
|
|
4398
|
+
const headingResults = await this.analyzeHeadings(directory);
|
|
4399
|
+
const totalHeadingSuggestions = headingResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4400
|
+
results.steps.push({ step: 9, name: 'Heading analysis', suggestions: totalHeadingSuggestions });
|
|
4401
|
+
console.log(chalk.gray('๐ก Heading issues require manual review and cannot be auto-fixed'));
|
|
4402
|
+
|
|
4403
|
+
// Step 10: Broken links check
|
|
4404
|
+
console.log(chalk.blue('๐ Step 10: Broken links check...'));
|
|
4405
|
+
const brokenLinksResults = await this.checkBrokenLinks(directory);
|
|
4406
|
+
const totalBrokenLinks = brokenLinksResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4407
|
+
results.steps.push({ step: 10, name: 'Broken links check', issues: totalBrokenLinks });
|
|
4408
|
+
console.log(chalk.gray('๐ก Broken link issues require manual review and cannot be auto-fixed'));
|
|
4409
|
+
|
|
4410
|
+
// Step 11: Cleanup duplicate roles
|
|
4411
|
+
console.log(chalk.blue('๐งน Step 11: Cleanup duplicate roles...'));
|
|
4412
|
+
const cleanupResults = await this.cleanupDuplicateRoles(directory);
|
|
4413
|
+
const cleanupFixed = cleanupResults.filter(r => r.status === 'fixed').length;
|
|
4414
|
+
results.steps.push({ step: 11, name: 'Cleanup duplicate roles', fixed: cleanupFixed });
|
|
4415
|
+
|
|
4416
|
+
// Calculate totals
|
|
4417
|
+
results.totalFiles = Math.max(
|
|
4418
|
+
langResults.length, altResults.length, roleResults.length, formResults.length,
|
|
4419
|
+
nestedResults.length, buttonResults.length, linkResults.length, landmarkResults.length,
|
|
4420
|
+
headingResults.length, brokenLinksResults.length, cleanupResults.length
|
|
4421
|
+
);
|
|
4422
|
+
|
|
4423
|
+
results.fixedFiles = new Set([
|
|
4424
|
+
...langResults.filter(r => r.status === 'fixed').map(r => r.file),
|
|
4425
|
+
...altResults.filter(r => r.status === 'fixed').map(r => r.file),
|
|
4426
|
+
...roleResults.filter(r => r.status === 'fixed').map(r => r.file),
|
|
4427
|
+
...formResults.filter(r => r.status === 'fixed').map(r => r.file),
|
|
4428
|
+
...nestedResults.filter(r => r.status === 'fixed').map(r => r.file),
|
|
4429
|
+
...buttonResults.filter(r => r.status === 'fixed').map(r => r.file),
|
|
4430
|
+
...linkResults.filter(r => r.status === 'fixed').map(r => r.file),
|
|
4431
|
+
...landmarkResults.filter(r => r.status === 'fixed').map(r => r.file),
|
|
4432
|
+
...cleanupResults.filter(r => r.status === 'fixed').map(r => r.file)
|
|
4433
|
+
]).size;
|
|
4434
|
+
|
|
4435
|
+
results.totalIssues = totalAltIssues + totalRoleIssues + totalFormIssues + totalNestedIssues +
|
|
4436
|
+
totalButtonIssues + totalLinkIssues + totalLandmarkIssues;
|
|
4437
|
+
|
|
4438
|
+
// Final summary
|
|
4439
|
+
console.log(chalk.green('\n๐ All accessibility fixes completed!'));
|
|
4440
|
+
console.log(chalk.blue('๐ Final Summary:'));
|
|
4441
|
+
console.log(chalk.blue(` Total files scanned: ${results.totalFiles}`));
|
|
4442
|
+
console.log(chalk.blue(` Files fixed: ${results.fixedFiles}`));
|
|
4443
|
+
console.log(chalk.blue(` Total issues resolved: ${results.totalIssues}`));
|
|
4444
|
+
|
|
4445
|
+
if (this.config.dryRun) {
|
|
4446
|
+
console.log(chalk.yellow('\n๐ก This was a dry run. Use without --dry-run to apply changes.'));
|
|
4447
|
+
}
|
|
4448
|
+
|
|
4449
|
+
return results;
|
|
4450
|
+
|
|
4451
|
+
} catch (error) {
|
|
4452
|
+
console.error(chalk.red(`โ Error during comprehensive fixes: ${error.message}`));
|
|
4453
|
+
throw error;
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
4456
|
+
|
|
4096
4457
|
async findHtmlFiles(directory) {
|
|
4097
4458
|
const files = [];
|
|
4098
4459
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gbu-accessibility-package",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"description": "Comprehensive accessibility fixes for HTML files. Smart context-aware alt text generation, form labels, button names, link names, landmarks, heading analysis, and WCAG-compliant role attributes. Covers major axe DevTools issues with individual fix modes.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|