@govtechsg/oobee 0.10.39 → 0.10.42
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/.github/workflows/docker-test.yml +1 -1
- package/README.md +2 -0
- package/REPORTS.md +362 -0
- package/package.json +1 -1
- package/src/crawlers/commonCrawlerFunc.ts +29 -1
- package/src/crawlers/crawlDomain.ts +4 -21
- package/src/crawlers/crawlSitemap.ts +1 -1
- package/src/crawlers/custom/flagUnlabelledClickableElements.ts +589 -554
- package/src/crawlers/pdfScanFunc.ts +67 -26
- package/src/mergeAxeResults.ts +302 -237
- package/src/screenshotFunc/htmlScreenshotFunc.ts +1 -1
- package/src/screenshotFunc/pdfScreenshotFunc.ts +34 -1
- package/src/utils.ts +289 -13
@@ -3,15 +3,26 @@ export async function flagUnlabelledClickableElements() {
|
|
3
3
|
// There's some code that is not needed when running this on backend but
|
4
4
|
// we avoid changing the script for now to make it easy to update
|
5
5
|
const allowNonClickableFlagging = true; // Change this to true to flag non-clickable images
|
6
|
-
const landmarkElements = [
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
6
|
+
const landmarkElements = ['header', 'footer', 'nav', 'main', 'article', 'section', 'aside', 'form'];
|
7
|
+
const validAriaRoles = [
|
8
|
+
// Landmark Roles
|
9
|
+
"banner", "complementary", "contentinfo", "form", "main",
|
10
|
+
"navigation", "region", "search",
|
11
|
+
|
12
|
+
// Document Structure Roles
|
13
|
+
"article", "heading", "list", "listitem", "table", "row",
|
14
|
+
"cell", "grid", "gridcell", "separator",
|
15
|
+
|
16
|
+
// Widget Roles
|
17
|
+
"button", "checkbox", "combobox", "dialog", "grid", "link",
|
18
|
+
"menu", "menuitem", "progressbar", "radio", "slider",
|
19
|
+
"spinbutton", "switch", "tab", "tabpanel", "textbox", "tooltip",
|
20
|
+
|
21
|
+
// Live Region Roles
|
22
|
+
"alert", "log", "marquee", "status", "timer",
|
23
|
+
|
24
|
+
// Custom Roles
|
25
|
+
"application", "presentation", "none"
|
15
26
|
];
|
16
27
|
const loggingEnabled = false; // Set to true to enable console warnings
|
17
28
|
|
@@ -48,29 +59,29 @@ export async function flagUnlabelledClickableElements() {
|
|
48
59
|
}
|
49
60
|
}
|
50
61
|
|
51
|
-
|
52
|
-
|
62
|
+
function hasPointerCursor(node: Node): boolean {
|
63
|
+
if (node.nodeType !== Node.ELEMENT_NODE) {
|
64
|
+
// Check if it's a parent and can be converted to an element
|
65
|
+
node = (node as HTMLElement).parentElement;
|
66
|
+
if (!node || node.nodeType !== Node.ELEMENT_NODE) {
|
67
|
+
return false; // Still not a valid element
|
68
|
+
}
|
69
|
+
}
|
70
|
+
|
71
|
+
const computedStyle = window.getComputedStyle(node as HTMLElement);
|
53
72
|
const hasPointerStyle = computedStyle.cursor === 'pointer';
|
54
|
-
const hasOnClick =
|
55
|
-
const hasEventListeners = Object.keys(
|
56
|
-
|
57
|
-
// Check if the
|
58
|
-
const isClickableRole = ['button', 'link', 'menuitem'].includes(
|
59
|
-
const isNativeClickableElement =
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
hasPointerStyle ||
|
67
|
-
hasOnClick ||
|
68
|
-
hasEventListeners ||
|
69
|
-
isClickableRole ||
|
70
|
-
isNativeClickableElement ||
|
71
|
-
hasTabIndex
|
72
|
-
);
|
73
|
-
}
|
73
|
+
const hasOnClick = (node as HTMLElement).hasAttribute('onclick');
|
74
|
+
const hasEventListeners = Object.keys(node).some(prop => prop.startsWith('on'));
|
75
|
+
|
76
|
+
// Check if the node is inherently interactive
|
77
|
+
const isClickableRole = ['button', 'link', 'menuitem'].includes((node as HTMLElement).getAttribute('role') || '');
|
78
|
+
const isNativeClickableElement = ['a', 'button', 'input'].includes((node as HTMLElement).nodeName.toLowerCase()) &&
|
79
|
+
(node.nodeName.toLowerCase() !== 'a' || (node as HTMLAnchorElement).hasAttribute('href'));
|
80
|
+
const hasTabIndex = (node as HTMLElement).hasAttribute('tabindex') && (node as HTMLElement).getAttribute('tabindex') !== '-1';
|
81
|
+
|
82
|
+
return hasPointerStyle || hasOnClick || hasEventListeners || isClickableRole || isNativeClickableElement || hasTabIndex;
|
83
|
+
}
|
84
|
+
|
74
85
|
|
75
86
|
function isAccessibleText(value: string) {
|
76
87
|
if (!value || value.trim().length === 0) {
|
@@ -79,20 +90,27 @@ export async function flagUnlabelledClickableElements() {
|
|
79
90
|
|
80
91
|
const trimmedValue = value.trim();
|
81
92
|
|
82
|
-
// Check if the text
|
93
|
+
// Check if the text is a URL/link or a CSS url() pattern.
|
94
|
+
const linkRegex = /^(https?:\/\/|file:\/\/|[a-zA-Z]:[\\/]|\/)[^\s]+$/i;
|
95
|
+
const cssUrlRegex = /^url\(.*\)$/i;
|
96
|
+
if (linkRegex.test(trimmedValue) || cssUrlRegex.test(trimmedValue)) {
|
97
|
+
return false;
|
98
|
+
}
|
99
|
+
|
100
|
+
// Check if the text contains any private use characters.
|
83
101
|
const privateUseRegex = /\p{Private_Use}/u;
|
84
102
|
if (privateUseRegex.test(trimmedValue)) {
|
85
|
-
|
103
|
+
return false;
|
86
104
|
}
|
87
105
|
|
88
|
-
// Check if the text
|
89
|
-
|
90
|
-
|
91
|
-
return true;
|
106
|
+
// Check if the text is valid Unicode (assuming isValidUnicode is defined elsewhere).
|
107
|
+
if (!isValidUnicode(trimmedValue)) {
|
108
|
+
return false;
|
92
109
|
}
|
93
110
|
|
94
|
-
//
|
95
|
-
|
111
|
+
// Check if the text contains at least one letter or number.
|
112
|
+
const accessibleTextRegex = /[\p{L}\p{N}]/u;
|
113
|
+
return accessibleTextRegex.test(trimmedValue);
|
96
114
|
}
|
97
115
|
|
98
116
|
function isInOpenDetails(element: Element) {
|
@@ -122,7 +140,7 @@ export async function flagUnlabelledClickableElements() {
|
|
122
140
|
isInOpenDetails(el)
|
123
141
|
);
|
124
142
|
} catch (error) {
|
125
|
-
|
143
|
+
customConsoleWarn('Error in ELEMENT', error.message);
|
126
144
|
return false;
|
127
145
|
}
|
128
146
|
}
|
@@ -139,6 +157,44 @@ export async function flagUnlabelledClickableElements() {
|
|
139
157
|
return validTextOrEmojiRegex.test(text);
|
140
158
|
}
|
141
159
|
|
160
|
+
function isTitleValid(element:Element) {
|
161
|
+
// Get text content of the element (including children)
|
162
|
+
const titleText = getTextContent(element);
|
163
|
+
|
164
|
+
// Check if the element itself has valid text or content
|
165
|
+
if (titleText && isValidUnicode(titleText)) {
|
166
|
+
return true;
|
167
|
+
}
|
168
|
+
|
169
|
+
// Check if the element has any children that are non-Unicode elements
|
170
|
+
const nonUnicodeSelector = ['img', 'a', 'svg', 'button', 'video', 'audio', 'canvas'].join(', ');
|
171
|
+
const nonUnicodeChild = element.querySelector(nonUnicodeSelector);
|
172
|
+
if (nonUnicodeChild) {
|
173
|
+
return true;
|
174
|
+
}
|
175
|
+
|
176
|
+
// Check for any leaf nodes (text nodes) that are valid Unicode
|
177
|
+
const leafNodes = element.querySelectorAll('*');
|
178
|
+
for (const child of leafNodes) {
|
179
|
+
const childText = getTextContent(child);
|
180
|
+
if (childText && isValidUnicode(childText)) {
|
181
|
+
return true;
|
182
|
+
}
|
183
|
+
|
184
|
+
// Check if the child contains any text nodes directly (e.g., <a>testing</a>)
|
185
|
+
for (const node of child.childNodes) {
|
186
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
187
|
+
const textContent = node.nodeValue.trim();
|
188
|
+
if (textContent && isValidUnicode(textContent)) {
|
189
|
+
return true;
|
190
|
+
}
|
191
|
+
}
|
192
|
+
}
|
193
|
+
}
|
194
|
+
|
195
|
+
return false; // Return false if no valid content is found
|
196
|
+
}
|
197
|
+
|
142
198
|
function getElementById(element: Element, id: string) {
|
143
199
|
return element.ownerDocument.getElementById(id);
|
144
200
|
}
|
@@ -154,18 +210,18 @@ export async function flagUnlabelledClickableElements() {
|
|
154
210
|
}
|
155
211
|
return '';
|
156
212
|
}
|
157
|
-
|
213
|
+
|
158
214
|
function hasAccessibleLabel(element: Element) {
|
159
215
|
const ariaLabel = element.getAttribute('aria-label');
|
160
216
|
const ariaLabelledByText = getAriaLabelledByText(element);
|
161
217
|
const altText = element.getAttribute('alt');
|
162
218
|
const title = element.getAttribute('title');
|
163
219
|
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
220
|
+
|
221
|
+
return (isAccessibleText(ariaLabel) ||
|
222
|
+
(isAccessibleText(ariaLabelledByText)) ||
|
223
|
+
(isAccessibleText(altText)) ||
|
224
|
+
(title && isTitleValid(element))
|
169
225
|
);
|
170
226
|
}
|
171
227
|
|
@@ -199,17 +255,15 @@ export async function flagUnlabelledClickableElements() {
|
|
199
255
|
// Check previous and next siblings
|
200
256
|
const previousSibling = element.previousElementSibling;
|
201
257
|
const nextSibling = element.nextElementSibling;
|
202
|
-
if (
|
203
|
-
|
204
|
-
|
205
|
-
) {
|
206
|
-
return true;
|
258
|
+
if ((previousSibling && hasAccessibleLabel(previousSibling)) ||
|
259
|
+
(nextSibling && hasAccessibleLabel(nextSibling))) {
|
260
|
+
return true;
|
207
261
|
}
|
208
262
|
|
209
263
|
// Check the parent element
|
210
264
|
const parent = element.parentElement;
|
211
265
|
if (parent && hasAccessibleLabel(parent)) {
|
212
|
-
|
266
|
+
return true;
|
213
267
|
}
|
214
268
|
|
215
269
|
return false;
|
@@ -218,22 +272,24 @@ export async function flagUnlabelledClickableElements() {
|
|
218
272
|
function hasChildWithAccessibleText(element: Element) {
|
219
273
|
// Check element children
|
220
274
|
const hasAccessibleChildElement = Array.from(element.children).some(child => {
|
221
|
-
if (child.nodeName.toLowerCase() ===
|
222
|
-
|
275
|
+
if (child.nodeName.toLowerCase() === "style" || child.nodeName.toLowerCase() === "script" || !isVisibleFocusAble(child))
|
276
|
+
{
|
277
|
+
return false
|
223
278
|
}
|
224
279
|
// Skip children that are aria-hidden
|
225
280
|
if (child.getAttribute('aria-hidden') === 'true') {
|
226
|
-
|
281
|
+
return true;
|
227
282
|
}
|
228
|
-
return (
|
229
|
-
isAccessibleText(child.textContent) || hasAccessibleLabel(child) || hasCSSContent(child)
|
230
|
-
);
|
283
|
+
return (isAccessibleText(getTextContent(child))) || hasAccessibleLabel(child) || hasCSSContent(child);
|
231
284
|
});
|
232
285
|
|
233
|
-
// Check direct text nodes
|
286
|
+
// Check direct text nodes inside the element itself (like <a>"text"</a>)
|
234
287
|
const hasDirectAccessibleText = Array.from(element.childNodes).some(node => {
|
235
288
|
if (node.nodeType === Node.TEXT_NODE) {
|
236
|
-
|
289
|
+
const parentElement = node.parentElement; // Get the parent element of the text node
|
290
|
+
return parentElement
|
291
|
+
? isAccessibleText(getTextContent(node)) && isVisibleFocusAble(parentElement)
|
292
|
+
: false;
|
237
293
|
}
|
238
294
|
return false;
|
239
295
|
});
|
@@ -273,95 +329,92 @@ export async function flagUnlabelledClickableElements() {
|
|
273
329
|
}
|
274
330
|
|
275
331
|
function hasChildNotANewInteractWithAccessibleText(element: Element) {
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
child.nodeName.toLowerCase() === 'a' ||
|
282
|
-
child.nodeName.toLowerCase() === 'button' ||
|
283
|
-
child.getAttribute('role') === 'link' ||
|
284
|
-
child.getAttribute('role') === 'button'
|
285
|
-
);
|
286
|
-
}
|
287
|
-
return false;
|
332
|
+
|
333
|
+
// Helper function to check if the element is a link or button
|
334
|
+
const isBuildInInteractable = (child) => {
|
335
|
+
return child.nodeName.toLowerCase() === "a" || child.nodeName.toLowerCase() === "button" || child.nodeName.toLowerCase() === "input" ||
|
336
|
+
child.getAttribute('role') === 'link' || child.getAttribute('role') === 'button';
|
288
337
|
};
|
289
338
|
|
290
339
|
// Check element children
|
291
340
|
const hasAccessibleChildElement = Array.from(element.children).some(child => {
|
292
|
-
if (child instanceof Element) {
|
293
|
-
// Ensure child is an Element
|
294
341
|
if (!hasPointerCursor(child)) {
|
295
|
-
|
342
|
+
return false;
|
296
343
|
}
|
297
344
|
|
298
|
-
if (child.nodeName.toLowerCase() ===
|
299
|
-
|
345
|
+
if (child.nodeName.toLowerCase() === "style" || child.nodeName.toLowerCase() === "script") {
|
346
|
+
return false;
|
300
347
|
}
|
301
|
-
|
302
348
|
// Skip children that are aria-hidden or links/buttons
|
303
|
-
if (
|
304
|
-
|
305
|
-
isLinkOrButton(child) ||
|
306
|
-
!isVisibleFocusAble(child)
|
307
|
-
) {
|
308
|
-
return false;
|
349
|
+
if (child.getAttribute('aria-hidden') === 'true' || isBuildInInteractable(child) || !isVisibleFocusAble(child) ) {
|
350
|
+
return false;
|
309
351
|
}
|
310
|
-
|
311
352
|
// Check if the child element has accessible text or label
|
312
|
-
return (
|
313
|
-
isAccessibleText(getTextContent(child)) ||
|
314
|
-
hasAccessibleLabel(child) ||
|
315
|
-
hasCSSContent(child)
|
316
|
-
);
|
317
|
-
}
|
318
|
-
return false;
|
353
|
+
return isAccessibleText(getTextContent(child)) || hasAccessibleLabel(child) || hasCSSContent(child);
|
319
354
|
});
|
320
355
|
|
321
356
|
// Check direct text nodes inside the element itself (like <a>"text"</a>)
|
322
357
|
const hasDirectAccessibleText = Array.from(element.childNodes).some(node => {
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
358
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
359
|
+
|
360
|
+
if (!(hasPointerCursor(node) || (node.nodeType === Node.TEXT_NODE && hasPointerCursor(node.parentElement) && isAccessibleText(getTextContent(node))))) {
|
361
|
+
return false;
|
362
|
+
}
|
363
|
+
|
364
|
+
const textContent = getTextContent(node);
|
365
|
+
|
366
|
+
// Check if the text contains non-ASCII characters (Unicode)
|
367
|
+
const containsUnicode = /[^\x00-\x7F]/.test(textContent);
|
368
|
+
|
369
|
+
// If contains non-ASCII characters, validate with isValidUnicode
|
370
|
+
if (containsUnicode) {
|
371
|
+
return isValidUnicode(textContent);
|
372
|
+
}
|
328
373
|
|
329
|
-
|
330
|
-
|
331
|
-
return isValidUnicode(textContent);
|
374
|
+
// Otherwise, just check if it's non-empty text
|
375
|
+
return textContent.length > 0;
|
332
376
|
}
|
333
377
|
|
334
|
-
//
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
return false;
|
346
|
-
});
|
347
|
-
}
|
348
|
-
|
349
|
-
return false;
|
378
|
+
// Recursively check for text content inside child nodes of elements that are not links or buttons
|
379
|
+
if (node.nodeType === Node.ELEMENT_NODE && !isBuildInInteractable(node)) {
|
380
|
+
return Array.from(node.childNodes).some(innerNode => {
|
381
|
+
if (innerNode.nodeType === Node.TEXT_NODE) {
|
382
|
+
const innerTextContent = getTextContent(innerNode).trim();
|
383
|
+
return innerTextContent && !isValidUnicode(innerTextContent); // Check for non-Unicode content
|
384
|
+
}
|
385
|
+
return false;
|
386
|
+
});
|
387
|
+
}
|
388
|
+
return false;
|
350
389
|
});
|
351
390
|
|
352
391
|
return hasAccessibleChildElement || hasDirectAccessibleText;
|
353
392
|
}
|
354
393
|
|
355
|
-
|
356
|
-
|
357
|
-
|
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 */
|
394
|
+
function hasChildWhichIsVisibleFocusable(element:Element) {
|
395
|
+
if (!element || !element.children) {
|
396
|
+
return false; // If no element or no children, return false
|
362
397
|
}
|
363
|
-
|
364
|
-
|
398
|
+
|
399
|
+
for (let child of element.children) {
|
400
|
+
// Check if the child is visible and focusable
|
401
|
+
if (isVisibleFocusAble(child)) {
|
402
|
+
return true; // Found a visible and focusable child, return true
|
403
|
+
}
|
404
|
+
|
405
|
+
// Recursively check its children
|
406
|
+
if (hasChildWhichIsVisibleFocusable(child)) {
|
407
|
+
return true; // If any descendant is visible and focusable, return true
|
408
|
+
}
|
409
|
+
}
|
410
|
+
|
411
|
+
return false; // No visible and focusable child found
|
412
|
+
}
|
413
|
+
|
414
|
+
function hasDisplayContentsWithChildren(element: Element) {
|
415
|
+
const style = window.getComputedStyle(element);
|
416
|
+
return style.display === "contents" && element.children.length > 0;
|
417
|
+
}
|
365
418
|
|
366
419
|
function injectStylesIntoFrame(frame: HTMLIFrameElement) {
|
367
420
|
try {
|
@@ -387,15 +440,15 @@ export async function flagUnlabelledClickableElements() {
|
|
387
440
|
const beforeContent = window.getComputedStyle(element, '::before').getPropertyValue('content');
|
388
441
|
const afterContent = window.getComputedStyle(element, '::after').getPropertyValue('content');
|
389
442
|
|
390
|
-
function isAccessibleContent(value
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
443
|
+
function isAccessibleContent(value) {
|
444
|
+
if (!value || value === 'none' || value === 'normal') {
|
445
|
+
return false;
|
446
|
+
}
|
447
|
+
// Remove quotes from the content value
|
448
|
+
const unquotedValue = value.replace(/^['"]|['"]$/g, '').trim();
|
396
449
|
|
397
|
-
|
398
|
-
|
450
|
+
// Use the isAccessibleText function
|
451
|
+
return isAccessibleText(unquotedValue);
|
399
452
|
}
|
400
453
|
|
401
454
|
return isAccessibleContent(beforeContent) || isAccessibleContent(afterContent);
|
@@ -404,586 +457,568 @@ export async function flagUnlabelledClickableElements() {
|
|
404
457
|
function isElementTooSmall(element: Element) {
|
405
458
|
// Get the bounding rectangle of the element
|
406
459
|
const rect = element.getBoundingClientRect();
|
460
|
+
|
461
|
+
// Check if the element has a valid width or height
|
462
|
+
if (rect.width > 0 || rect.height > 0) {
|
463
|
+
return false; // Element is not too small
|
464
|
+
}
|
407
465
|
|
408
|
-
//
|
409
|
-
|
466
|
+
// If the element itself is too small, check the ::after pseudo-element
|
467
|
+
const afterStyles = window.getComputedStyle(element, '::after');
|
468
|
+
const afterWidth = parseFloat(afterStyles.width);
|
469
|
+
const afterHeight = parseFloat(afterStyles.height);
|
470
|
+
|
471
|
+
// If ::after has valid width or height, return false
|
472
|
+
if ((afterWidth > 0 || afterHeight > 0) || afterStyles.content.trim() === "") {
|
473
|
+
return false;
|
474
|
+
}
|
475
|
+
|
476
|
+
// Check the ::before pseudo-element
|
477
|
+
const beforeStyles = window.getComputedStyle(element, '::before');
|
478
|
+
const beforeWidth = parseFloat(beforeStyles.width);
|
479
|
+
const beforeHeight = parseFloat(beforeStyles.height);
|
480
|
+
|
481
|
+
// If ::before has valid width or height, return false
|
482
|
+
if ((beforeWidth > 0 || beforeHeight > 0) || beforeStyles.content.trim() === "") {
|
483
|
+
return false;
|
484
|
+
}
|
485
|
+
|
486
|
+
// If both the element, ::after, and ::before are too small, return true
|
487
|
+
return true;
|
410
488
|
}
|
411
489
|
|
412
490
|
function getTextContent(element: Element | ChildNode): string {
|
413
491
|
if (element.nodeType === Node.TEXT_NODE) {
|
414
|
-
return element.nodeValue
|
492
|
+
return element.nodeValue.trim(); // Return the text directly if it's a TEXT_NODE
|
415
493
|
}
|
416
494
|
|
417
495
|
let textContent = '';
|
418
496
|
|
419
|
-
for (
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
textContent += childText; // Append valid child text
|
439
|
-
}
|
497
|
+
for (let node of element.childNodes) {
|
498
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
499
|
+
textContent += node.nodeValue.trim(); // Append text content from text nodes
|
500
|
+
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
501
|
+
// If it's an SVG and has a <title> tag inside it, we want to grab that text
|
502
|
+
const elementNode = node as Element; // Assert that the node is an Element
|
503
|
+
if (elementNode.tagName.toLowerCase() === 'svg') {
|
504
|
+
const titleElement = elementNode.querySelector('title');
|
505
|
+
if (titleElement && isVisibleFocusAble(elementNode)) {
|
506
|
+
return titleElement.textContent.trim(); // Return the title text if valid
|
507
|
+
}
|
508
|
+
}
|
509
|
+
// Recursively check child elements if it's an element node
|
510
|
+
if (isVisibleFocusAble(elementNode) || hasDisplayContentsWithChildren(elementNode) ) {
|
511
|
+
const childText = getTextContent(elementNode);
|
512
|
+
if (childText) {
|
513
|
+
textContent += childText; // Append valid child text
|
514
|
+
}
|
515
|
+
}
|
440
516
|
}
|
441
|
-
}
|
442
517
|
}
|
443
518
|
|
444
519
|
return textContent.trim(); // Return the combined text content
|
445
520
|
}
|
446
521
|
|
522
|
+
const style = document.createElement('style');
|
523
|
+
style.innerHTML = `
|
524
|
+
.highlight-flagged {
|
525
|
+
outline: 4px solid rgba(128, 0, 128, 1) !important; /* Thicker primary outline with purple in rgba format */
|
526
|
+
box-shadow:
|
527
|
+
0 0 25px 15px rgba(255, 255, 255, 1), /* White glow for contrast */
|
528
|
+
0 0 15px 10px rgba(144, 33, 166, 1) !important; /* Consistent purple glow in rgba format */
|
529
|
+
}
|
530
|
+
`;
|
531
|
+
document.head.appendChild(style);
|
532
|
+
|
447
533
|
function shouldFlagElement(element: HTMLElement, allowNonClickableFlagging: boolean) {
|
448
|
-
if (isElementTooSmall(element))
|
449
|
-
|
534
|
+
if (isElementTooSmall(element))
|
535
|
+
{
|
536
|
+
customConsoleWarn("TOO SMALL");
|
537
|
+
return false;
|
450
538
|
}
|
451
539
|
|
452
540
|
// Skip non-clickable elements if allowNonClickableFlagging is false
|
453
541
|
if (allowNonClickableFlagging && !hasPointerCursor(element)) {
|
454
|
-
|
455
|
-
|
456
|
-
);
|
457
|
-
return false;
|
542
|
+
customConsoleWarn("Element is not clickable and allowNonClickableFlagging is false, skipping flagging.");
|
543
|
+
return false;
|
458
544
|
}
|
459
|
-
|
545
|
+
|
460
546
|
// Do not flag elements if any ancestor has aria-hidden="true"
|
461
547
|
if (element.closest('[aria-hidden="true"]')) {
|
462
|
-
|
463
|
-
|
548
|
+
customConsoleWarn("An ancestor element has aria-hidden='true', skipping flagging.");
|
549
|
+
return false;
|
464
550
|
}
|
465
551
|
|
466
|
-
let parents = element.parentElement;
|
467
552
|
|
553
|
+
let parents = element.parentElement;
|
554
|
+
|
468
555
|
// Causing false negative of svg
|
469
556
|
if (parents) {
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
557
|
+
// Check if the parent has an accessible label
|
558
|
+
if (hasAccessibleLabel(parents) || hasChildNotANewInteractWithAccessibleText(parents)) {
|
559
|
+
customConsoleWarn("Parent element has an accessible label, skipping flagging of this element.");
|
560
|
+
return false;
|
561
|
+
}
|
562
|
+
|
563
|
+
}
|
564
|
+
|
479
565
|
let maxLayers = 3;
|
480
566
|
let tracedBackedLayerCount = 0;
|
481
|
-
while (parents && tracedBackedLayerCount
|
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
|
-
}
|
567
|
+
while (parents && tracedBackedLayerCount < maxLayers) {
|
487
568
|
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
569
|
+
// DO NOT LOOK AT BODY
|
570
|
+
if (landmarkElements.includes(parents.nodeName.toLowerCase()))
|
571
|
+
{
|
572
|
+
customConsoleWarn("Parent went up all the way to body. Too far up hence flagging.",parents);
|
573
|
+
break;
|
574
|
+
}
|
575
|
+
|
576
|
+
// Skip flagging if the parent or the element itself has an accessible label
|
577
|
+
if (hasAccessibleLabel(parents) || hasChildNotANewInteractWithAccessibleText(parents)) {
|
578
|
+
customConsoleWarn("Parent or element has an accessible label, skipping flagging.",parents);
|
579
|
+
return false;
|
580
|
+
}
|
498
581
|
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
582
|
+
|
583
|
+
// Skip flagging if the parent is a button-like element with aria-expanded
|
584
|
+
if (
|
585
|
+
parents.getAttribute('role') === 'button' &&
|
586
|
+
(parents.hasAttribute('aria-expanded') || parents.hasAttribute('aria-controls'))
|
587
|
+
) {
|
588
|
+
customConsoleWarn("Parent element is an interactive button with aria-expanded or aria-controls, skipping flagging.");
|
589
|
+
return false;
|
590
|
+
}
|
591
|
+
|
592
|
+
// Skip flagging if an ancestor has an accessible label or an interactive role (e.g., button, link)
|
593
|
+
if (
|
594
|
+
['div', 'section', 'article', 'nav'].includes(parents.nodeName.toLowerCase()) &&
|
595
|
+
hasAccessibleLabel(parents)
|
596
|
+
) {
|
597
|
+
customConsoleWarn("Ancestor element with contextual role has an accessible label, skipping flagging.");
|
598
|
+
return false;
|
599
|
+
}
|
509
600
|
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
return false;
|
519
|
-
}
|
601
|
+
// Skip flag if parent is an a link or button that already contains accessible text
|
602
|
+
if (
|
603
|
+
(parents.nodeName.toLowerCase() === "a" || parents.nodeName.toLowerCase() === "button" ||
|
604
|
+
parents.getAttribute('role') === 'link' || parents.getAttribute('role') === 'button') && hasChildWithAccessibleText(parents)
|
605
|
+
){
|
606
|
+
customConsoleWarn("Skip flag if parent is an a link or button that already contains accessible text")
|
607
|
+
return false;
|
608
|
+
}
|
520
609
|
|
521
|
-
|
522
|
-
|
610
|
+
if (parents.children.length > 1)
|
611
|
+
{
|
612
|
+
tracedBackedLayerCount++;
|
613
|
+
}
|
614
|
+
|
615
|
+
parents = parents.parentElement;
|
523
616
|
}
|
524
617
|
|
618
|
+
|
619
|
+
|
525
620
|
// Skip elements with role="menuitem" if an accessible sibling, parent, or child is present
|
526
621
|
if (element.getAttribute('role') === 'menuitem') {
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
) {
|
532
|
-
customConsoleWarn(
|
533
|
-
'Menuitem element or its sibling/parent has an accessible label, skipping flagging.',
|
534
|
-
);
|
535
|
-
return false;
|
536
|
-
}
|
622
|
+
if (hasSiblingWithAccessibleLabel(element) || hasChildWithAccessibleText(element) || hasAccessibleLabel(element.parentElement)) {
|
623
|
+
customConsoleWarn("Menuitem element or its sibling/parent has an accessible label, skipping flagging.");
|
624
|
+
return false;
|
625
|
+
}
|
537
626
|
}
|
538
627
|
|
539
628
|
// Skip flagging child elements if the parent element has role="menuitem" and is accessible
|
540
629
|
const parentMenuItem = element.closest('[role="menuitem"]');
|
541
|
-
if (
|
542
|
-
|
543
|
-
|
544
|
-
) {
|
545
|
-
customConsoleWarn(
|
546
|
-
'Parent menuitem element has an accessible label or child with accessible text, skipping flagging of its children.',
|
547
|
-
);
|
548
|
-
return false;
|
630
|
+
if (parentMenuItem && (hasAccessibleLabel(parentMenuItem) || hasChildWithAccessibleText(parentMenuItem))) {
|
631
|
+
customConsoleWarn("Parent menuitem element has an accessible label or child with accessible text, skipping flagging of its children.");
|
632
|
+
return false;
|
549
633
|
}
|
550
634
|
|
551
635
|
// Add the new condition for empty div or span elements without any accessible text or children with accessible labels
|
552
|
-
if (
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
const hasAccessibleChild = Array.from(parent.children).some(
|
560
|
-
child => child !== element && hasAccessibleLabel(child),
|
561
|
-
);
|
636
|
+
if ((element.nodeName.toLowerCase() === 'span' || element.nodeName.toLowerCase() === 'div') &&
|
637
|
+
element.children.length === 0 && getTextContent(element).trim().length === 0) {
|
638
|
+
const parent = element.parentElement;
|
639
|
+
if (parent) {
|
640
|
+
const hasAccessibleChild = Array.from(parent.children).some(child =>
|
641
|
+
child !== element && hasAccessibleLabel(child)
|
642
|
+
);
|
562
643
|
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
return false;
|
644
|
+
if (hasAccessibleChild) {
|
645
|
+
customConsoleWarn("Parent element has an accessible child, skipping flagging of empty span or div.");
|
646
|
+
return false;
|
647
|
+
}
|
568
648
|
}
|
569
|
-
}
|
570
649
|
}
|
571
650
|
|
572
651
|
// Do not flag elements with aria-hidden="true"
|
573
652
|
if (element.getAttribute('aria-hidden') === 'true') {
|
574
|
-
|
575
|
-
|
653
|
+
customConsoleWarn("Element is aria-hidden, skipping flagging.");
|
654
|
+
return false;
|
576
655
|
}
|
577
656
|
|
578
|
-
if (
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
657
|
+
if (element.getAttribute("aria-labelledby") !== null && element.getAttribute("aria-labelledby") !== "") {
|
658
|
+
// Get the list of IDs referenced in aria-labelledby
|
659
|
+
const ids = element.getAttribute("aria-labelledby").split(' ');
|
660
|
+
let shouldNotFlag = false
|
661
|
+
|
662
|
+
// Loop through each ID and find the corresponding elements
|
663
|
+
ids.forEach(id => {
|
664
|
+
const referencedElement = document.getElementById(id);
|
665
|
+
|
666
|
+
// Check if the element was found
|
667
|
+
if (referencedElement &&
|
668
|
+
(hasAccessibleLabel(referencedElement) ||
|
669
|
+
isAccessibleText(getTextContent(referencedElement)) ||
|
670
|
+
hasAllChildrenAccessible(referencedElement) ))
|
671
|
+
{
|
672
|
+
shouldNotFlag = true;
|
673
|
+
}
|
674
|
+
});
|
589
675
|
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
(hasAccessibleLabel(referencedElement) ||
|
594
|
-
isAccessibleText(getTextContent(referencedElement)) ||
|
595
|
-
hasAllChildrenAccessible(referencedElement))
|
596
|
-
) {
|
597
|
-
shouldNotFlag = true;
|
676
|
+
if (shouldNotFlag)
|
677
|
+
{
|
678
|
+
return false
|
598
679
|
}
|
599
|
-
|
600
|
-
|
601
|
-
if (shouldNotFlag) {
|
602
|
-
return false;
|
603
|
-
}
|
680
|
+
|
604
681
|
}
|
605
682
|
|
606
683
|
// Do not flag elements with role="presentation"
|
607
684
|
if (element.getAttribute('role') === 'presentation') {
|
608
|
-
|
609
|
-
|
685
|
+
customConsoleWarn("Element has role='presentation', skipping flagging.");
|
686
|
+
return false;
|
610
687
|
}
|
611
688
|
|
612
689
|
if (element.dataset.flagged === 'true') {
|
613
|
-
|
614
|
-
|
690
|
+
customConsoleWarn("Element is already flagged.");
|
691
|
+
return false;
|
615
692
|
}
|
616
693
|
|
617
694
|
// If an ancestor element is flagged, do not flag this element
|
618
695
|
if (element.closest('[data-flagged="true"]')) {
|
619
|
-
|
620
|
-
|
696
|
+
customConsoleWarn("An ancestor element is already flagged.");
|
697
|
+
return false;
|
621
698
|
}
|
622
699
|
|
623
700
|
// Skip elements that are not visible (e.g., display:none)
|
624
701
|
const computedStyle = element.ownerDocument.defaultView.getComputedStyle(element);
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
) {
|
630
|
-
customConsoleWarn('Element is not visible, skipping flagging.');
|
631
|
-
return false;
|
702
|
+
|
703
|
+
if (computedStyle.display === 'none' || computedStyle.visibility === 'hidden' || (element.offsetParent === null && !(computedStyle.position === 'fixed'))) {
|
704
|
+
customConsoleWarn("Element is not visible, skipping flagging.");
|
705
|
+
return false;
|
632
706
|
}
|
633
707
|
|
634
708
|
// Skip empty <div> or <span> elements without any accessible text or children with accessible labels, unless they have a pointer cursor
|
635
|
-
if (
|
636
|
-
|
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
|
-
}
|
709
|
+
if ((element.nodeName.toLowerCase() === 'div' || element.nodeName.toLowerCase() === 'span') &&
|
710
|
+
element.children.length === 0 && getTextContent(element).trim().length === 0) {
|
646
711
|
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
712
|
+
if (!hasPointerCursor(element)) {
|
713
|
+
customConsoleWarn("Empty div or span without accessible text and without pointer cursor, skipping flagging.");
|
714
|
+
return false;
|
715
|
+
}
|
651
716
|
|
652
|
-
//
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
);
|
666
|
-
return false; // Do not flag
|
717
|
+
// **New background-image check**
|
718
|
+
const backgroundImage = window.getComputedStyle(element).getPropertyValue('background-image');
|
719
|
+
if (backgroundImage && backgroundImage !== 'none') {
|
720
|
+
customConsoleWarn("Element has a background image.");
|
721
|
+
|
722
|
+
// Check if the element has accessible labels or text content
|
723
|
+
if (!hasAccessibleLabel(element) && !hasChildWithAccessibleText(element) && !isAccessibleText(getTextContent(element)) && !(hasChildNotANewInteractWithAccessibleText(element.parentElement))) {
|
724
|
+
customConsoleWarn("Flagging element with background image but without accessible label or text.");
|
725
|
+
return true; // Flag the element
|
726
|
+
} else {
|
727
|
+
customConsoleWarn("Element with background image has accessible label or text, skipping flagging.");
|
728
|
+
return false; // Do not flag
|
729
|
+
}
|
667
730
|
}
|
668
|
-
}
|
669
731
|
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
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
|
-
}
|
732
|
+
// **Proceed with ancestor traversal if no background image is found**
|
733
|
+
// Traverse ancestors to check for interactive elements with accessible labels
|
734
|
+
let ancestor = element.parentElement;
|
735
|
+
let depth = 0;
|
736
|
+
const maxDepth = 4; // Limit the depth to prevent skipping elements incorrectly
|
737
|
+
while (ancestor && depth < maxDepth) {
|
738
|
+
// Determine if ancestor is interactive
|
739
|
+
const isAncestorInteractive = hasPointerCursor(ancestor) ||
|
740
|
+
ancestor.hasAttribute('onclick') ||
|
741
|
+
ancestor.hasAttribute('role') ||
|
742
|
+
(ancestor.hasAttribute('tabindex') && ancestor.getAttribute('tabindex') !== '-1') ||
|
743
|
+
ancestor.hasAttribute('jsaction') ||
|
744
|
+
ancestor.hasAttribute('jscontroller');
|
745
|
+
|
746
|
+
if (isAncestorInteractive) {
|
747
|
+
// Check if ancestor has accessible label or text content
|
748
|
+
if (hasAccessibleLabel(ancestor) || isAccessibleText(getTextContent(ancestor)) || hasChildWithAccessibleText(ancestor)) {
|
749
|
+
customConsoleWarn("Ancestor interactive element has accessible label or text content, skipping flagging.");
|
750
|
+
return false;
|
751
|
+
} else {
|
752
|
+
// Ancestor is interactive but lacks accessible labeling
|
753
|
+
customConsoleWarn("Ancestor interactive element lacks accessible label, continue flagging.");
|
754
|
+
// Do not skip flagging
|
755
|
+
}
|
756
|
+
}
|
757
|
+
ancestor = ancestor.parentElement;
|
758
|
+
depth++;
|
703
759
|
}
|
704
|
-
ancestor = ancestor.parentElement;
|
705
|
-
depth++;
|
706
|
-
}
|
707
760
|
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
customConsoleWarn(
|
714
|
-
'Not Flagging clickable div or span with pointer cursor with accessible text.',
|
715
|
-
);
|
716
|
-
return false;
|
717
|
-
}
|
761
|
+
if (hasAccessibleLabel(element) || isAccessibleText(getTextContent(element)) || validAriaRoles.includes(element.getAttribute("role")))
|
762
|
+
{
|
763
|
+
customConsoleWarn("Not Flagging clickable div or span with pointer cursor with accessible text.");
|
764
|
+
return false;
|
765
|
+
}
|
718
766
|
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
);
|
723
|
-
return true;
|
767
|
+
// If no interactive ancestor with accessible label is found, flag the element
|
768
|
+
customConsoleWarn("Flagging clickable div or span with pointer cursor and no accessible text.");
|
769
|
+
return true;
|
724
770
|
}
|
725
771
|
|
726
772
|
// Skip elements with role="menuitem" and ensure accessibility label for any nested elements
|
727
773
|
if (element.getAttribute('role') === 'menuitem') {
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
774
|
+
if (hasChildWithAccessibleText(element)) {
|
775
|
+
customConsoleWarn("Menuitem element has child with accessible text, skipping flagging.");
|
776
|
+
return false;
|
777
|
+
}
|
732
778
|
}
|
733
779
|
|
734
780
|
// Check if the parent element has an accessible label
|
735
781
|
const parent = element.closest('[aria-label], [role="button"], [role="link"], a, button');
|
736
782
|
|
737
|
-
if (parent && (hasAccessibleLabel(parent) || hasChildWithAccessibleText(parent))) {
|
738
|
-
|
739
|
-
|
740
|
-
);
|
741
|
-
return false;
|
783
|
+
if (parent && parent !== element && !landmarkElements.includes(parent.nodeName.toLowerCase()) && (hasAccessibleLabel(parent) || hasChildWithAccessibleText(parent))) {
|
784
|
+
customConsoleWarn("Parent element has an accessible label or accessible child, skipping flagging.",parent);
|
785
|
+
return false;
|
742
786
|
}
|
743
787
|
|
744
788
|
// Skip flagging if any child has an accessible label (e.g., <img alt="...">
|
745
789
|
if (hasAllChildrenAccessible(element)) {
|
746
|
-
|
747
|
-
|
790
|
+
customConsoleWarn("Element has child nodes with accessible text.");
|
791
|
+
return false;
|
748
792
|
}
|
749
793
|
|
750
794
|
// Check if the <a> element has all children accessible
|
751
795
|
if (element.nodeName.toLowerCase() === 'a' && hasAllChildrenAccessible(element)) {
|
752
|
-
|
753
|
-
|
796
|
+
customConsoleWarn("Hyperlink has all children with accessible labels, skipping flagging.");
|
797
|
+
return false;
|
754
798
|
}
|
755
799
|
|
756
800
|
if (element.hasAttribute('tabindex') && element.getAttribute('tabindex') === '-1') {
|
757
|
-
|
758
|
-
|
801
|
+
customConsoleWarn("Element has tabindex='-1'.");
|
802
|
+
return false;
|
759
803
|
}
|
760
804
|
|
761
|
-
const childWithTabindexNegativeOne = Array.from(element.children).some(
|
762
|
-
|
805
|
+
const childWithTabindexNegativeOne = Array.from(element.children).some(child =>
|
806
|
+
child.hasAttribute('tabindex') && child.getAttribute('tabindex') === '-1'
|
763
807
|
);
|
764
808
|
if (childWithTabindexNegativeOne) {
|
765
|
-
|
766
|
-
|
809
|
+
customConsoleWarn("Element has a child with tabindex='-1'.");
|
810
|
+
return false;
|
767
811
|
}
|
768
812
|
|
769
813
|
if (landmarkElements.includes(element.nodeName.toLowerCase())) {
|
770
|
-
|
771
|
-
|
814
|
+
customConsoleWarn("Element is a landmark element.");
|
815
|
+
return false;
|
772
816
|
}
|
773
817
|
|
774
818
|
// 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
|
-
|
777
|
-
|
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;
|
819
|
+
if ((element.nodeName.toLowerCase() === 'svg' || element.nodeName.toLowerCase() === 'icon') && (element.getAttribute('focusable') === 'false' || hasSiblingOrParentAccessibleLabel(element) || element.closest('[role="button"]') || element.closest('button'))) {
|
820
|
+
customConsoleWarn("Sibling or parent element has an accessible label or svg is part of a button, skipping flagging of svg or icon.");
|
821
|
+
return false;
|
786
822
|
}
|
787
823
|
|
788
824
|
if (element.nodeName.toLowerCase() === 'svg') {
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
|
825
|
+
const parentGroup = element.closest('g');
|
826
|
+
if (parentGroup && parentGroup.querySelector('title')) {
|
827
|
+
customConsoleWarn("Parent group element has a <title>, skipping flagging of svg.");
|
828
|
+
return false;
|
829
|
+
}
|
794
830
|
}
|
795
831
|
|
796
832
|
if (element.nodeName.toLowerCase() === 'button') {
|
797
|
-
|
798
|
-
|
799
|
-
|
800
|
-
|
801
|
-
|
802
|
-
}
|
833
|
+
const hasAccessibleLabelForButton = hasAccessibleLabel(element) || isAccessibleText(getTextContent(element));
|
834
|
+
if (hasAccessibleLabelForButton) {
|
835
|
+
customConsoleWarn("Button has an accessible label, skipping flagging.");
|
836
|
+
return false;
|
837
|
+
}
|
803
838
|
|
804
|
-
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
|
809
|
-
|
810
|
-
|
839
|
+
const hasSvgChildWithoutLabel = Array.from(element.children).some(child => child.nodeName.toLowerCase() === 'svg' && !hasAccessibleLabel(child));
|
840
|
+
if (hasSvgChildWithoutLabel) {
|
841
|
+
customConsoleWarn("Flagging button with child SVG lacking accessible label.");
|
842
|
+
return true;
|
843
|
+
}
|
844
|
+
|
845
|
+
// HAS CSS CONTENT
|
846
|
+
const hasAccessibleCSSContent = hasCSSContent(element)
|
847
|
+
if (!hasAccessibleCSSContent && !hasChildWhichIsVisibleFocusable(element) && !hasChildNotANewInteractWithAccessibleText(element))
|
848
|
+
{
|
849
|
+
customConsoleWarn("Flagging button without Valid CSSCONTENT CHILDREN as well as no other valid children");
|
850
|
+
return true;
|
851
|
+
}
|
852
|
+
|
853
|
+
if (hasChildWhichIsVisibleFocusable(element) && !hasChildNotANewInteractWithAccessibleText(element))
|
854
|
+
{
|
855
|
+
customConsoleWarn("Flagging button with focusable but without any valid children");
|
856
|
+
return true;
|
857
|
+
}
|
811
858
|
}
|
812
859
|
|
813
|
-
if (
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
860
|
+
if (element.nodeName.toLowerCase() === 'input' && !hasAccessibleLabel(element)) {
|
861
|
+
|
862
|
+
if (element.getAttribute("placeholder") && !isAccessibleText(element.getAttribute("placeholder")))
|
863
|
+
{
|
864
|
+
customConsoleWarn("Flagging <input> without valid placeholder text");
|
865
|
+
return true;
|
866
|
+
}
|
867
|
+
|
868
|
+
if (element.getAttribute("value") && !isAccessibleText(element.getAttribute("value")))
|
869
|
+
{
|
870
|
+
customConsoleWarn("Flagging <input> without valid placeholder text");
|
871
|
+
return true;
|
872
|
+
}
|
873
|
+
|
874
|
+
if (element.tagName === 'image')
|
875
|
+
{
|
876
|
+
customConsoleWarn("Flagging <input type='image'> without accessible label.");
|
877
|
+
return true;
|
878
|
+
}
|
820
879
|
}
|
821
880
|
|
822
881
|
if (element.nodeName.toLowerCase() === 'a') {
|
823
|
-
|
882
|
+
const img = element.querySelector('img');
|
824
883
|
|
825
|
-
|
826
|
-
|
884
|
+
// Log to verify visibility and pointer checks
|
885
|
+
customConsoleWarn("Processing <a> element.");
|
827
886
|
|
828
|
-
|
829
|
-
|
887
|
+
// Ensure this <a> does not have an accessible label
|
888
|
+
const linkHasAccessibleLabel = hasAccessibleLabel(element);
|
830
889
|
|
831
|
-
|
832
|
-
|
890
|
+
// Ensure the <img> inside <a> does not have an accessible label
|
891
|
+
const imgHasAccessibleLabel = img ? hasAccessibleLabel(img) : false;
|
833
892
|
|
834
|
-
|
835
|
-
|
836
|
-
|
837
|
-
|
838
|
-
|
839
|
-
|
893
|
+
// Log to verify if <img> has accessible label
|
894
|
+
if (img) {
|
895
|
+
customConsoleWarn("Found <img> inside <a>. Accessible label: " + imgHasAccessibleLabel);
|
896
|
+
} else {
|
897
|
+
customConsoleWarn("No <img> found inside <a>.");
|
898
|
+
}
|
899
|
+
|
840
900
|
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
845
|
-
|
901
|
+
// Skip flagging if <a> has an accessible label or all children are accessible
|
902
|
+
if (linkHasAccessibleLabel || hasChildNotANewInteractWithAccessibleText(element)) {
|
903
|
+
customConsoleWarn("Hyperlink has an accessible label, skipping flagging.");
|
904
|
+
return false;
|
905
|
+
}
|
846
906
|
|
847
|
-
|
848
|
-
|
849
|
-
|
850
|
-
|
851
|
-
|
907
|
+
// Flag if both <a> and <img> inside lack accessible labels
|
908
|
+
if (!linkHasAccessibleLabel && img && !imgHasAccessibleLabel) {
|
909
|
+
customConsoleWarn("Flagging <a> with inaccessible <img>.");
|
910
|
+
return true;
|
911
|
+
}
|
852
912
|
|
853
|
-
|
854
|
-
|
855
|
-
|
856
|
-
|
913
|
+
if (!linkHasAccessibleLabel)
|
914
|
+
{
|
915
|
+
customConsoleWarn("Flagging <a> with no accessible label");
|
916
|
+
return true;
|
917
|
+
}
|
857
918
|
}
|
858
919
|
|
859
920
|
// Modify this section for generic elements
|
860
921
|
if (['span', 'div', 'icon', 'svg', 'button'].includes(element.nodeName.toLowerCase())) {
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
) {
|
869
|
-
customConsoleWarn('Flagging icon or svg without accessible label.');
|
870
|
-
return true;
|
922
|
+
if (element.nodeName.toLowerCase() === 'icon' || element.nodeName.toLowerCase() === 'svg') {
|
923
|
+
// Check if the element has an accessible label or if it has a sibling, parent, or summary/related element that provides an accessible label
|
924
|
+
if (!hasAccessibleLabel(element) && !hasSiblingOrParentAccessibleLabel(element) && !hasSummaryOrDetailsLabel(element) && element.getAttribute('focusable') !== 'false') {
|
925
|
+
customConsoleWarn("Flagging icon or svg without accessible label.");
|
926
|
+
return true;
|
927
|
+
}
|
928
|
+
return false;
|
871
929
|
}
|
872
|
-
return false;
|
873
|
-
}
|
874
930
|
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
|
931
|
+
if (getTextContent(element).trim().length > 0) {
|
932
|
+
customConsoleWarn("Element has valid text content.");
|
933
|
+
return false;
|
934
|
+
}
|
879
935
|
|
880
|
-
|
881
|
-
|
882
|
-
|
883
|
-
|
884
|
-
|
885
|
-
|
886
|
-
|
936
|
+
if (element.hasAttribute('aria-label') && isAccessibleText(element.getAttribute('aria-label'))) {
|
937
|
+
customConsoleWarn("Element has an aria-label attribute, skipping flagging.");
|
938
|
+
return false;
|
939
|
+
}
|
940
|
+
|
941
|
+
if(getTextContent(element) === "" && !hasChildWhichIsVisibleFocusable(element))
|
942
|
+
{
|
943
|
+
customConsoleWarn("Button has no text content or anything that can be used for a screen reader");
|
944
|
+
return true;
|
945
|
+
}
|
887
946
|
}
|
888
947
|
|
889
948
|
if (element.nodeName.toLowerCase() === 'div') {
|
890
|
-
const flaggedChild = Array.from(element.children).some(
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
949
|
+
const flaggedChild = Array.from(element.children).some(child => {
|
950
|
+
const childElement = child as HTMLElement; // Cast child to HTMLElement
|
951
|
+
return childElement.dataset.flagged === 'true'; // Now TypeScript will recognize dataset
|
952
|
+
});
|
953
|
+
|
954
|
+
if (flaggedChild) {
|
955
|
+
customConsoleWarn("Div contains a flagged child, flagging only outermost element.");
|
956
|
+
return false;
|
957
|
+
}
|
897
958
|
|
898
|
-
|
899
|
-
|
900
|
-
|
901
|
-
|
902
|
-
|
959
|
+
// Update this condition to include hasChildWithAccessibleText
|
960
|
+
if (getTextContent(element).trim().length > 0 || hasChildWithAccessibleText(element)) {
|
961
|
+
customConsoleWarn("Div has valid text content or child with accessible text.");
|
962
|
+
return false;
|
963
|
+
}
|
903
964
|
|
904
|
-
|
905
|
-
|
906
|
-
|
907
|
-
|
908
|
-
|
909
|
-
|
910
|
-
|
911
|
-
|
912
|
-
|
913
|
-
return false;
|
965
|
+
const img = element.querySelector('img');
|
966
|
+
if (img) {
|
967
|
+
const altText = img.getAttribute('alt');
|
968
|
+
const ariaLabel = img.getAttribute('aria-label');
|
969
|
+
const ariaLabelledByText = getAriaLabelledByText(img);
|
970
|
+
if (altText !== null || ariaLabel || ariaLabelledByText) {
|
971
|
+
customConsoleWarn("Div contains an accessible img or an img with an alt attribute (even if empty).");
|
972
|
+
return false;
|
973
|
+
}
|
914
974
|
}
|
915
|
-
}
|
916
975
|
|
917
|
-
|
918
|
-
|
919
|
-
|
920
|
-
|
921
|
-
|
922
|
-
|
923
|
-
svg.getAttribute('focusable') !== 'false'
|
924
|
-
) {
|
925
|
-
customConsoleWarn('Flagging clickable div with SVG without accessible label.');
|
926
|
-
return true;
|
976
|
+
const svg = element.querySelector('svg');
|
977
|
+
if (svg) {
|
978
|
+
if (hasPointerCursor(element) && !hasAccessibleLabel(svg) && !hasSummaryOrDetailsLabel(svg) && svg.getAttribute('focusable') !== 'false') {
|
979
|
+
customConsoleWarn("Flagging clickable div with SVG without accessible label.");
|
980
|
+
return true;
|
981
|
+
}
|
927
982
|
}
|
928
|
-
}
|
929
983
|
|
930
|
-
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
) {
|
935
|
-
customConsoleWarn('Clickable div without accessible label or text content.');
|
936
|
-
return true;
|
937
|
-
}
|
984
|
+
if (hasPointerCursor(element) && !hasAccessibleLabel(element) && !isAccessibleText(getTextContent(element)) && !hasChildWhichIsVisibleFocusable(element)) {
|
985
|
+
customConsoleWarn("Clickable div without accessible label or text content.");
|
986
|
+
return true;
|
987
|
+
}
|
938
988
|
}
|
939
989
|
|
940
990
|
if (element.nodeName.toLowerCase() === 'img' || element.nodeName.toLowerCase() === 'picture') {
|
941
|
-
|
942
|
-
|
943
|
-
|
944
|
-
|
945
|
-
|
946
|
-
|
947
|
-
|
948
|
-
|
949
|
-
|
950
|
-
|
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;
|
991
|
+
const imgElement = element.nodeName.toLowerCase() === 'picture' ? element.querySelector('img') : element;
|
992
|
+
const altText = imgElement.getAttribute('alt');
|
993
|
+
const ariaLabel = imgElement.getAttribute('aria-label');
|
994
|
+
const ariaLabelledByText = getAriaLabelledByText(imgElement);
|
995
|
+
|
996
|
+
if (!allowNonClickableFlagging) {
|
997
|
+
if (!imgElement.closest('a') && !imgElement.closest('button') && !hasPointerCursor(imgElement) && !(altText !== null) && !(ariaLabel && ariaLabel.trim().length > 0) && !(ariaLabelledByText && ariaLabelledByText.length > 0)) {
|
998
|
+
customConsoleWarn("Non-clickable image ignored.");
|
999
|
+
return false;
|
1000
|
+
}
|
958
1001
|
}
|
959
|
-
}
|
960
1002
|
|
961
|
-
|
962
|
-
|
963
|
-
|
964
|
-
|
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
|
-
}
|
1003
|
+
if (!imgElement.closest('a') && !imgElement.closest('button') && !(altText !== null) && !(ariaLabel && ariaLabel.trim().length > 0) && !(ariaLabelledByText && ariaLabelledByText.length > 0)) {
|
1004
|
+
customConsoleWarn("Flagging img or picture without accessible label.");
|
1005
|
+
return true;
|
1006
|
+
}
|
971
1007
|
}
|
972
1008
|
|
973
1009
|
// 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
|
-
);
|
1010
|
+
const areAllDescendantsEmpty = Array.from(element.querySelectorAll('*')).every(child => getTextContent(child).trim().length === 0 && !hasAccessibleLabel(child));
|
977
1011
|
if (element.nodeName.toLowerCase() === 'div' && areAllDescendantsEmpty) {
|
978
|
-
|
979
|
-
|
1012
|
+
customConsoleWarn("Div with empty descendants, skipping flagging.");
|
1013
|
+
return false;
|
980
1014
|
}
|
981
1015
|
|
982
1016
|
if (hasCSSContent(element)) {
|
983
|
-
|
984
|
-
|
1017
|
+
customConsoleWarn("Element has CSS ::before or ::after content, skipping flagging.");
|
1018
|
+
return false;
|
985
1019
|
}
|
986
1020
|
|
1021
|
+
customConsoleWarn("DEFAULT CASE");
|
987
1022
|
return false; // Default case: do not flag
|
988
1023
|
}
|
989
1024
|
|