@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.
@@ -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
- 'header',
8
- 'footer',
9
- 'nav',
10
- 'main',
11
- 'article',
12
- 'section',
13
- 'aside',
14
- 'form',
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
- function hasPointerCursor(element: Element) {
52
- const computedStyle = element.ownerDocument.defaultView.getComputedStyle(element);
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 = 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
- }
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 contains any private use characters
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
- return false;
103
+ return false;
86
104
  }
87
105
 
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;
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
- // If it doesn't contain letters or numbers, consider it not accessible
95
- return false;
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
- console.log('Error in ELEMENT', el, error.message);
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
- return (
165
- isAccessibleText(ariaLabel) ||
166
- isAccessibleText(ariaLabelledByText) ||
167
- isAccessibleText(altText) ||
168
- isAccessibleText(title)
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
- (previousSibling && hasAccessibleLabel(previousSibling)) ||
204
- (nextSibling && hasAccessibleLabel(nextSibling))
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
- return true;
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() === 'style' || child.nodeName.toLowerCase() === 'script') {
222
- return false;
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
- return false;
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
- return isAccessibleText(node.textContent);
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
- // 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
- );
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
- return false;
342
+ return false;
296
343
  }
297
344
 
298
- if (child.nodeName.toLowerCase() === 'style' || child.nodeName.toLowerCase() === 'script') {
299
- return false;
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
- child.getAttribute('aria-hidden') === 'true' ||
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
- if (node.nodeType === Node.TEXT_NODE) {
324
- const textContent = getTextContent(node);
325
-
326
- // Check if the text contains non-ASCII characters (Unicode)
327
- const containsUnicode = /[^\x00-\x7F]/.test(textContent);
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
- // If contains non-ASCII characters, validate with isValidUnicode
330
- if (containsUnicode) {
331
- return isValidUnicode(textContent);
374
+ // Otherwise, just check if it's non-empty text
375
+ return textContent.length > 0;
332
376
  }
333
377
 
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
- }
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
- 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 */
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
- document.head.appendChild(style);
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: 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();
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
- // Use the isAccessibleText function
398
- return isAccessibleText(unquotedValue);
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
- // Check if width or height is less than 1
409
- return rect.width < 1 || rect.height < 1;
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?.trim() ?? ''; // Return the text directly if it's a TEXT_NODE
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 (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
- }
432
- }
433
-
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
- }
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
- return false;
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
- customConsoleWarn(
455
- 'Element is not clickable and allowNonClickableFlagging is false, skipping flagging.',
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
- customConsoleWarn("An ancestor element has aria-hidden='true', skipping flagging.");
463
- return false;
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
- // 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
-
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 <= 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
- }
567
+ while (parents && tracedBackedLayerCount < maxLayers) {
487
568
 
488
- // Skip flagging if the parent is a button-like element with aria-expanded
489
- if (
490
- parents.getAttribute('role') === 'button' &&
491
- (parents.hasAttribute('aria-expanded') || parents.hasAttribute('aria-controls'))
492
- ) {
493
- customConsoleWarn(
494
- 'Parent element is an interactive button with aria-expanded or aria-controls, skipping flagging.',
495
- );
496
- return false;
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
- // 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
- }
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
- // 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;
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
- parents = parents.parentElement;
522
- tracedBackedLayerCount++;
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
- 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
- }
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
- 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
- );
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
- (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
- );
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
- if (hasAccessibleChild) {
564
- customConsoleWarn(
565
- 'Parent element has an accessible child, skipping flagging of empty span or div.',
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
- customConsoleWarn('Element is aria-hidden, skipping flagging.');
575
- return false;
653
+ customConsoleWarn("Element is aria-hidden, skipping flagging.");
654
+ return false;
576
655
  }
577
656
 
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);
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
- // Check if the element was found
591
- if (
592
- referencedElement &&
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
- customConsoleWarn("Element has role='presentation', skipping flagging.");
609
- return false;
685
+ customConsoleWarn("Element has role='presentation', skipping flagging.");
686
+ return false;
610
687
  }
611
688
 
612
689
  if (element.dataset.flagged === 'true') {
613
- customConsoleWarn('Element is already flagged.');
614
- return false;
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
- customConsoleWarn('An ancestor element is already flagged.');
620
- return false;
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
- 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;
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
- (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
- }
709
+ if ((element.nodeName.toLowerCase() === 'div' || element.nodeName.toLowerCase() === 'span') &&
710
+ element.children.length === 0 && getTextContent(element).trim().length === 0) {
646
711
 
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.');
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
- // 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
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
- // **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
- }
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
- if (
709
- hasAccessibleLabel(element) ||
710
- isAccessibleText(getTextContent(element)) ||
711
- hasChildWithAccessibleText(element)
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
- // 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;
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
- if (hasChildWithAccessibleText(element)) {
729
- customConsoleWarn('Menuitem element has child with accessible text, skipping flagging.');
730
- return false;
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
- customConsoleWarn(
739
- 'Parent element has an accessible label or accessible child, skipping flagging.',
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
- customConsoleWarn('Element has child nodes with accessible text.');
747
- return false;
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
- customConsoleWarn('Hyperlink has all children with accessible labels, skipping flagging.');
753
- return false;
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
- customConsoleWarn("Element has tabindex='-1'.");
758
- return false;
801
+ customConsoleWarn("Element has tabindex='-1'.");
802
+ return false;
759
803
  }
760
804
 
761
- const childWithTabindexNegativeOne = Array.from(element.children).some(
762
- child => child.hasAttribute('tabindex') && child.getAttribute('tabindex') === '-1',
805
+ const childWithTabindexNegativeOne = Array.from(element.children).some(child =>
806
+ child.hasAttribute('tabindex') && child.getAttribute('tabindex') === '-1'
763
807
  );
764
808
  if (childWithTabindexNegativeOne) {
765
- customConsoleWarn("Element has a child with tabindex='-1'.");
766
- return false;
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
- customConsoleWarn('Element is a landmark element.');
771
- return false;
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
- (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;
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
- 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
- }
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
- const hasAccessibleLabelForButton =
798
- hasAccessibleLabel(element) || isAccessibleText(getTextContent(element));
799
- if (hasAccessibleLabelForButton) {
800
- customConsoleWarn('Button has an accessible label, skipping flagging.');
801
- return false;
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
- 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;
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
- 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;
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
- const img = element.querySelector('img');
882
+ const img = element.querySelector('img');
824
883
 
825
- // Log to verify visibility and pointer checks
826
- customConsoleWarn('Processing <a> element.');
884
+ // Log to verify visibility and pointer checks
885
+ customConsoleWarn("Processing <a> element.");
827
886
 
828
- // Ensure this <a> does not have an accessible label
829
- const linkHasAccessibleLabel = hasAccessibleLabel(element);
887
+ // Ensure this <a> does not have an accessible label
888
+ const linkHasAccessibleLabel = hasAccessibleLabel(element);
830
889
 
831
- // Ensure the <img> inside <a> does not have an accessible label
832
- const imgHasAccessibleLabel = img ? hasAccessibleLabel(img) : false;
890
+ // Ensure the <img> inside <a> does not have an accessible label
891
+ const imgHasAccessibleLabel = img ? hasAccessibleLabel(img) : false;
833
892
 
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>.');
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
- // 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
- }
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
- // 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
- }
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
- if (!linkHasAccessibleLabel) {
854
- customConsoleWarn('Flagging <a> with no accessible label');
855
- return true;
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
- 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;
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
- if (getTextContent(element).trim().length > 0) {
876
- customConsoleWarn('Element has valid text content.');
877
- return false;
878
- }
931
+ if (getTextContent(element).trim().length > 0) {
932
+ customConsoleWarn("Element has valid text content.");
933
+ return false;
934
+ }
879
935
 
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
- }
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
- 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
- }
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
- // 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
- }
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
- 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;
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
- 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;
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
- 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
- }
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
- 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;
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
- 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
- }
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
- customConsoleWarn('Div with empty descendants, skipping flagging.');
979
- return false;
1012
+ customConsoleWarn("Div with empty descendants, skipping flagging.");
1013
+ return false;
980
1014
  }
981
1015
 
982
1016
  if (hasCSSContent(element)) {
983
- customConsoleWarn('Element has CSS ::before or ::after content, skipping flagging.');
984
- return false;
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