@govtechsg/oobee 0.10.36 → 0.10.39
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/DETAILS.md +3 -3
- package/INTEGRATION.md +142 -53
- package/README.md +15 -0
- package/exclusions.txt +4 -1
- package/package.json +2 -2
- package/src/constants/cliFunctions.ts +0 -7
- package/src/constants/common.ts +39 -1
- package/src/constants/constants.ts +9 -8
- package/src/crawlers/commonCrawlerFunc.ts +66 -219
- package/src/crawlers/crawlDomain.ts +6 -2
- package/src/crawlers/crawlLocalFile.ts +2 -0
- package/src/crawlers/crawlSitemap.ts +5 -3
- package/src/crawlers/custom/escapeCssSelector.ts +10 -0
- package/src/crawlers/custom/evaluateAltText.ts +13 -0
- package/src/crawlers/custom/extractAndGradeText.ts +0 -2
- package/src/crawlers/custom/extractText.ts +28 -0
- package/src/crawlers/custom/findElementByCssSelector.ts +46 -0
- package/src/crawlers/custom/flagUnlabelledClickableElements.ts +1006 -901
- package/src/crawlers/custom/framesCheck.ts +51 -0
- package/src/crawlers/custom/getAxeConfiguration.ts +126 -0
- package/src/crawlers/custom/gradeReadability.ts +30 -0
- package/src/crawlers/custom/xPathToCss.ts +178 -0
- package/src/mergeAxeResults.ts +467 -129
- package/src/npmIndex.ts +130 -62
- package/src/static/ejs/partials/components/ruleOffcanvas.ejs +1 -1
- package/src/static/ejs/partials/components/scanAbout.ejs +1 -1
- package/src/static/ejs/partials/footer.ejs +3 -3
- package/src/static/ejs/partials/scripts/reportSearch.ejs +112 -74
- package/src/static/ejs/partials/scripts/ruleOffcanvas.ejs +2 -2
- package/src/static/ejs/partials/summaryMain.ejs +3 -3
- package/src/static/ejs/report.ejs +3 -3
- package/src/xPathToCssCypress.ts +178 -0
- package/src/crawlers/customAxeFunctions.ts +0 -82
@@ -1,1036 +1,1141 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
export const flagUnlabelledClickableElements = async (page: Page) => {
|
1
|
+
export async function flagUnlabelledClickableElements() {
|
4
2
|
// Just paste the entire script into the body of the page.evaluate callback below
|
5
3
|
// There's some code that is not needed when running this on backend but
|
6
4
|
// we avoid changing the script for now to make it easy to update
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
5
|
+
const allowNonClickableFlagging = true; // Change this to true to flag non-clickable images
|
6
|
+
const landmarkElements = [
|
7
|
+
'header',
|
8
|
+
'footer',
|
9
|
+
'nav',
|
10
|
+
'main',
|
11
|
+
'article',
|
12
|
+
'section',
|
13
|
+
'aside',
|
14
|
+
'form',
|
15
|
+
];
|
16
|
+
const loggingEnabled = false; // Set to true to enable console warnings
|
17
|
+
|
18
|
+
let previousFlaggedXPathsByDocument = {}; // Object to hold previous flagged XPaths
|
19
|
+
const previousAllFlaggedElementsXPaths = []; // Array to store all flagged XPaths
|
20
|
+
|
21
|
+
function getXPath(element: Node) {
|
22
|
+
if (!element) return null;
|
23
|
+
if (element instanceof HTMLElement && element.id) {
|
24
|
+
return `//*[@id="${element.id}"]`;
|
25
|
+
}
|
26
|
+
if (element === element.ownerDocument.body) {
|
27
|
+
return '/html/body';
|
28
|
+
}
|
29
|
+
if (!element.parentNode || element.parentNode.nodeType !== 1) {
|
30
|
+
return '';
|
31
|
+
}
|
32
|
+
|
33
|
+
const siblings: Node[] = Array.from(element.parentNode.childNodes).filter(
|
34
|
+
node => node.nodeName === element.nodeName,
|
35
|
+
);
|
36
|
+
const ix = siblings.indexOf(element) + 1;
|
37
|
+
const siblingIndex = siblings.length > 1 ? `[${ix}]` : '';
|
38
|
+
return `${getXPath(element.parentNode)}/${element.nodeName.toLowerCase()}${siblingIndex}`;
|
39
|
+
}
|
40
|
+
|
41
|
+
function customConsoleWarn(message: string, data?: any) {
|
42
|
+
if (loggingEnabled) {
|
43
|
+
if (data) {
|
44
|
+
console.warn(message, data);
|
45
|
+
} else {
|
46
|
+
console.warn(message);
|
34
47
|
}
|
48
|
+
}
|
49
|
+
}
|
35
50
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
51
|
+
function hasPointerCursor(element: Element) {
|
52
|
+
const computedStyle = element.ownerDocument.defaultView.getComputedStyle(element);
|
53
|
+
const hasPointerStyle = computedStyle.cursor === 'pointer';
|
54
|
+
const hasOnClick = element.hasAttribute('onclick');
|
55
|
+
const hasEventListeners = Object.keys(element).some(prop => prop.startsWith('on'));
|
56
|
+
|
57
|
+
// Check if the element is inherently interactive
|
58
|
+
const isClickableRole = ['button', 'link', 'menuitem'].includes(element.getAttribute('role'));
|
59
|
+
const isNativeClickableElement =
|
60
|
+
['a', 'button', 'input'].includes(element.nodeName.toLowerCase()) &&
|
61
|
+
(element.nodeName.toLowerCase() !== 'a' || element.hasAttribute('href'));
|
62
|
+
const hasTabIndex =
|
63
|
+
element.hasAttribute('tabindex') && element.getAttribute('tabindex') !== '-1';
|
64
|
+
|
65
|
+
return (
|
66
|
+
hasPointerStyle ||
|
67
|
+
hasOnClick ||
|
68
|
+
hasEventListeners ||
|
69
|
+
isClickableRole ||
|
70
|
+
isNativeClickableElement ||
|
71
|
+
hasTabIndex
|
72
|
+
);
|
73
|
+
}
|
74
|
+
|
75
|
+
function isAccessibleText(value: string) {
|
76
|
+
if (!value || value.trim().length === 0) {
|
77
|
+
return false;
|
42
78
|
}
|
43
79
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
}
|
51
|
-
}
|
80
|
+
const trimmedValue = value.trim();
|
81
|
+
|
82
|
+
// Check if the text contains any private use characters
|
83
|
+
const privateUseRegex = /\p{Private_Use}/u;
|
84
|
+
if (privateUseRegex.test(trimmedValue)) {
|
85
|
+
return false;
|
52
86
|
}
|
53
87
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
88
|
+
// Check if the text contains at least one letter or number
|
89
|
+
const accessibleTextRegex = /[\p{L}\p{N}]/u;
|
90
|
+
if (accessibleTextRegex.test(trimmedValue)) {
|
91
|
+
return true;
|
92
|
+
}
|
59
93
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
['a', 'button', 'input'].includes(element.nodeName.toLowerCase()) &&
|
64
|
-
(element.nodeName.toLowerCase() !== 'a' || element.hasAttribute('href'));
|
65
|
-
const hasTabIndex =
|
66
|
-
element.hasAttribute('tabindex') && element.getAttribute('tabindex') !== '-1';
|
94
|
+
// If it doesn't contain letters or numbers, consider it not accessible
|
95
|
+
return false;
|
96
|
+
}
|
67
97
|
|
98
|
+
function isInOpenDetails(element: Element) {
|
99
|
+
let parentDetails = element.closest('details');
|
100
|
+
return parentDetails ? parentDetails.open : true;
|
101
|
+
}
|
102
|
+
|
103
|
+
function isVisibleFocusAble(el: Element) {
|
104
|
+
if (!el) {
|
105
|
+
return false;
|
106
|
+
}
|
107
|
+
if (el.nodeName !== undefined && el.nodeName === '#text') {
|
108
|
+
// cause #text cannot getComputedStyle
|
109
|
+
return false;
|
110
|
+
}
|
111
|
+
try {
|
112
|
+
const style = window.getComputedStyle(el);
|
113
|
+
const rect = el.getBoundingClientRect();
|
68
114
|
return (
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
115
|
+
// Visible
|
116
|
+
style.display !== 'none' &&
|
117
|
+
style.visibility !== 'hidden' &&
|
118
|
+
style.opacity !== '0' &&
|
119
|
+
rect.width > 0 &&
|
120
|
+
rect.height > 0 &&
|
121
|
+
// <detail> tag will show it as visual so need to account for that
|
122
|
+
isInOpenDetails(el)
|
75
123
|
);
|
124
|
+
} catch (error) {
|
125
|
+
console.log('Error in ELEMENT', el, error.message);
|
126
|
+
return false;
|
127
|
+
}
|
128
|
+
}
|
129
|
+
|
130
|
+
function isValidUnicode(text: string) {
|
131
|
+
if (typeof text !== 'string') {
|
132
|
+
return false;
|
76
133
|
}
|
77
134
|
|
78
|
-
|
79
|
-
|
80
|
-
|
135
|
+
// Regular expression to match valid Unicode characters, including surrogate pairs
|
136
|
+
const validTextOrEmojiRegex = /[\p{L}\p{N}\p{S}\p{P}\p{Emoji}]/gu; // Letters, numbers, symbols, punctuation, and emojis
|
137
|
+
|
138
|
+
// Check if the text contains at least one valid character or emoji
|
139
|
+
return validTextOrEmojiRegex.test(text);
|
140
|
+
}
|
141
|
+
|
142
|
+
function getElementById(element: Element, id: string) {
|
143
|
+
return element.ownerDocument.getElementById(id);
|
144
|
+
}
|
145
|
+
|
146
|
+
function getAriaLabelledByText(element: Element) {
|
147
|
+
const labelledById = element.getAttribute('aria-labelledby');
|
148
|
+
if (labelledById) {
|
149
|
+
const labelledByElement = getElementById(element, labelledById);
|
150
|
+
if (labelledByElement) {
|
151
|
+
const ariaLabel = labelledByElement.getAttribute('aria-label');
|
152
|
+
return ariaLabel ? ariaLabel.trim() : labelledByElement.textContent.trim();
|
81
153
|
}
|
154
|
+
}
|
155
|
+
return '';
|
156
|
+
}
|
157
|
+
|
158
|
+
function hasAccessibleLabel(element: Element) {
|
159
|
+
const ariaLabel = element.getAttribute('aria-label');
|
160
|
+
const ariaLabelledByText = getAriaLabelledByText(element);
|
161
|
+
const altText = element.getAttribute('alt');
|
162
|
+
const title = element.getAttribute('title');
|
163
|
+
|
164
|
+
return (
|
165
|
+
isAccessibleText(ariaLabel) ||
|
166
|
+
isAccessibleText(ariaLabelledByText) ||
|
167
|
+
isAccessibleText(altText) ||
|
168
|
+
isAccessibleText(title)
|
169
|
+
);
|
170
|
+
}
|
82
171
|
|
83
|
-
|
172
|
+
function hasSummaryOrDetailsLabel(element: Element) {
|
173
|
+
const summary = element.closest('summary, details');
|
174
|
+
return summary && hasAccessibleLabel(summary);
|
175
|
+
}
|
84
176
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
177
|
+
function hasSiblingWithAccessibleLabel(element: Element) {
|
178
|
+
// Check all siblings (previous and next)
|
179
|
+
let sibling = element.previousElementSibling;
|
180
|
+
while (sibling) {
|
181
|
+
if (hasAccessibleLabel(sibling)) {
|
182
|
+
return true;
|
89
183
|
}
|
184
|
+
sibling = sibling.previousElementSibling;
|
185
|
+
}
|
90
186
|
|
91
|
-
|
92
|
-
|
93
|
-
if (
|
187
|
+
sibling = element.nextElementSibling;
|
188
|
+
while (sibling) {
|
189
|
+
if (hasAccessibleLabel(sibling)) {
|
94
190
|
return true;
|
95
191
|
}
|
192
|
+
sibling = sibling.nextElementSibling;
|
193
|
+
}
|
194
|
+
|
195
|
+
return false;
|
196
|
+
}
|
197
|
+
|
198
|
+
function hasSiblingOrParentAccessibleLabel(element: Element) {
|
199
|
+
// Check previous and next siblings
|
200
|
+
const previousSibling = element.previousElementSibling;
|
201
|
+
const nextSibling = element.nextElementSibling;
|
202
|
+
if (
|
203
|
+
(previousSibling && hasAccessibleLabel(previousSibling)) ||
|
204
|
+
(nextSibling && hasAccessibleLabel(nextSibling))
|
205
|
+
) {
|
206
|
+
return true;
|
207
|
+
}
|
208
|
+
|
209
|
+
// Check the parent element
|
210
|
+
const parent = element.parentElement;
|
211
|
+
if (parent && hasAccessibleLabel(parent)) {
|
212
|
+
return true;
|
213
|
+
}
|
96
214
|
|
97
|
-
|
215
|
+
return false;
|
216
|
+
}
|
217
|
+
|
218
|
+
function hasChildWithAccessibleText(element: Element) {
|
219
|
+
// Check element children
|
220
|
+
const hasAccessibleChildElement = Array.from(element.children).some(child => {
|
221
|
+
if (child.nodeName.toLowerCase() === 'style' || child.nodeName.toLowerCase() === 'script') {
|
222
|
+
return false;
|
223
|
+
}
|
224
|
+
// Skip children that are aria-hidden
|
225
|
+
if (child.getAttribute('aria-hidden') === 'true') {
|
226
|
+
return false;
|
227
|
+
}
|
228
|
+
return (
|
229
|
+
isAccessibleText(child.textContent) || hasAccessibleLabel(child) || hasCSSContent(child)
|
230
|
+
);
|
231
|
+
});
|
232
|
+
|
233
|
+
// Check direct text nodes
|
234
|
+
const hasDirectAccessibleText = Array.from(element.childNodes).some(node => {
|
235
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
236
|
+
return isAccessibleText(node.textContent);
|
237
|
+
}
|
98
238
|
return false;
|
239
|
+
});
|
240
|
+
|
241
|
+
return hasAccessibleChildElement || hasDirectAccessibleText;
|
242
|
+
}
|
243
|
+
|
244
|
+
function hasAllChildrenAccessible(element: Element) {
|
245
|
+
// If the element is aria-hidden, consider it accessible
|
246
|
+
if (element.getAttribute('aria-hidden') === 'true') {
|
247
|
+
return true;
|
99
248
|
}
|
100
249
|
|
101
|
-
|
102
|
-
|
103
|
-
|
250
|
+
// Check if the element itself has an accessible label, text content, or CSS content
|
251
|
+
if (
|
252
|
+
hasAccessibleLabel(element) ||
|
253
|
+
isAccessibleText(element.textContent) ||
|
254
|
+
hasCSSContent(element)
|
255
|
+
) {
|
256
|
+
return true;
|
104
257
|
}
|
105
258
|
|
106
|
-
|
107
|
-
|
108
|
-
{
|
109
|
-
|
259
|
+
// If the element has children, ensure at least one of them is accessible
|
260
|
+
if (element.children.length > 0) {
|
261
|
+
return Array.from(element.children).some(child => {
|
262
|
+
// If child is aria-hidden, skip it in the accessibility check
|
263
|
+
if (child.getAttribute('aria-hidden') === 'true') {
|
264
|
+
return true;
|
265
|
+
}
|
266
|
+
// Recursively check if the child or any of its descendants are accessible
|
267
|
+
return hasAllChildrenAccessible(child);
|
268
|
+
});
|
269
|
+
}
|
270
|
+
|
271
|
+
// If the element and all its children have no accessible labels or text, it's not accessible
|
272
|
+
return false;
|
273
|
+
}
|
274
|
+
|
275
|
+
function hasChildNotANewInteractWithAccessibleText(element: Element) {
|
276
|
+
// Helper function to check if the element is a link or button
|
277
|
+
const isLinkOrButton = (child: Node) => {
|
278
|
+
if (child instanceof Element) {
|
279
|
+
// Check if the child is an Element
|
280
|
+
return (
|
281
|
+
child.nodeName.toLowerCase() === 'a' ||
|
282
|
+
child.nodeName.toLowerCase() === 'button' ||
|
283
|
+
child.getAttribute('role') === 'link' ||
|
284
|
+
child.getAttribute('role') === 'button'
|
285
|
+
);
|
110
286
|
}
|
111
|
-
|
112
|
-
|
287
|
+
return false;
|
288
|
+
};
|
289
|
+
|
290
|
+
// Check element children
|
291
|
+
const hasAccessibleChildElement = Array.from(element.children).some(child => {
|
292
|
+
if (child instanceof Element) {
|
293
|
+
// Ensure child is an Element
|
294
|
+
if (!hasPointerCursor(child)) {
|
113
295
|
return false;
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
const rect = el.getBoundingClientRect();
|
118
|
-
return (
|
119
|
-
// Visible
|
120
|
-
style.display !== 'none'
|
121
|
-
&& style.visibility !== 'hidden'
|
122
|
-
&& style.opacity !== "0"
|
123
|
-
&& rect.width > 0 && rect.height > 0
|
124
|
-
// <detail> tag will show it as visual so need to account for that
|
125
|
-
&& isInOpenDetails(el)
|
126
|
-
);
|
127
|
-
} catch (error) {
|
128
|
-
console.log("Error in ELEMENT",el,error.message)
|
296
|
+
}
|
297
|
+
|
298
|
+
if (child.nodeName.toLowerCase() === 'style' || child.nodeName.toLowerCase() === 'script') {
|
129
299
|
return false;
|
300
|
+
}
|
301
|
+
|
302
|
+
// Skip children that are aria-hidden or links/buttons
|
303
|
+
if (
|
304
|
+
child.getAttribute('aria-hidden') === 'true' ||
|
305
|
+
isLinkOrButton(child) ||
|
306
|
+
!isVisibleFocusAble(child)
|
307
|
+
) {
|
308
|
+
return false;
|
309
|
+
}
|
310
|
+
|
311
|
+
// Check if the child element has accessible text or label
|
312
|
+
return (
|
313
|
+
isAccessibleText(getTextContent(child)) ||
|
314
|
+
hasAccessibleLabel(child) ||
|
315
|
+
hasCSSContent(child)
|
316
|
+
);
|
130
317
|
}
|
131
|
-
|
318
|
+
return false;
|
319
|
+
});
|
320
|
+
|
321
|
+
// Check direct text nodes inside the element itself (like <a>"text"</a>)
|
322
|
+
const hasDirectAccessibleText = Array.from(element.childNodes).some(node => {
|
323
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
324
|
+
const textContent = getTextContent(node);
|
132
325
|
|
133
|
-
|
134
|
-
|
326
|
+
// Check if the text contains non-ASCII characters (Unicode)
|
327
|
+
const containsUnicode = /[^\x00-\x7F]/.test(textContent);
|
328
|
+
|
329
|
+
// If contains non-ASCII characters, validate with isValidUnicode
|
330
|
+
if (containsUnicode) {
|
331
|
+
return isValidUnicode(textContent);
|
332
|
+
}
|
333
|
+
|
334
|
+
// Otherwise, just check if it's non-empty text
|
335
|
+
return textContent.length > 0;
|
336
|
+
}
|
337
|
+
|
338
|
+
// Recursively check for text content inside child nodes of elements that are not links or buttons
|
339
|
+
if (node.nodeType === Node.ELEMENT_NODE && node instanceof Element && !isLinkOrButton(node)) {
|
340
|
+
return Array.from(node.childNodes).some(innerNode => {
|
341
|
+
if (innerNode.nodeType === Node.TEXT_NODE) {
|
342
|
+
const innerTextContent = getTextContent(innerNode).trim();
|
343
|
+
return innerTextContent && !isValidUnicode(innerTextContent); // Check for non-Unicode content
|
344
|
+
}
|
135
345
|
return false;
|
346
|
+
});
|
136
347
|
}
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
return validTextOrEmojiRegex.test(text);
|
348
|
+
|
349
|
+
return false;
|
350
|
+
});
|
351
|
+
|
352
|
+
return hasAccessibleChildElement || hasDirectAccessibleText;
|
143
353
|
}
|
144
354
|
|
145
|
-
|
146
|
-
|
355
|
+
const style = document.createElement('style');
|
356
|
+
style.innerHTML = `
|
357
|
+
.highlight-flagged {
|
358
|
+
outline: 4px solid rgba(128, 0, 128, 1) !important; /* Thicker primary outline with purple in rgba format */
|
359
|
+
box-shadow:
|
360
|
+
0 0 25px 15px rgba(255, 255, 255, 1), /* White glow for contrast */
|
361
|
+
0 0 15px 10px rgba(144, 33, 166, 1) !important; /* Consistent purple glow in rgba format */
|
147
362
|
}
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
363
|
+
`;
|
364
|
+
document.head.appendChild(style);
|
365
|
+
|
366
|
+
function injectStylesIntoFrame(frame: HTMLIFrameElement) {
|
367
|
+
try {
|
368
|
+
const frameDocument = frame.contentDocument || frame.contentWindow.document;
|
369
|
+
if (frameDocument) {
|
370
|
+
const frameStyle = frameDocument.createElement('style');
|
371
|
+
frameStyle.innerHTML = `
|
372
|
+
.highlight-flagged {
|
373
|
+
outline: 4px solid rgba(128, 0, 128, 1) !important; /* Thicker primary outline with purple in rgba format */
|
374
|
+
box-shadow:
|
375
|
+
0 0 25px 15px rgba(255, 255, 255, 1), /* White glow for contrast */
|
376
|
+
0 0 15px 10px rgba(144, 33, 166, 1) !important; /* Consistent purple glow in rgba format */
|
377
|
+
}
|
378
|
+
`;
|
379
|
+
frameDocument.head.appendChild(frameStyle);
|
157
380
|
}
|
158
|
-
|
381
|
+
} catch (error) {
|
382
|
+
customConsoleWarn(`Cannot access frame document: ${error}`);
|
159
383
|
}
|
384
|
+
}
|
160
385
|
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
const altText = element.getAttribute('alt');
|
165
|
-
const title = element.getAttribute('title');
|
386
|
+
function hasCSSContent(element: Element) {
|
387
|
+
const beforeContent = window.getComputedStyle(element, '::before').getPropertyValue('content');
|
388
|
+
const afterContent = window.getComputedStyle(element, '::after').getPropertyValue('content');
|
166
389
|
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
);
|
390
|
+
function isAccessibleContent(value: string) {
|
391
|
+
if (!value || value === 'none' || value === 'normal') {
|
392
|
+
return false;
|
393
|
+
}
|
394
|
+
// Remove quotes from the content value
|
395
|
+
const unquotedValue = value.replace(/^['"]|['"]$/g, '').trim();
|
396
|
+
|
397
|
+
// Use the isAccessibleText function
|
398
|
+
return isAccessibleText(unquotedValue);
|
173
399
|
}
|
174
400
|
|
175
|
-
|
176
|
-
|
177
|
-
|
401
|
+
return isAccessibleContent(beforeContent) || isAccessibleContent(afterContent);
|
402
|
+
}
|
403
|
+
|
404
|
+
function isElementTooSmall(element: Element) {
|
405
|
+
// Get the bounding rectangle of the element
|
406
|
+
const rect = element.getBoundingClientRect();
|
407
|
+
|
408
|
+
// Check if width or height is less than 1
|
409
|
+
return rect.width < 1 || rect.height < 1;
|
410
|
+
}
|
411
|
+
|
412
|
+
function getTextContent(element: Element | ChildNode): string {
|
413
|
+
if (element.nodeType === Node.TEXT_NODE) {
|
414
|
+
return element.nodeValue?.trim() ?? ''; // Return the text directly if it's a TEXT_NODE
|
178
415
|
}
|
179
416
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
417
|
+
let textContent = '';
|
418
|
+
|
419
|
+
for (const node of element.childNodes) {
|
420
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
421
|
+
textContent += node.nodeValue?.trim() ?? ''; // Append text content from text nodes
|
422
|
+
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
423
|
+
// Type assertion: node is an Element
|
424
|
+
const elementNode = node as Element;
|
425
|
+
|
426
|
+
// If it's an SVG and has a <title> tag inside it, we want to grab that text
|
427
|
+
if (elementNode.tagName.toLowerCase() === 'svg') {
|
428
|
+
const titleElement = elementNode.querySelector('title');
|
429
|
+
if (titleElement && isVisibleFocusAble(elementNode)) {
|
430
|
+
return titleElement.textContent?.trim() ?? ''; // Return the title text if valid
|
431
|
+
}
|
186
432
|
}
|
187
|
-
sibling = sibling.previousElementSibling;
|
188
|
-
}
|
189
433
|
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
434
|
+
// Recursively check child elements if it's an element node
|
435
|
+
if (isVisibleFocusAble(elementNode)) {
|
436
|
+
const childText = getTextContent(elementNode);
|
437
|
+
if (childText) {
|
438
|
+
textContent += childText; // Append valid child text
|
439
|
+
}
|
194
440
|
}
|
195
|
-
sibling = sibling.nextElementSibling;
|
196
441
|
}
|
442
|
+
}
|
443
|
+
|
444
|
+
return textContent.trim(); // Return the combined text content
|
445
|
+
}
|
446
|
+
|
447
|
+
function shouldFlagElement(element: HTMLElement, allowNonClickableFlagging: boolean) {
|
448
|
+
if (isElementTooSmall(element)) {
|
449
|
+
return false;
|
450
|
+
}
|
451
|
+
|
452
|
+
// Skip non-clickable elements if allowNonClickableFlagging is false
|
453
|
+
if (allowNonClickableFlagging && !hasPointerCursor(element)) {
|
454
|
+
customConsoleWarn(
|
455
|
+
'Element is not clickable and allowNonClickableFlagging is false, skipping flagging.',
|
456
|
+
);
|
457
|
+
return false;
|
458
|
+
}
|
197
459
|
|
460
|
+
// Do not flag elements if any ancestor has aria-hidden="true"
|
461
|
+
if (element.closest('[aria-hidden="true"]')) {
|
462
|
+
customConsoleWarn("An ancestor element has aria-hidden='true', skipping flagging.");
|
198
463
|
return false;
|
199
464
|
}
|
200
465
|
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
466
|
+
let parents = element.parentElement;
|
467
|
+
|
468
|
+
// Causing false negative of svg
|
469
|
+
if (parents) {
|
470
|
+
// Check if the parent has an accessible label
|
471
|
+
if (hasAccessibleLabel(parents) || hasChildNotANewInteractWithAccessibleText(parents)) {
|
472
|
+
customConsoleWarn(
|
473
|
+
'Parent element has an accessible label, skipping flagging of this element.',
|
474
|
+
);
|
475
|
+
return false;
|
476
|
+
}
|
477
|
+
}
|
478
|
+
|
479
|
+
let maxLayers = 3;
|
480
|
+
let tracedBackedLayerCount = 0;
|
481
|
+
while (parents && tracedBackedLayerCount <= maxLayers) {
|
482
|
+
// Skip flagging if the parent or the element itself has an accessible label
|
483
|
+
if (hasAccessibleLabel(parents) || hasChildNotANewInteractWithAccessibleText(parents)) {
|
484
|
+
customConsoleWarn('Parent or element has an accessible label, skipping flagging.', parents);
|
485
|
+
return false;
|
486
|
+
}
|
487
|
+
|
488
|
+
// Skip flagging if the parent is a button-like element with aria-expanded
|
205
489
|
if (
|
206
|
-
(
|
207
|
-
(
|
490
|
+
parents.getAttribute('role') === 'button' &&
|
491
|
+
(parents.hasAttribute('aria-expanded') || parents.hasAttribute('aria-controls'))
|
208
492
|
) {
|
209
|
-
|
493
|
+
customConsoleWarn(
|
494
|
+
'Parent element is an interactive button with aria-expanded or aria-controls, skipping flagging.',
|
495
|
+
);
|
496
|
+
return false;
|
210
497
|
}
|
211
498
|
|
212
|
-
//
|
213
|
-
|
214
|
-
|
215
|
-
|
499
|
+
// Skip flagging if an ancestor has an accessible label or an interactive role (e.g., button, link)
|
500
|
+
if (
|
501
|
+
['div', 'section', 'article', 'nav'].includes(parents.nodeName.toLowerCase()) &&
|
502
|
+
hasAccessibleLabel(parents)
|
503
|
+
) {
|
504
|
+
customConsoleWarn(
|
505
|
+
'Ancestor element with contextual role has an accessible label, skipping flagging.',
|
506
|
+
);
|
507
|
+
return false;
|
508
|
+
}
|
509
|
+
|
510
|
+
// Skip flag if parent is an a link or button that already contains accessible text
|
511
|
+
if (
|
512
|
+
(parents.nodeName.toLowerCase() === 'a' ||
|
513
|
+
parents.nodeName.toLowerCase() === 'button' ||
|
514
|
+
parents.getAttribute('role') === 'link' ||
|
515
|
+
parents.getAttribute('role') === 'button') &&
|
516
|
+
hasChildWithAccessibleText(parents)
|
517
|
+
) {
|
518
|
+
return false;
|
216
519
|
}
|
217
520
|
|
521
|
+
parents = parents.parentElement;
|
522
|
+
tracedBackedLayerCount++;
|
523
|
+
}
|
524
|
+
|
525
|
+
// Skip elements with role="menuitem" if an accessible sibling, parent, or child is present
|
526
|
+
if (element.getAttribute('role') === 'menuitem') {
|
527
|
+
if (
|
528
|
+
hasSiblingWithAccessibleLabel(element) ||
|
529
|
+
hasChildWithAccessibleText(element) ||
|
530
|
+
hasAccessibleLabel(element.parentElement)
|
531
|
+
) {
|
532
|
+
customConsoleWarn(
|
533
|
+
'Menuitem element or its sibling/parent has an accessible label, skipping flagging.',
|
534
|
+
);
|
535
|
+
return false;
|
536
|
+
}
|
537
|
+
}
|
538
|
+
|
539
|
+
// Skip flagging child elements if the parent element has role="menuitem" and is accessible
|
540
|
+
const parentMenuItem = element.closest('[role="menuitem"]');
|
541
|
+
if (
|
542
|
+
parentMenuItem &&
|
543
|
+
(hasAccessibleLabel(parentMenuItem) || hasChildWithAccessibleText(parentMenuItem))
|
544
|
+
) {
|
545
|
+
customConsoleWarn(
|
546
|
+
'Parent menuitem element has an accessible label or child with accessible text, skipping flagging of its children.',
|
547
|
+
);
|
218
548
|
return false;
|
219
549
|
}
|
220
550
|
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
551
|
+
// Add the new condition for empty div or span elements without any accessible text or children with accessible labels
|
552
|
+
if (
|
553
|
+
(element.nodeName.toLowerCase() === 'span' || element.nodeName.toLowerCase() === 'div') &&
|
554
|
+
element.children.length === 0 &&
|
555
|
+
getTextContent(element).trim().length === 0
|
556
|
+
) {
|
557
|
+
const parent = element.parentElement;
|
558
|
+
if (parent) {
|
559
|
+
const hasAccessibleChild = Array.from(parent.children).some(
|
560
|
+
child => child !== element && hasAccessibleLabel(child),
|
561
|
+
);
|
562
|
+
|
563
|
+
if (hasAccessibleChild) {
|
564
|
+
customConsoleWarn(
|
565
|
+
'Parent element has an accessible child, skipping flagging of empty span or div.',
|
566
|
+
);
|
225
567
|
return false;
|
226
568
|
}
|
227
|
-
|
228
|
-
|
229
|
-
|
569
|
+
}
|
570
|
+
}
|
571
|
+
|
572
|
+
// Do not flag elements with aria-hidden="true"
|
573
|
+
if (element.getAttribute('aria-hidden') === 'true') {
|
574
|
+
customConsoleWarn('Element is aria-hidden, skipping flagging.');
|
575
|
+
return false;
|
576
|
+
}
|
577
|
+
|
578
|
+
if (
|
579
|
+
element.getAttribute('aria-labelledby') !== null &&
|
580
|
+
element.getAttribute('aria-labelledby') !== ''
|
581
|
+
) {
|
582
|
+
// Get the list of IDs referenced in aria-labelledby
|
583
|
+
const ids = element.getAttribute('aria-labelledby').split(' ');
|
584
|
+
let shouldNotFlag = false;
|
585
|
+
|
586
|
+
// Loop through each ID and find the corresponding elements
|
587
|
+
ids.forEach(id => {
|
588
|
+
const referencedElement = document.getElementById(id);
|
589
|
+
|
590
|
+
// Check if the element was found
|
591
|
+
if (
|
592
|
+
referencedElement &&
|
593
|
+
(hasAccessibleLabel(referencedElement) ||
|
594
|
+
isAccessibleText(getTextContent(referencedElement)) ||
|
595
|
+
hasAllChildrenAccessible(referencedElement))
|
596
|
+
) {
|
597
|
+
shouldNotFlag = true;
|
230
598
|
}
|
231
|
-
return (
|
232
|
-
isAccessibleText(child.textContent) || hasAccessibleLabel(child) || hasCSSContent(child)
|
233
|
-
);
|
234
599
|
});
|
235
600
|
|
236
|
-
|
237
|
-
const hasDirectAccessibleText = Array.from(element.childNodes).some(node => {
|
238
|
-
if (node.nodeType === Node.TEXT_NODE) {
|
239
|
-
return isAccessibleText(node.textContent);
|
240
|
-
}
|
601
|
+
if (shouldNotFlag) {
|
241
602
|
return false;
|
242
|
-
}
|
603
|
+
}
|
604
|
+
}
|
243
605
|
|
244
|
-
|
606
|
+
// Do not flag elements with role="presentation"
|
607
|
+
if (element.getAttribute('role') === 'presentation') {
|
608
|
+
customConsoleWarn("Element has role='presentation', skipping flagging.");
|
609
|
+
return false;
|
245
610
|
}
|
246
611
|
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
612
|
+
if (element.dataset.flagged === 'true') {
|
613
|
+
customConsoleWarn('Element is already flagged.');
|
614
|
+
return false;
|
615
|
+
}
|
616
|
+
|
617
|
+
// If an ancestor element is flagged, do not flag this element
|
618
|
+
if (element.closest('[data-flagged="true"]')) {
|
619
|
+
customConsoleWarn('An ancestor element is already flagged.');
|
620
|
+
return false;
|
621
|
+
}
|
622
|
+
|
623
|
+
// Skip elements that are not visible (e.g., display:none)
|
624
|
+
const computedStyle = element.ownerDocument.defaultView.getComputedStyle(element);
|
625
|
+
if (
|
626
|
+
computedStyle.display === 'none' ||
|
627
|
+
computedStyle.visibility === 'hidden' ||
|
628
|
+
element.offsetParent === null
|
629
|
+
) {
|
630
|
+
customConsoleWarn('Element is not visible, skipping flagging.');
|
631
|
+
return false;
|
632
|
+
}
|
633
|
+
|
634
|
+
// Skip empty <div> or <span> elements without any accessible text or children with accessible labels, unless they have a pointer cursor
|
635
|
+
if (
|
636
|
+
(element.nodeName.toLowerCase() === 'div' || element.nodeName.toLowerCase() === 'span') &&
|
637
|
+
element.children.length === 0 &&
|
638
|
+
getTextContent(element).trim().length === 0
|
639
|
+
) {
|
640
|
+
if (!hasPointerCursor(element)) {
|
641
|
+
customConsoleWarn(
|
642
|
+
'Empty div or span without accessible text and without pointer cursor, skipping flagging.',
|
643
|
+
);
|
644
|
+
return false;
|
645
|
+
}
|
646
|
+
|
647
|
+
// **New background-image check**
|
648
|
+
const backgroundImage = window.getComputedStyle(element).getPropertyValue('background-image');
|
649
|
+
if (backgroundImage && backgroundImage !== 'none') {
|
650
|
+
customConsoleWarn('Element has a background image.');
|
651
|
+
|
652
|
+
// Check if the element has accessible labels or text content
|
653
|
+
if (
|
654
|
+
!hasAccessibleLabel(element) &&
|
655
|
+
!hasChildWithAccessibleText(element) &&
|
656
|
+
!isAccessibleText(getTextContent(element))
|
657
|
+
) {
|
658
|
+
customConsoleWarn(
|
659
|
+
'Flagging element with background image but without accessible label or text.',
|
660
|
+
);
|
661
|
+
return true; // Flag the element
|
662
|
+
} else {
|
663
|
+
customConsoleWarn(
|
664
|
+
'Element with background image has accessible label or text, skipping flagging.',
|
665
|
+
);
|
666
|
+
return false; // Do not flag
|
667
|
+
}
|
668
|
+
}
|
669
|
+
|
670
|
+
// **Proceed with ancestor traversal if no background image is found**
|
671
|
+
// Traverse ancestors to check for interactive elements with accessible labels
|
672
|
+
let ancestor = element.parentElement;
|
673
|
+
let depth = 0;
|
674
|
+
const maxDepth = 4; // Limit the depth to prevent skipping elements incorrectly
|
675
|
+
while (ancestor && depth < maxDepth) {
|
676
|
+
// Determine if ancestor is interactive
|
677
|
+
const isAncestorInteractive =
|
678
|
+
hasPointerCursor(ancestor) ||
|
679
|
+
ancestor.hasAttribute('onclick') ||
|
680
|
+
ancestor.hasAttribute('role') ||
|
681
|
+
(ancestor.hasAttribute('tabindex') && ancestor.getAttribute('tabindex') !== '-1') ||
|
682
|
+
ancestor.hasAttribute('jsaction') ||
|
683
|
+
ancestor.hasAttribute('jscontroller');
|
684
|
+
|
685
|
+
if (isAncestorInteractive) {
|
686
|
+
// Check if ancestor has accessible label or text content
|
687
|
+
if (
|
688
|
+
hasAccessibleLabel(ancestor) ||
|
689
|
+
isAccessibleText(getTextContent(ancestor)) ||
|
690
|
+
hasChildWithAccessibleText(ancestor)
|
691
|
+
) {
|
692
|
+
customConsoleWarn(
|
693
|
+
'Ancestor interactive element has accessible label or text content, skipping flagging.',
|
694
|
+
);
|
695
|
+
return false;
|
696
|
+
} else {
|
697
|
+
// Ancestor is interactive but lacks accessible labeling
|
698
|
+
customConsoleWarn(
|
699
|
+
'Ancestor interactive element lacks accessible label, continue flagging.',
|
700
|
+
);
|
701
|
+
// Do not skip flagging
|
702
|
+
}
|
703
|
+
}
|
704
|
+
ancestor = ancestor.parentElement;
|
705
|
+
depth++;
|
251
706
|
}
|
252
707
|
|
253
|
-
// Check if the element itself has an accessible label, text content, or CSS content
|
254
708
|
if (
|
255
709
|
hasAccessibleLabel(element) ||
|
256
|
-
isAccessibleText(element
|
257
|
-
|
710
|
+
isAccessibleText(getTextContent(element)) ||
|
711
|
+
hasChildWithAccessibleText(element)
|
258
712
|
) {
|
259
|
-
|
713
|
+
customConsoleWarn(
|
714
|
+
'Not Flagging clickable div or span with pointer cursor with accessible text.',
|
715
|
+
);
|
716
|
+
return false;
|
260
717
|
}
|
261
718
|
|
262
|
-
// If
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
719
|
+
// If no interactive ancestor with accessible label is found, flag the element
|
720
|
+
customConsoleWarn(
|
721
|
+
'Flagging clickable div or span with pointer cursor and no accessible text.',
|
722
|
+
);
|
723
|
+
return true;
|
724
|
+
}
|
725
|
+
|
726
|
+
// Skip elements with role="menuitem" and ensure accessibility label for any nested elements
|
727
|
+
if (element.getAttribute('role') === 'menuitem') {
|
728
|
+
if (hasChildWithAccessibleText(element)) {
|
729
|
+
customConsoleWarn('Menuitem element has child with accessible text, skipping flagging.');
|
730
|
+
return false;
|
272
731
|
}
|
732
|
+
}
|
273
733
|
|
274
|
-
|
734
|
+
// Check if the parent element has an accessible label
|
735
|
+
const parent = element.closest('[aria-label], [role="button"], [role="link"], a, button');
|
736
|
+
|
737
|
+
if (parent && (hasAccessibleLabel(parent) || hasChildWithAccessibleText(parent))) {
|
738
|
+
customConsoleWarn(
|
739
|
+
'Parent element has an accessible label or accessible child, skipping flagging.',
|
740
|
+
);
|
275
741
|
return false;
|
276
742
|
}
|
277
743
|
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
child.nodeName.toLowerCase() === "button" ||
|
284
|
-
child.getAttribute('role') === 'link' ||
|
285
|
-
child.getAttribute('role') === 'button';
|
286
|
-
}
|
287
|
-
return false;
|
288
|
-
};
|
289
|
-
|
290
|
-
// Check element children
|
291
|
-
const hasAccessibleChildElement = Array.from(element.children).some(child => {
|
292
|
-
if (child instanceof Element) { // Ensure child is an Element
|
293
|
-
if (!hasPointerCursor(child)) {
|
294
|
-
return false;
|
295
|
-
}
|
296
|
-
|
297
|
-
if (child.nodeName.toLowerCase() === "style" || child.nodeName.toLowerCase() === "script") {
|
298
|
-
return false;
|
299
|
-
}
|
300
|
-
|
301
|
-
// Skip children that are aria-hidden or links/buttons
|
302
|
-
if (child.getAttribute('aria-hidden') === 'true' || isLinkOrButton(child) || !isVisibleFocusAble(child)) {
|
303
|
-
return false;
|
304
|
-
}
|
305
|
-
|
306
|
-
// Check if the child element has accessible text or label
|
307
|
-
return isAccessibleText(getTextContent(child)) || hasAccessibleLabel(child) || hasCSSContent(child);
|
308
|
-
}
|
309
|
-
return false;
|
310
|
-
});
|
311
|
-
|
312
|
-
// Check direct text nodes inside the element itself (like <a>"text"</a>)
|
313
|
-
const hasDirectAccessibleText = Array.from(element.childNodes).some(node => {
|
314
|
-
if (node.nodeType === Node.TEXT_NODE) {
|
315
|
-
const textContent = getTextContent(node);
|
316
|
-
|
317
|
-
// Check if the text contains non-ASCII characters (Unicode)
|
318
|
-
const containsUnicode = /[^\x00-\x7F]/.test(textContent);
|
319
|
-
|
320
|
-
// If contains non-ASCII characters, validate with isValidUnicode
|
321
|
-
if (containsUnicode) {
|
322
|
-
return isValidUnicode(textContent);
|
323
|
-
}
|
744
|
+
// Skip flagging if any child has an accessible label (e.g., <img alt="...">
|
745
|
+
if (hasAllChildrenAccessible(element)) {
|
746
|
+
customConsoleWarn('Element has child nodes with accessible text.');
|
747
|
+
return false;
|
748
|
+
}
|
324
749
|
|
325
|
-
|
326
|
-
|
327
|
-
|
750
|
+
// Check if the <a> element has all children accessible
|
751
|
+
if (element.nodeName.toLowerCase() === 'a' && hasAllChildrenAccessible(element)) {
|
752
|
+
customConsoleWarn('Hyperlink has all children with accessible labels, skipping flagging.');
|
753
|
+
return false;
|
754
|
+
}
|
328
755
|
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
const innerTextContent = getTextContent(innerNode).trim();
|
334
|
-
return innerTextContent && !isValidUnicode(innerTextContent); // Check for non-Unicode content
|
335
|
-
}
|
336
|
-
return false;
|
337
|
-
});
|
338
|
-
}
|
756
|
+
if (element.hasAttribute('tabindex') && element.getAttribute('tabindex') === '-1') {
|
757
|
+
customConsoleWarn("Element has tabindex='-1'.");
|
758
|
+
return false;
|
759
|
+
}
|
339
760
|
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
761
|
+
const childWithTabindexNegativeOne = Array.from(element.children).some(
|
762
|
+
child => child.hasAttribute('tabindex') && child.getAttribute('tabindex') === '-1',
|
763
|
+
);
|
764
|
+
if (childWithTabindexNegativeOne) {
|
765
|
+
customConsoleWarn("Element has a child with tabindex='-1'.");
|
766
|
+
return false;
|
345
767
|
}
|
346
|
-
|
347
768
|
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
outline: 4px solid rgba(128, 0, 128, 1) !important; /* Thicker primary outline with purple in rgba format */
|
352
|
-
box-shadow:
|
353
|
-
0 0 25px 15px rgba(255, 255, 255, 1), /* White glow for contrast */
|
354
|
-
0 0 15px 10px rgba(144, 33, 166, 1) !important; /* Consistent purple glow in rgba format */
|
769
|
+
if (landmarkElements.includes(element.nodeName.toLowerCase())) {
|
770
|
+
customConsoleWarn('Element is a landmark element.');
|
771
|
+
return false;
|
355
772
|
}
|
356
|
-
`;
|
357
|
-
document.head.appendChild(style);
|
358
773
|
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
`;
|
372
|
-
frameDocument.head.appendChild(frameStyle);
|
373
|
-
}
|
374
|
-
} catch (error) {
|
375
|
-
customConsoleWarn(`Cannot access frame document: ${error}`);
|
376
|
-
}
|
774
|
+
// Prevent flagging <svg> or <icon> if a sibling or parent has an accessible label or if it is part of a button-like element
|
775
|
+
if (
|
776
|
+
(element.nodeName.toLowerCase() === 'svg' || element.nodeName.toLowerCase() === 'icon') &&
|
777
|
+
(element.getAttribute('focusable') === 'false' ||
|
778
|
+
hasSiblingOrParentAccessibleLabel(element) ||
|
779
|
+
element.closest('[role="button"]') ||
|
780
|
+
element.closest('button'))
|
781
|
+
) {
|
782
|
+
customConsoleWarn(
|
783
|
+
'Sibling or parent element has an accessible label or svg is part of a button, skipping flagging of svg or icon.',
|
784
|
+
);
|
785
|
+
return false;
|
377
786
|
}
|
378
787
|
|
379
|
-
|
380
|
-
const
|
381
|
-
|
382
|
-
|
383
|
-
|
788
|
+
if (element.nodeName.toLowerCase() === 'svg') {
|
789
|
+
const parentGroup = element.closest('g');
|
790
|
+
if (parentGroup && parentGroup.querySelector('title')) {
|
791
|
+
customConsoleWarn('Parent group element has a <title>, skipping flagging of svg.');
|
792
|
+
return false;
|
793
|
+
}
|
794
|
+
}
|
384
795
|
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
796
|
+
if (element.nodeName.toLowerCase() === 'button') {
|
797
|
+
const hasAccessibleLabelForButton =
|
798
|
+
hasAccessibleLabel(element) || isAccessibleText(getTextContent(element));
|
799
|
+
if (hasAccessibleLabelForButton) {
|
800
|
+
customConsoleWarn('Button has an accessible label, skipping flagging.');
|
801
|
+
return false;
|
802
|
+
}
|
391
803
|
|
392
|
-
|
393
|
-
|
804
|
+
const hasSvgChildWithoutLabel = Array.from(element.children).some(
|
805
|
+
child => child.nodeName.toLowerCase() === 'svg' && !hasAccessibleLabel(child),
|
806
|
+
);
|
807
|
+
if (hasSvgChildWithoutLabel) {
|
808
|
+
customConsoleWarn('Flagging button with child SVG lacking accessible label.');
|
809
|
+
return true;
|
394
810
|
}
|
811
|
+
}
|
395
812
|
|
396
|
-
|
813
|
+
if (
|
814
|
+
element.nodeName.toLowerCase() === 'input' &&
|
815
|
+
(element as HTMLInputElement).type === 'image' &&
|
816
|
+
!hasAccessibleLabel(element)
|
817
|
+
) {
|
818
|
+
customConsoleWarn("Flagging <input type='image'> without accessible label.");
|
819
|
+
return true;
|
397
820
|
}
|
398
821
|
|
399
|
-
|
400
|
-
|
401
|
-
const rect = element.getBoundingClientRect();
|
822
|
+
if (element.nodeName.toLowerCase() === 'a') {
|
823
|
+
const img = element.querySelector('img');
|
402
824
|
|
403
|
-
//
|
404
|
-
|
405
|
-
|
825
|
+
// Log to verify visibility and pointer checks
|
826
|
+
customConsoleWarn('Processing <a> element.');
|
827
|
+
|
828
|
+
// Ensure this <a> does not have an accessible label
|
829
|
+
const linkHasAccessibleLabel = hasAccessibleLabel(element);
|
830
|
+
|
831
|
+
// Ensure the <img> inside <a> does not have an accessible label
|
832
|
+
const imgHasAccessibleLabel = img ? hasAccessibleLabel(img) : false;
|
406
833
|
|
407
|
-
|
408
|
-
if (
|
409
|
-
|
834
|
+
// Log to verify if <img> has accessible label
|
835
|
+
if (img) {
|
836
|
+
customConsoleWarn('Found <img> inside <a>. Accessible label: ' + imgHasAccessibleLabel);
|
837
|
+
} else {
|
838
|
+
customConsoleWarn('No <img> found inside <a>.');
|
410
839
|
}
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
}
|
428
|
-
|
429
|
-
// Recursively check child elements if it's an element node
|
430
|
-
if (isVisibleFocusAble(elementNode)) {
|
431
|
-
const childText = getTextContent(elementNode);
|
432
|
-
if (childText) {
|
433
|
-
textContent += childText; // Append valid child text
|
434
|
-
}
|
435
|
-
}
|
436
|
-
}
|
840
|
+
|
841
|
+
// Skip flagging if <a> has an accessible label or all children are accessible
|
842
|
+
if (linkHasAccessibleLabel || hasChildNotANewInteractWithAccessibleText(element)) {
|
843
|
+
customConsoleWarn('Hyperlink has an accessible label, skipping flagging.');
|
844
|
+
return false;
|
845
|
+
}
|
846
|
+
|
847
|
+
// Flag if both <a> and <img> inside lack accessible labels
|
848
|
+
if (!linkHasAccessibleLabel && img && !imgHasAccessibleLabel) {
|
849
|
+
customConsoleWarn('Flagging <a> with inaccessible <img>.');
|
850
|
+
return true;
|
851
|
+
}
|
852
|
+
|
853
|
+
if (!linkHasAccessibleLabel) {
|
854
|
+
customConsoleWarn('Flagging <a> with no accessible label');
|
855
|
+
return true;
|
437
856
|
}
|
438
|
-
|
439
|
-
return textContent.trim(); // Return the combined text content
|
440
857
|
}
|
441
|
-
|
442
858
|
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
// Do not flag elements if any ancestor has aria-hidden="true"
|
456
|
-
if (element.closest('[aria-hidden="true"]')) {
|
457
|
-
customConsoleWarn("An ancestor element has aria-hidden='true', skipping flagging.");
|
458
|
-
return false;
|
459
|
-
}
|
460
|
-
|
461
|
-
let parents = element.parentElement;
|
462
|
-
|
463
|
-
// Causing false negative of svg
|
464
|
-
if (parents) {
|
465
|
-
// Check if the parent has an accessible label
|
466
|
-
if (hasAccessibleLabel(parents) || hasChildNotANewInteractWithAccessibleText(parents)) {
|
467
|
-
customConsoleWarn("Parent element has an accessible label, skipping flagging of this element.");
|
468
|
-
return false;
|
469
|
-
}
|
470
|
-
|
471
|
-
}
|
472
|
-
|
473
|
-
let maxLayers = 3;
|
474
|
-
let tracedBackedLayerCount = 0;
|
475
|
-
while (parents && tracedBackedLayerCount <= maxLayers) {
|
476
|
-
// Skip flagging if the parent or the element itself has an accessible label
|
477
|
-
if (hasAccessibleLabel(parents) || hasChildNotANewInteractWithAccessibleText(parents)) {
|
478
|
-
customConsoleWarn("Parent or element has an accessible label, skipping flagging.",parents);
|
479
|
-
return false;
|
480
|
-
}
|
481
|
-
|
482
|
-
// Skip flagging if the parent is a button-like element with aria-expanded
|
483
|
-
if (
|
484
|
-
parents.getAttribute('role') === 'button' &&
|
485
|
-
(parents.hasAttribute('aria-expanded') || parents.hasAttribute('aria-controls'))
|
486
|
-
) {
|
487
|
-
customConsoleWarn("Parent element is an interactive button with aria-expanded or aria-controls, skipping flagging.");
|
488
|
-
return false;
|
489
|
-
}
|
490
|
-
|
491
|
-
// Skip flagging if an ancestor has an accessible label or an interactive role (e.g., button, link)
|
492
|
-
if (
|
493
|
-
['div', 'section', 'article', 'nav'].includes(parents.nodeName.toLowerCase()) &&
|
494
|
-
hasAccessibleLabel(parents)
|
495
|
-
) {
|
496
|
-
customConsoleWarn("Ancestor element with contextual role has an accessible label, skipping flagging.");
|
497
|
-
return false;
|
498
|
-
}
|
499
|
-
|
500
|
-
// Skip flag if parent is an a link or button that already contains accessible text
|
501
|
-
if (
|
502
|
-
(parents.nodeName.toLowerCase() === "a" || parents.nodeName.toLowerCase() === "button" ||
|
503
|
-
parents.getAttribute('role') === 'link' || parents.getAttribute('role') === 'button') && hasChildWithAccessibleText(parents)
|
504
|
-
){
|
505
|
-
return false;
|
506
|
-
}
|
507
|
-
|
508
|
-
parents = parents.parentElement;
|
509
|
-
tracedBackedLayerCount++;
|
510
|
-
}
|
511
|
-
|
512
|
-
|
513
|
-
// Skip elements with role="menuitem" if an accessible sibling, parent, or child is present
|
514
|
-
if (element.getAttribute('role') === 'menuitem') {
|
515
|
-
if (hasSiblingWithAccessibleLabel(element) || hasChildWithAccessibleText(element) || hasAccessibleLabel(element.parentElement)) {
|
516
|
-
customConsoleWarn("Menuitem element or its sibling/parent has an accessible label, skipping flagging.");
|
517
|
-
return false;
|
518
|
-
}
|
519
|
-
}
|
520
|
-
|
521
|
-
// Skip flagging child elements if the parent element has role="menuitem" and is accessible
|
522
|
-
const parentMenuItem = element.closest('[role="menuitem"]');
|
523
|
-
if (parentMenuItem && (hasAccessibleLabel(parentMenuItem) || hasChildWithAccessibleText(parentMenuItem))) {
|
524
|
-
customConsoleWarn("Parent menuitem element has an accessible label or child with accessible text, skipping flagging of its children.");
|
525
|
-
return false;
|
526
|
-
}
|
527
|
-
|
528
|
-
// Add the new condition for empty div or span elements without any accessible text or children with accessible labels
|
529
|
-
if ((element.nodeName.toLowerCase() === 'span' || element.nodeName.toLowerCase() === 'div') &&
|
530
|
-
element.children.length === 0 && getTextContent(element).trim().length === 0) {
|
531
|
-
const parent = element.parentElement;
|
532
|
-
if (parent) {
|
533
|
-
const hasAccessibleChild = Array.from(parent.children).some(child =>
|
534
|
-
child !== element && hasAccessibleLabel(child)
|
535
|
-
);
|
536
|
-
|
537
|
-
if (hasAccessibleChild) {
|
538
|
-
customConsoleWarn("Parent element has an accessible child, skipping flagging of empty span or div.");
|
539
|
-
return false;
|
540
|
-
}
|
541
|
-
}
|
542
|
-
}
|
543
|
-
|
544
|
-
// Do not flag elements with aria-hidden="true"
|
545
|
-
if (element.getAttribute('aria-hidden') === 'true') {
|
546
|
-
customConsoleWarn("Element is aria-hidden, skipping flagging.");
|
547
|
-
return false;
|
548
|
-
}
|
549
|
-
|
550
|
-
if (element.getAttribute("aria-labelledby") !== null && element.getAttribute("aria-labelledby") !== "") {
|
551
|
-
// Get the list of IDs referenced in aria-labelledby
|
552
|
-
const ids = element.getAttribute("aria-labelledby").split(' ');
|
553
|
-
let shouldNotFlag = false
|
554
|
-
|
555
|
-
// Loop through each ID and find the corresponding elements
|
556
|
-
ids.forEach(id => {
|
557
|
-
const referencedElement = document.getElementById(id);
|
558
|
-
|
559
|
-
// Check if the element was found
|
560
|
-
if (referencedElement &&
|
561
|
-
(hasAccessibleLabel(referencedElement) ||
|
562
|
-
isAccessibleText(getTextContent(referencedElement)) ||
|
563
|
-
hasAllChildrenAccessible(referencedElement) ))
|
564
|
-
{
|
565
|
-
shouldNotFlag = true;
|
566
|
-
}
|
567
|
-
});
|
568
|
-
|
569
|
-
if (shouldNotFlag)
|
570
|
-
{
|
571
|
-
return false
|
572
|
-
}
|
573
|
-
|
574
|
-
}
|
575
|
-
|
576
|
-
// Do not flag elements with role="presentation"
|
577
|
-
if (element.getAttribute('role') === 'presentation') {
|
578
|
-
customConsoleWarn("Element has role='presentation', skipping flagging.");
|
579
|
-
return false;
|
580
|
-
}
|
581
|
-
|
582
|
-
if (element.dataset.flagged === 'true') {
|
583
|
-
customConsoleWarn("Element is already flagged.");
|
584
|
-
return false;
|
585
|
-
}
|
586
|
-
|
587
|
-
// If an ancestor element is flagged, do not flag this element
|
588
|
-
if (element.closest('[data-flagged="true"]')) {
|
589
|
-
customConsoleWarn("An ancestor element is already flagged.");
|
590
|
-
return false;
|
591
|
-
}
|
592
|
-
|
593
|
-
// Skip elements that are not visible (e.g., display:none)
|
594
|
-
const computedStyle = element.ownerDocument.defaultView.getComputedStyle(element);
|
595
|
-
if (computedStyle.display === 'none' || computedStyle.visibility === 'hidden' || element.offsetParent === null) {
|
596
|
-
customConsoleWarn("Element is not visible, skipping flagging.");
|
597
|
-
return false;
|
859
|
+
// Modify this section for generic elements
|
860
|
+
if (['span', 'div', 'icon', 'svg', 'button'].includes(element.nodeName.toLowerCase())) {
|
861
|
+
if (element.nodeName.toLowerCase() === 'icon' || element.nodeName.toLowerCase() === 'svg') {
|
862
|
+
// Check if the element has an accessible label or if it has a sibling, parent, or summary/related element that provides an accessible label
|
863
|
+
if (
|
864
|
+
!hasAccessibleLabel(element) &&
|
865
|
+
!hasSiblingOrParentAccessibleLabel(element) &&
|
866
|
+
!hasSummaryOrDetailsLabel(element) &&
|
867
|
+
element.getAttribute('focusable') !== 'false'
|
868
|
+
) {
|
869
|
+
customConsoleWarn('Flagging icon or svg without accessible label.');
|
870
|
+
return true;
|
598
871
|
}
|
599
|
-
|
600
|
-
|
601
|
-
if ((element.nodeName.toLowerCase() === 'div' || element.nodeName.toLowerCase() === 'span') &&
|
602
|
-
element.children.length === 0 && getTextContent(element).trim().length === 0) {
|
603
|
-
|
604
|
-
if (!hasPointerCursor(element)) {
|
605
|
-
customConsoleWarn("Empty div or span without accessible text and without pointer cursor, skipping flagging.");
|
606
|
-
return false;
|
607
|
-
}
|
608
|
-
|
609
|
-
// **New background-image check**
|
610
|
-
const backgroundImage = window.getComputedStyle(element).getPropertyValue('background-image');
|
611
|
-
if (backgroundImage && backgroundImage !== 'none') {
|
612
|
-
customConsoleWarn("Element has a background image.");
|
613
|
-
|
614
|
-
// Check if the element has accessible labels or text content
|
615
|
-
if (!hasAccessibleLabel(element) && !hasChildWithAccessibleText(element) && !isAccessibleText(getTextContent(element))) {
|
616
|
-
customConsoleWarn("Flagging element with background image but without accessible label or text.");
|
617
|
-
return true; // Flag the element
|
618
|
-
} else {
|
619
|
-
customConsoleWarn("Element with background image has accessible label or text, skipping flagging.");
|
620
|
-
return false; // Do not flag
|
621
|
-
}
|
622
|
-
}
|
623
|
-
|
624
|
-
// **Proceed with ancestor traversal if no background image is found**
|
625
|
-
// Traverse ancestors to check for interactive elements with accessible labels
|
626
|
-
let ancestor = element.parentElement;
|
627
|
-
let depth = 0;
|
628
|
-
const maxDepth = 4; // Limit the depth to prevent skipping elements incorrectly
|
629
|
-
while (ancestor && depth < maxDepth) {
|
630
|
-
// Determine if ancestor is interactive
|
631
|
-
const isAncestorInteractive = hasPointerCursor(ancestor) ||
|
632
|
-
ancestor.hasAttribute('onclick') ||
|
633
|
-
ancestor.hasAttribute('role') ||
|
634
|
-
(ancestor.hasAttribute('tabindex') && ancestor.getAttribute('tabindex') !== '-1') ||
|
635
|
-
ancestor.hasAttribute('jsaction') ||
|
636
|
-
ancestor.hasAttribute('jscontroller');
|
637
|
-
|
638
|
-
if (isAncestorInteractive) {
|
639
|
-
// Check if ancestor has accessible label or text content
|
640
|
-
if (hasAccessibleLabel(ancestor) || isAccessibleText(getTextContent(ancestor)) || hasChildWithAccessibleText(ancestor)) {
|
641
|
-
customConsoleWarn("Ancestor interactive element has accessible label or text content, skipping flagging.");
|
642
|
-
return false;
|
643
|
-
} else {
|
644
|
-
// Ancestor is interactive but lacks accessible labeling
|
645
|
-
customConsoleWarn("Ancestor interactive element lacks accessible label, continue flagging.");
|
646
|
-
// Do not skip flagging
|
647
|
-
}
|
648
|
-
}
|
649
|
-
ancestor = ancestor.parentElement;
|
650
|
-
depth++;
|
651
|
-
}
|
652
|
-
|
653
|
-
if (hasAccessibleLabel(element) || isAccessibleText(getTextContent(element)) || hasChildWithAccessibleText(element))
|
654
|
-
{
|
655
|
-
customConsoleWarn("Not Flagging clickable div or span with pointer cursor with accessible text.");
|
656
|
-
return false;
|
657
|
-
}
|
872
|
+
return false;
|
873
|
+
}
|
658
874
|
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
}
|
663
|
-
|
664
|
-
// Skip elements with role="menuitem" and ensure accessibility label for any nested elements
|
665
|
-
if (element.getAttribute('role') === 'menuitem') {
|
666
|
-
if (hasChildWithAccessibleText(element)) {
|
667
|
-
customConsoleWarn("Menuitem element has child with accessible text, skipping flagging.");
|
668
|
-
return false;
|
669
|
-
}
|
670
|
-
}
|
671
|
-
|
672
|
-
// Check if the parent element has an accessible label
|
673
|
-
const parent = element.closest('[aria-label], [role="button"], [role="link"], a, button');
|
674
|
-
|
675
|
-
if (parent && (hasAccessibleLabel(parent) || hasChildWithAccessibleText(parent))) {
|
676
|
-
customConsoleWarn("Parent element has an accessible label or accessible child, skipping flagging.");
|
677
|
-
return false;
|
678
|
-
}
|
679
|
-
|
680
|
-
// Skip flagging if any child has an accessible label (e.g., <img alt="...">
|
681
|
-
if (hasAllChildrenAccessible(element)) {
|
682
|
-
customConsoleWarn("Element has child nodes with accessible text.");
|
683
|
-
return false;
|
684
|
-
}
|
685
|
-
|
686
|
-
// Check if the <a> element has all children accessible
|
687
|
-
if (element.nodeName.toLowerCase() === 'a' && hasAllChildrenAccessible(element)) {
|
688
|
-
customConsoleWarn("Hyperlink has all children with accessible labels, skipping flagging.");
|
689
|
-
return false;
|
690
|
-
}
|
691
|
-
|
692
|
-
if (element.hasAttribute('tabindex') && element.getAttribute('tabindex') === '-1') {
|
693
|
-
customConsoleWarn("Element has tabindex='-1'.");
|
694
|
-
return false;
|
695
|
-
}
|
696
|
-
|
697
|
-
const childWithTabindexNegativeOne = Array.from(element.children).some(child =>
|
698
|
-
child.hasAttribute('tabindex') && child.getAttribute('tabindex') === '-1'
|
699
|
-
);
|
700
|
-
if (childWithTabindexNegativeOne) {
|
701
|
-
customConsoleWarn("Element has a child with tabindex='-1'.");
|
702
|
-
return false;
|
703
|
-
}
|
704
|
-
|
705
|
-
if (landmarkElements.includes(element.nodeName.toLowerCase())) {
|
706
|
-
customConsoleWarn("Element is a landmark element.");
|
707
|
-
return false;
|
708
|
-
}
|
709
|
-
|
710
|
-
// Prevent flagging <svg> or <icon> if a sibling or parent has an accessible label or if it is part of a button-like element
|
711
|
-
if ((element.nodeName.toLowerCase() === 'svg' || element.nodeName.toLowerCase() === 'icon') && (element.getAttribute('focusable') === 'false' || hasSiblingOrParentAccessibleLabel(element) || element.closest('[role="button"]') || element.closest('button'))) {
|
712
|
-
customConsoleWarn("Sibling or parent element has an accessible label or svg is part of a button, skipping flagging of svg or icon.");
|
713
|
-
return false;
|
714
|
-
}
|
715
|
-
|
716
|
-
if (element.nodeName.toLowerCase() === 'svg') {
|
717
|
-
const parentGroup = element.closest('g');
|
718
|
-
if (parentGroup && parentGroup.querySelector('title')) {
|
719
|
-
customConsoleWarn("Parent group element has a <title>, skipping flagging of svg.");
|
720
|
-
return false;
|
721
|
-
}
|
722
|
-
}
|
723
|
-
|
724
|
-
if (element.nodeName.toLowerCase() === 'button') {
|
725
|
-
const hasAccessibleLabelForButton = hasAccessibleLabel(element) || isAccessibleText(getTextContent(element));
|
726
|
-
if (hasAccessibleLabelForButton) {
|
727
|
-
customConsoleWarn("Button has an accessible label, skipping flagging.");
|
728
|
-
return false;
|
729
|
-
}
|
730
|
-
|
731
|
-
const hasSvgChildWithoutLabel = Array.from(element.children).some(child => child.nodeName.toLowerCase() === 'svg' && !hasAccessibleLabel(child));
|
732
|
-
if (hasSvgChildWithoutLabel) {
|
733
|
-
customConsoleWarn("Flagging button with child SVG lacking accessible label.");
|
734
|
-
return true;
|
735
|
-
}
|
736
|
-
}
|
737
|
-
|
738
|
-
if (element.nodeName.toLowerCase() === 'input' && (element as HTMLInputElement).type === 'image' && !hasAccessibleLabel(element)) {
|
739
|
-
customConsoleWarn("Flagging <input type='image'> without accessible label.");
|
740
|
-
return true;
|
875
|
+
if (getTextContent(element).trim().length > 0) {
|
876
|
+
customConsoleWarn('Element has valid text content.');
|
877
|
+
return false;
|
741
878
|
}
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
customConsoleWarn("Flagging <a> with no accessible label");
|
778
|
-
return true;
|
779
|
-
}
|
780
|
-
}
|
781
|
-
|
782
|
-
// Modify this section for generic elements
|
783
|
-
if (['span', 'div', 'icon', 'svg', 'button'].includes(element.nodeName.toLowerCase())) {
|
784
|
-
if (element.nodeName.toLowerCase() === 'icon' || element.nodeName.toLowerCase() === 'svg') {
|
785
|
-
// Check if the element has an accessible label or if it has a sibling, parent, or summary/related element that provides an accessible label
|
786
|
-
if (!hasAccessibleLabel(element) && !hasSiblingOrParentAccessibleLabel(element) && !hasSummaryOrDetailsLabel(element) && element.getAttribute('focusable') !== 'false') {
|
787
|
-
customConsoleWarn("Flagging icon or svg without accessible label.");
|
788
|
-
return true;
|
789
|
-
}
|
790
|
-
return false;
|
791
|
-
}
|
792
|
-
|
793
|
-
if (getTextContent(element).trim().length > 0) {
|
794
|
-
customConsoleWarn("Element has valid text content.");
|
795
|
-
return false;
|
796
|
-
}
|
797
|
-
|
798
|
-
if (element.hasAttribute('aria-label') && element.getAttribute('aria-label').trim().length > 0) {
|
799
|
-
customConsoleWarn("Element has an aria-label attribute, skipping flagging.");
|
800
|
-
return false;
|
801
|
-
}
|
802
|
-
}
|
803
|
-
|
804
|
-
if (element.nodeName.toLowerCase() === 'div') {
|
805
|
-
const flaggedChild = Array.from(element.children).some(child => (child as HTMLElement).dataset.flagged === 'true');
|
806
|
-
if (flaggedChild) {
|
807
|
-
customConsoleWarn("Div contains a flagged child, flagging only outermost element.");
|
808
|
-
return false;
|
809
|
-
}
|
810
|
-
|
811
|
-
// Update this condition to include hasChildWithAccessibleText
|
812
|
-
if (getTextContent(element).trim().length > 0 || hasChildWithAccessibleText(element)) {
|
813
|
-
customConsoleWarn("Div has valid text content or child with accessible text.");
|
814
|
-
return false;
|
815
|
-
}
|
816
|
-
|
817
|
-
const img = element.querySelector('img');
|
818
|
-
if (img) {
|
819
|
-
const altText = img.getAttribute('alt');
|
820
|
-
const ariaLabel = img.getAttribute('aria-label');
|
821
|
-
const ariaLabelledByText = getAriaLabelledByText(img);
|
822
|
-
if (altText !== null || ariaLabel || ariaLabelledByText) {
|
823
|
-
customConsoleWarn("Div contains an accessible img or an img with an alt attribute (even if empty).");
|
824
|
-
return false;
|
825
|
-
}
|
826
|
-
}
|
827
|
-
|
828
|
-
const svg = element.querySelector('svg');
|
829
|
-
if (svg) {
|
830
|
-
if (hasPointerCursor(element) && !hasAccessibleLabel(svg) && !hasSummaryOrDetailsLabel(svg) && svg.getAttribute('focusable') !== 'false') {
|
831
|
-
customConsoleWarn("Flagging clickable div with SVG without accessible label.");
|
832
|
-
return true;
|
833
|
-
}
|
834
|
-
}
|
835
|
-
|
836
|
-
if (hasPointerCursor(element) && !hasAccessibleLabel(element) && !isAccessibleText(getTextContent(element))) {
|
837
|
-
customConsoleWarn("Clickable div without accessible label or text content.");
|
838
|
-
return true;
|
839
|
-
}
|
840
|
-
}
|
841
|
-
|
842
|
-
if (element.nodeName.toLowerCase() === 'img' || element.nodeName.toLowerCase() === 'picture') {
|
843
|
-
const imgElement = element.nodeName.toLowerCase() === 'picture' ? element.querySelector('img') : element;
|
844
|
-
const altText = imgElement.getAttribute('alt');
|
845
|
-
const ariaLabel = imgElement.getAttribute('aria-label');
|
846
|
-
const ariaLabelledByText = getAriaLabelledByText(imgElement);
|
847
|
-
|
848
|
-
if (!allowNonClickableFlagging) {
|
849
|
-
if (!imgElement.closest('a') && !imgElement.closest('button') && !hasPointerCursor(imgElement) && !(altText !== null) && !(ariaLabel && ariaLabel.trim().length > 0) && !(ariaLabelledByText && ariaLabelledByText.length > 0)) {
|
850
|
-
customConsoleWarn("Non-clickable image ignored.");
|
851
|
-
return false;
|
852
|
-
}
|
853
|
-
}
|
854
|
-
|
855
|
-
if (!imgElement.closest('a') && !imgElement.closest('button') && !(altText !== null) && !(ariaLabel && ariaLabel.trim().length > 0) && !(ariaLabelledByText && ariaLabelledByText.length > 0)) {
|
856
|
-
customConsoleWarn("Flagging img or picture without accessible label.");
|
857
|
-
return true;
|
858
|
-
}
|
879
|
+
|
880
|
+
if (
|
881
|
+
element.hasAttribute('aria-label') &&
|
882
|
+
element.getAttribute('aria-label').trim().length > 0
|
883
|
+
) {
|
884
|
+
customConsoleWarn('Element has an aria-label attribute, skipping flagging.');
|
885
|
+
return false;
|
886
|
+
}
|
887
|
+
}
|
888
|
+
|
889
|
+
if (element.nodeName.toLowerCase() === 'div') {
|
890
|
+
const flaggedChild = Array.from(element.children).some(
|
891
|
+
child => (child as HTMLElement).dataset.flagged === 'true',
|
892
|
+
);
|
893
|
+
if (flaggedChild) {
|
894
|
+
customConsoleWarn('Div contains a flagged child, flagging only outermost element.');
|
895
|
+
return false;
|
896
|
+
}
|
897
|
+
|
898
|
+
// Update this condition to include hasChildWithAccessibleText
|
899
|
+
if (getTextContent(element).trim().length > 0 || hasChildWithAccessibleText(element)) {
|
900
|
+
customConsoleWarn('Div has valid text content or child with accessible text.');
|
901
|
+
return false;
|
902
|
+
}
|
903
|
+
|
904
|
+
const img = element.querySelector('img');
|
905
|
+
if (img) {
|
906
|
+
const altText = img.getAttribute('alt');
|
907
|
+
const ariaLabel = img.getAttribute('aria-label');
|
908
|
+
const ariaLabelledByText = getAriaLabelledByText(img);
|
909
|
+
if (altText !== null || ariaLabel || ariaLabelledByText) {
|
910
|
+
customConsoleWarn(
|
911
|
+
'Div contains an accessible img or an img with an alt attribute (even if empty).',
|
912
|
+
);
|
913
|
+
return false;
|
859
914
|
}
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
915
|
+
}
|
916
|
+
|
917
|
+
const svg = element.querySelector('svg');
|
918
|
+
if (svg) {
|
919
|
+
if (
|
920
|
+
hasPointerCursor(element) &&
|
921
|
+
!hasAccessibleLabel(svg) &&
|
922
|
+
!hasSummaryOrDetailsLabel(svg) &&
|
923
|
+
svg.getAttribute('focusable') !== 'false'
|
924
|
+
) {
|
925
|
+
customConsoleWarn('Flagging clickable div with SVG without accessible label.');
|
926
|
+
return true;
|
866
927
|
}
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
|
928
|
+
}
|
929
|
+
|
930
|
+
if (
|
931
|
+
hasPointerCursor(element) &&
|
932
|
+
!hasAccessibleLabel(element) &&
|
933
|
+
!isAccessibleText(getTextContent(element))
|
934
|
+
) {
|
935
|
+
customConsoleWarn('Clickable div without accessible label or text content.');
|
936
|
+
return true;
|
937
|
+
}
|
938
|
+
}
|
939
|
+
|
940
|
+
if (element.nodeName.toLowerCase() === 'img' || element.nodeName.toLowerCase() === 'picture') {
|
941
|
+
const imgElement =
|
942
|
+
element.nodeName.toLowerCase() === 'picture' ? element.querySelector('img') : element;
|
943
|
+
const altText = imgElement.getAttribute('alt');
|
944
|
+
const ariaLabel = imgElement.getAttribute('aria-label');
|
945
|
+
const ariaLabelledByText = getAriaLabelledByText(imgElement);
|
946
|
+
|
947
|
+
if (!allowNonClickableFlagging) {
|
948
|
+
if (
|
949
|
+
!imgElement.closest('a') &&
|
950
|
+
!imgElement.closest('button') &&
|
951
|
+
!hasPointerCursor(imgElement) &&
|
952
|
+
!(altText !== null) &&
|
953
|
+
!(ariaLabel && ariaLabel.trim().length > 0) &&
|
954
|
+
!(ariaLabelledByText && ariaLabelledByText.length > 0)
|
955
|
+
) {
|
956
|
+
customConsoleWarn('Non-clickable image ignored.');
|
957
|
+
return false;
|
871
958
|
}
|
872
|
-
|
873
|
-
|
959
|
+
}
|
960
|
+
|
961
|
+
if (
|
962
|
+
!imgElement.closest('a') &&
|
963
|
+
!imgElement.closest('button') &&
|
964
|
+
!(altText !== null) &&
|
965
|
+
!(ariaLabel && ariaLabel.trim().length > 0) &&
|
966
|
+
!(ariaLabelledByText && ariaLabelledByText.length > 0)
|
967
|
+
) {
|
968
|
+
customConsoleWarn('Flagging img or picture without accessible label.');
|
969
|
+
return true;
|
970
|
+
}
|
874
971
|
}
|
875
972
|
|
876
|
-
|
973
|
+
// Additional check to skip divs with empty children or child-child elements
|
974
|
+
const areAllDescendantsEmpty = Array.from(element.querySelectorAll('*')).every(
|
975
|
+
child => getTextContent(child).trim().length === 0 && !hasAccessibleLabel(child),
|
976
|
+
);
|
977
|
+
if (element.nodeName.toLowerCase() === 'div' && areAllDescendantsEmpty) {
|
978
|
+
customConsoleWarn('Div with empty descendants, skipping flagging.');
|
979
|
+
return false;
|
980
|
+
}
|
877
981
|
|
878
|
-
|
982
|
+
if (hasCSSContent(element)) {
|
983
|
+
customConsoleWarn('Element has CSS ::before or ::after content, skipping flagging.');
|
984
|
+
return false;
|
985
|
+
}
|
986
|
+
|
987
|
+
return false; // Default case: do not flag
|
988
|
+
}
|
989
|
+
|
990
|
+
function flagElements() {
|
991
|
+
const currentFlaggedElementsByDocument: Record<string, HTMLElement[]> = {}; // Temporary object to hold current flagged elements
|
879
992
|
|
880
|
-
|
993
|
+
/*
|
881
994
|
Collects all the elements and places then into an array
|
882
995
|
Then places the array in the correct frame
|
883
996
|
*/
|
884
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
|
997
|
+
// Process main document
|
998
|
+
const currentFlaggedElements: HTMLElement[] = [];
|
999
|
+
const allElements = Array.from(document.querySelectorAll<HTMLElement>('*'));
|
1000
|
+
let indexofAllElements: number = 0;
|
1001
|
+
|
1002
|
+
while (indexofAllElements < allElements.length) {
|
1003
|
+
const element = allElements[indexofAllElements] as HTMLElement;
|
1004
|
+
// if it selects a frameset
|
1005
|
+
if (
|
1006
|
+
shouldFlagElement(element, allowNonClickableFlagging) ||
|
1007
|
+
element.dataset.flagged === 'true'
|
1008
|
+
) {
|
1009
|
+
element.dataset.flagged = 'true'; // Mark element as flagged
|
1010
|
+
currentFlaggedElements.push(element);
|
1011
|
+
}
|
899
1012
|
|
900
|
-
|
901
|
-
|
902
|
-
|
903
|
-
|
904
|
-
|
905
|
-
}
|
906
|
-
indexofAllElements++;
|
1013
|
+
// If the element has a shadowRoot, add its children
|
1014
|
+
if (element.shadowRoot) {
|
1015
|
+
allElements.push(
|
1016
|
+
...(Array.from(element.shadowRoot.querySelectorAll('*')) as HTMLElement[]),
|
1017
|
+
);
|
907
1018
|
}
|
908
|
-
|
909
|
-
|
910
|
-
|
911
|
-
const iframes = document.querySelectorAll('iframe');
|
912
|
-
iframes.forEach((iframe, index) => {
|
913
|
-
injectStylesIntoFrame(iframe);
|
914
|
-
try {
|
915
|
-
const iframeDocument = iframe.contentDocument || iframe.contentWindow.document;
|
916
|
-
if (iframeDocument) {
|
917
|
-
const iframeFlaggedElements: HTMLElement[] = [];
|
918
|
-
const iframeElements = Array.from(iframeDocument.querySelectorAll<HTMLElement>('*'));
|
919
|
-
let indexOfIframeElements: number = 0;
|
920
|
-
while (indexOfIframeElements < iframeElements.length) {
|
921
|
-
const element = iframeElements[indexOfIframeElements] as HTMLElement;
|
922
|
-
if (
|
923
|
-
shouldFlagElement(element, allowNonClickableFlagging) ||
|
924
|
-
element.dataset.flagged === 'true'
|
925
|
-
) {
|
926
|
-
element.dataset.flagged = 'true'; // Mark element as flagged
|
927
|
-
iframeFlaggedElements.push(element);
|
928
|
-
}
|
929
|
-
// If the element has a shadowRoot, add its children
|
930
|
-
if (element.shadowRoot) {
|
931
|
-
iframeElements.push(
|
932
|
-
...(Array.from(element.shadowRoot.querySelectorAll('*')) as HTMLElement[]),
|
933
|
-
);
|
934
|
-
}
|
935
|
-
indexOfIframeElements++;
|
936
|
-
}
|
937
|
-
const iframeXPath = getXPath(iframe);
|
938
|
-
currentFlaggedElementsByDocument[iframeXPath] = iframeFlaggedElements;
|
939
|
-
}
|
940
|
-
} catch (error) {
|
941
|
-
console.warn(`Cannot access iframe document (${index}): ${error.message}`);
|
942
|
-
}
|
943
|
-
});
|
1019
|
+
indexofAllElements++;
|
1020
|
+
}
|
1021
|
+
currentFlaggedElementsByDocument[''] = currentFlaggedElements; // Key "" represents the main document
|
944
1022
|
|
945
|
-
|
946
|
-
|
947
|
-
|
948
|
-
|
949
|
-
|
950
|
-
|
951
|
-
|
952
|
-
|
953
|
-
|
954
|
-
|
955
|
-
|
956
|
-
|
957
|
-
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
|
962
|
-
|
963
|
-
|
964
|
-
|
965
|
-
|
966
|
-
|
967
|
-
|
968
|
-
|
969
|
-
}
|
970
|
-
indexOfIframeElements++;
|
1023
|
+
// Process iframes
|
1024
|
+
const iframes = document.querySelectorAll('iframe');
|
1025
|
+
iframes.forEach((iframe, index) => {
|
1026
|
+
injectStylesIntoFrame(iframe);
|
1027
|
+
try {
|
1028
|
+
const iframeDocument = iframe.contentDocument || iframe.contentWindow.document;
|
1029
|
+
if (iframeDocument) {
|
1030
|
+
const iframeFlaggedElements: HTMLElement[] = [];
|
1031
|
+
const iframeElements = Array.from(iframeDocument.querySelectorAll<HTMLElement>('*'));
|
1032
|
+
let indexOfIframeElements: number = 0;
|
1033
|
+
while (indexOfIframeElements < iframeElements.length) {
|
1034
|
+
const element = iframeElements[indexOfIframeElements] as HTMLElement;
|
1035
|
+
if (
|
1036
|
+
shouldFlagElement(element, allowNonClickableFlagging) ||
|
1037
|
+
element.dataset.flagged === 'true'
|
1038
|
+
) {
|
1039
|
+
element.dataset.flagged = 'true'; // Mark element as flagged
|
1040
|
+
iframeFlaggedElements.push(element);
|
1041
|
+
}
|
1042
|
+
// If the element has a shadowRoot, add its children
|
1043
|
+
if (element.shadowRoot) {
|
1044
|
+
iframeElements.push(
|
1045
|
+
...(Array.from(element.shadowRoot.querySelectorAll('*')) as HTMLElement[]),
|
1046
|
+
);
|
971
1047
|
}
|
972
|
-
|
973
|
-
currentFlaggedElementsByDocument[iframeXPath] = iframeFlaggedElements;
|
1048
|
+
indexOfIframeElements++;
|
974
1049
|
}
|
975
|
-
|
976
|
-
|
1050
|
+
const iframeXPath = getXPath(iframe);
|
1051
|
+
currentFlaggedElementsByDocument[iframeXPath] = iframeFlaggedElements;
|
977
1052
|
}
|
978
|
-
})
|
1053
|
+
} catch (error) {
|
1054
|
+
console.warn(`Cannot access iframe document (${index}): ${error.message}`);
|
1055
|
+
}
|
1056
|
+
});
|
979
1057
|
|
980
|
-
|
981
|
-
|
982
|
-
|
983
|
-
|
984
|
-
|
985
|
-
const
|
986
|
-
|
987
|
-
const
|
988
|
-
|
989
|
-
|
990
|
-
|
991
|
-
|
992
|
-
|
1058
|
+
// Process frames
|
1059
|
+
const frames = document.querySelectorAll('frame');
|
1060
|
+
frames.forEach((frame, index) => {
|
1061
|
+
// injectStylesIntoFrame(frame);
|
1062
|
+
try {
|
1063
|
+
const iframeDocument = frame.contentDocument || frame.contentWindow.document;
|
1064
|
+
if (iframeDocument) {
|
1065
|
+
const iframeFlaggedElements: HTMLElement[] = [];
|
1066
|
+
const iframeElements = Array.from(iframeDocument.querySelectorAll<HTMLElement>('*'));
|
1067
|
+
let indexOfIframeElements: number = 0;
|
1068
|
+
while (indexOfIframeElements < iframeElements.length) {
|
1069
|
+
const element = iframeElements[indexOfIframeElements] as HTMLElement;
|
1070
|
+
if (
|
1071
|
+
shouldFlagElement(element, allowNonClickableFlagging) ||
|
1072
|
+
element.dataset.flagged === 'true'
|
1073
|
+
) {
|
1074
|
+
element.dataset.flagged = 'true'; // Mark element as flagged
|
1075
|
+
iframeFlaggedElements.push(element);
|
993
1076
|
}
|
994
|
-
|
995
|
-
|
996
|
-
|
997
|
-
|
998
|
-
// Check if the xpath already exists in previousAllFlaggedElementsXPaths
|
999
|
-
const alreadyExists = previousAllFlaggedElementsXPaths.some(
|
1000
|
-
entry => entry.xpath === xpath,
|
1077
|
+
// If the element has a shadowRoot, add its children
|
1078
|
+
if (element.shadowRoot) {
|
1079
|
+
iframeElements.push(
|
1080
|
+
...(Array.from(element.shadowRoot.querySelectorAll('*')) as HTMLElement[]),
|
1001
1081
|
);
|
1002
|
-
if (!alreadyExists) {
|
1003
|
-
// Add to previousAllFlaggedElementsXPaths only if not already present
|
1004
|
-
previousAllFlaggedElementsXPaths.push({ xpath, code: outerHTML });
|
1005
|
-
}
|
1006
1082
|
}
|
1083
|
+
indexOfIframeElements++;
|
1007
1084
|
}
|
1008
|
-
|
1009
|
-
|
1085
|
+
const iframeXPath = getXPath(frame);
|
1086
|
+
currentFlaggedElementsByDocument[iframeXPath] = iframeFlaggedElements;
|
1087
|
+
}
|
1088
|
+
} catch (error) {
|
1089
|
+
console.warn(`Cannot access iframe document (${index}): ${error.message}`);
|
1010
1090
|
}
|
1091
|
+
});
|
1092
|
+
|
1093
|
+
// Collect XPaths and outerHTMLs of flagged elements per document
|
1094
|
+
const flaggedXPathsByDocument = {};
|
1095
|
+
|
1096
|
+
for (const docKey in currentFlaggedElementsByDocument) {
|
1097
|
+
const elements = currentFlaggedElementsByDocument[docKey];
|
1098
|
+
const flaggedInfo = []; // Array to hold flagged element info
|
1099
|
+
elements.forEach(flaggedElement => {
|
1100
|
+
const parentFlagged = flaggedElement.closest('[data-flagged="true"]');
|
1101
|
+
if (!parentFlagged || parentFlagged === flaggedElement) {
|
1102
|
+
let xpath = getXPath(flaggedElement);
|
1103
|
+
if (docKey !== '') {
|
1104
|
+
// For elements in iframes, adjust XPath
|
1105
|
+
xpath = docKey + xpath;
|
1106
|
+
}
|
1107
|
+
if (xpath && flaggedElement !== null && flaggedElement.outerHTML) {
|
1108
|
+
const { outerHTML } = flaggedElement; // Get outerHTML
|
1109
|
+
flaggedInfo.push({ xpath, code: outerHTML }); // Store xpath and outerHTML
|
1110
|
+
|
1111
|
+
// Check if the xpath already exists in previousAllFlaggedElementsXPaths
|
1112
|
+
const alreadyExists = previousAllFlaggedElementsXPaths.some(
|
1113
|
+
entry => entry.xpath === xpath,
|
1114
|
+
);
|
1115
|
+
if (!alreadyExists) {
|
1116
|
+
// Add to previousAllFlaggedElementsXPaths only if not already present
|
1117
|
+
previousAllFlaggedElementsXPaths.push({ xpath, code: outerHTML });
|
1118
|
+
}
|
1119
|
+
}
|
1120
|
+
}
|
1121
|
+
});
|
1122
|
+
flaggedXPathsByDocument[docKey] = flaggedInfo; // Store all flagged element info
|
1123
|
+
}
|
1011
1124
|
|
1012
|
-
|
1013
|
-
|
1125
|
+
// Update previousFlaggedXPathsByDocument before finishing
|
1126
|
+
previousFlaggedXPathsByDocument = { ...flaggedXPathsByDocument };
|
1014
1127
|
|
1015
|
-
|
1016
|
-
|
1017
|
-
|
1128
|
+
cleanupFlaggedElements();
|
1129
|
+
return previousAllFlaggedElementsXPaths;
|
1130
|
+
}
|
1018
1131
|
|
1019
|
-
|
1020
|
-
|
1021
|
-
|
1022
|
-
|
1023
|
-
|
1024
|
-
|
1025
|
-
|
1026
|
-
function debounce(func, wait) {
|
1027
|
-
let timeout;
|
1028
|
-
return function (...args) {
|
1029
|
-
clearTimeout(timeout);
|
1030
|
-
timeout = setTimeout(() => func.apply(this, args), wait);
|
1031
|
-
};
|
1032
|
-
}
|
1132
|
+
// Clean up [data-flagged="true"] attribute added by this script
|
1133
|
+
function cleanupFlaggedElements() {
|
1134
|
+
const flaggedElements = document.querySelectorAll('[data-flagged="true"]');
|
1135
|
+
flaggedElements.forEach(flaggedElement => {
|
1136
|
+
flaggedElement.removeAttribute('data-flagged');
|
1137
|
+
});
|
1138
|
+
}
|
1033
1139
|
|
1034
|
-
|
1035
|
-
});
|
1140
|
+
return flagElements();
|
1036
1141
|
};
|