@afixt/test-utils 1.2.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/.claude/settings.local.json +1 -6
  2. package/BROWSER_TESTING.md +42 -22
  3. package/CHANGELOG.md +40 -0
  4. package/CLAUDE.md +10 -9
  5. package/package.json +1 -1
  6. package/src/constants.js +438 -1
  7. package/src/domUtils.js +17 -38
  8. package/src/formUtils.js +7 -24
  9. package/src/getAccessibleName.js +20 -56
  10. package/src/getCSSGeneratedContent.js +2 -0
  11. package/src/getFocusableElements.js +12 -21
  12. package/src/getGeneratedContent.js +18 -11
  13. package/src/getImageText.js +22 -7
  14. package/src/hasValidAriaRole.js +11 -19
  15. package/src/index.js +4 -4
  16. package/src/interactiveRoles.js +2 -19
  17. package/src/isA11yVisible.js +95 -0
  18. package/src/isAriaAttributesValid.js +5 -64
  19. package/src/isFocusable.js +30 -10
  20. package/src/isHidden.js +44 -8
  21. package/src/listEventListeners.js +115 -10
  22. package/src/stringUtils.js +19 -98
  23. package/src/tableUtils.js +4 -36
  24. package/src/testContrast.js +54 -0
  25. package/test/domUtils.test.js +156 -0
  26. package/test/formUtils.test.js +0 -47
  27. package/test/getAccessibleName.test.js +39 -0
  28. package/test/getGeneratedContent.test.js +305 -241
  29. package/test/getImageText.test.js +158 -99
  30. package/test/index.test.js +54 -17
  31. package/test/{isVisible.test.js → isA11yVisible.test.js} +39 -33
  32. package/test/isFocusable.test.js +265 -272
  33. package/test/isHidden.test.js +257 -153
  34. package/test/listEventListeners.test.js +163 -44
  35. package/test/playwright/css-pseudo-elements.spec.js +3 -13
  36. package/test/stringUtils.test.js +55 -228
  37. package/test/testContrast.test.js +104 -2
  38. package/todo.md +2 -2
  39. package/src/isVisible.js +0 -103
package/src/domUtils.js CHANGED
@@ -1,3 +1,12 @@
1
+ const {
2
+ VALID_ARIA_NESTING,
3
+ LANDMARK_TAGS,
4
+ LANDMARK_ROLES,
5
+ SEMANTIC_CONTAINER_TAGS,
6
+ SEMANTIC_CONTAINER_ROLES,
7
+ INTERACTIVE_HANDLER_ATTRIBUTES,
8
+ } = require('./constants.js');
9
+
1
10
  const domUtils = {
2
11
  /**
3
12
  * Checks if the specified element has the given attribute.
@@ -250,21 +259,6 @@ const domUtils = {
250
259
  * @returns {boolean} True if this is a valid ARIA nesting pattern
251
260
  */
252
261
  isValidAriaNesting(parent, child) {
253
- const VALID_ARIA_NESTING = {
254
- listbox: ['option'],
255
- menu: ['menuitem', 'menuitemcheckbox', 'menuitemradio', 'menu'],
256
- menubar: ['menuitem', 'menuitemcheckbox', 'menuitemradio', 'menu'],
257
- menuitem: ['menu', 'menubar'],
258
- tablist: ['tab'],
259
- tree: ['treeitem', 'group'],
260
- treeitem: ['group', 'tree'],
261
- grid: ['gridcell', 'row', 'rowgroup'],
262
- row: ['gridcell', 'columnheader', 'rowheader', 'cell'],
263
- rowgroup: ['row'],
264
- radiogroup: ['radio'],
265
- combobox: ['listbox', 'textbox', 'tree', 'grid', 'dialog'],
266
- };
267
-
268
262
  const parentRole = (parent.getAttribute('role') || '').toLowerCase();
269
263
  const childRole = (child.getAttribute('role') || '').toLowerCase();
270
264
 
@@ -287,14 +281,7 @@ const domUtils = {
287
281
  * @returns {boolean} True if element has interactive handlers
288
282
  */
289
283
  hasInteractiveHandler(element) {
290
- return (
291
- element.hasAttribute('onclick') ||
292
- element.hasAttribute('onmousedown') ||
293
- element.hasAttribute('onmouseup') ||
294
- element.hasAttribute('ontouchstart') ||
295
- element.hasAttribute('onkeydown') ||
296
- element.hasAttribute('onkeyup')
297
- );
284
+ return INTERACTIVE_HANDLER_ATTRIBUTES.some(attr => element.hasAttribute(attr));
298
285
  },
299
286
 
300
287
  /**
@@ -321,22 +308,11 @@ const domUtils = {
321
308
  * @returns {boolean} True if element is a landmark
322
309
  */
323
310
  isLandmark(element) {
324
- const landmarkTags = ['NAV', 'MAIN', 'HEADER', 'FOOTER', 'ASIDE', 'SECTION'];
325
- const landmarkRoles = [
326
- 'navigation',
327
- 'main',
328
- 'banner',
329
- 'contentinfo',
330
- 'complementary',
331
- 'region',
332
- 'search',
333
- 'form',
334
- ];
335
- if (landmarkTags.includes(element.tagName)) {
311
+ if (LANDMARK_TAGS.includes(element.tagName)) {
336
312
  return true;
337
313
  }
338
314
  const role = (element.getAttribute('role') || '').toLowerCase();
339
- return landmarkRoles.includes(role);
315
+ return LANDMARK_ROLES.includes(role);
340
316
  },
341
317
 
342
318
  /**
@@ -345,10 +321,13 @@ const domUtils = {
345
321
  * @returns {HTMLElement|null} The nearest semantic container
346
322
  */
347
323
  getSemanticContainer(el) {
348
- const containerTags = ['ARTICLE', 'SECTION', 'LI', 'TD', 'TH', 'BLOCKQUOTE', 'FIGURE'];
349
324
  let ancestor = el.parentElement;
350
325
  while (ancestor && ancestor !== document.body) {
351
- if (containerTags.includes(ancestor.tagName) || ancestor.hasAttribute('role')) {
326
+ if (SEMANTIC_CONTAINER_TAGS.includes(ancestor.tagName)) {
327
+ return ancestor;
328
+ }
329
+ const role = (ancestor.getAttribute('role') || '').toLowerCase();
330
+ if (role && SEMANTIC_CONTAINER_ROLES.includes(role)) {
352
331
  return ancestor;
353
332
  }
354
333
  ancestor = ancestor.parentElement;
package/src/formUtils.js CHANGED
@@ -35,8 +35,13 @@ const formUtils = {
35
35
 
36
36
  /**
37
37
  * Checks if an element has an explicit accessible name via aria-labelledby, aria-label, or title.
38
- * Text content is NOT checked (useful for containers like radiogroup/group where
39
- * text content is not a valid accessible name source).
38
+ *
39
+ * Unlike getAccessibleName/hasAccessibleName, this deliberately excludes text content.
40
+ * This is important for container roles (radiogroup, group) where text content of
41
+ * child elements is not a valid accessible name source per the ARIA spec — these
42
+ * roles require an author-provided label.
43
+ *
44
+ * Used by isProperlyGrouped() to enforce that ARIA groups have explicit labels.
40
45
  * @param {Element} element - The element to check
41
46
  * @returns {boolean} True if element has an explicit accessible name
42
47
  */
@@ -126,28 +131,6 @@ const formUtils = {
126
131
  return document.body;
127
132
  },
128
133
 
129
- /**
130
- * Check if a native form element has an associated label.
131
- * Checks label[for], wrapping label, aria-label, aria-labelledby, and title.
132
- * @param {HTMLElement} element - The element to check
133
- * @returns {boolean} True if the element has an associated label
134
- */
135
- hasAssociatedLabel(element) {
136
- if (element.id) {
137
- const label = document.querySelector('label[for="' + element.id + '"]');
138
- if (label && label.textContent.trim()) {
139
- return true;
140
- }
141
- }
142
-
143
- const parentLabel = element.closest('label');
144
- if (parentLabel && parentLabel.textContent.trim()) {
145
- return true;
146
- }
147
-
148
- return false;
149
- },
150
-
151
134
  /**
152
135
  * Get the text content of an element, excluding form control children.
153
136
  * Useful for wrapped labels like <label><input type="checkbox"> Remember me</label>
@@ -1,5 +1,11 @@
1
1
  const { isEmpty } = require('./stringUtils.js');
2
2
  const { getAccessibleText } = require('./getAccessibleText.js');
3
+ const {
4
+ TEXT_ROLES,
5
+ NON_VISIBLE_SELECTORS,
6
+ LANDMARK_ROLES,
7
+ LANDMARK_ELEMENT_MAP,
8
+ } = require('./constants.js');
3
9
 
4
10
  /**
5
11
  * Gets the accessible name of an element according to the accessible name calculation algorithm
@@ -64,28 +70,8 @@ function getAccessibleName(element) {
64
70
  // We check all of those here.
65
71
  if (element.hasAttribute('role')) {
66
72
  const roleValue = element.getAttribute('role');
67
- const textRoles = [
68
- 'button',
69
- 'checkbox',
70
- 'columnheader',
71
- 'gridcell',
72
- 'heading',
73
- 'link',
74
- 'listitem',
75
- 'menuitem',
76
- 'menuitemcheckbox',
77
- 'menuitemradio',
78
- 'option',
79
- 'radio',
80
- 'row',
81
- 'rowgroup',
82
- 'rowheader',
83
- 'tab',
84
- 'tooltip',
85
- 'treeitem',
86
- ];
87
-
88
- if (textRoles.includes(roleValue)) {
73
+
74
+ if (TEXT_ROLES.includes(roleValue)) {
89
75
  // Use getAccessibleText which handles both text nodes and
90
76
  // image alt text in the subtree (not just textContent)
91
77
  const text = getAccessibleText(element);
@@ -456,23 +442,23 @@ function getAccessibleName(element) {
456
442
  }
457
443
 
458
444
  // STEP 15: Landmark elements that require explicit accessible names
459
- // Navigation landmarks (nav elements and role="navigation") should NOT get
460
- // their accessible name from text content - they require explicit labeling
461
- // via aria-labelledby, aria-label, or title attribute.
445
+ // Landmark roles do NOT support "Name From: contents" per WAI-ARIA spec.
446
+ // They only support "Name From: author" (aria-labelledby, aria-label, title).
462
447
  // Since steps 1 & 2 already checked aria-labelledby and aria-label,
463
448
  // we only need to check title here, then return false.
464
- const isNavigation =
465
- element.tagName.toLowerCase() === 'nav' || element.getAttribute('role') === 'navigation';
449
+ const tagName = element.tagName.toLowerCase();
450
+ const role = element.getAttribute('role');
451
+ const isLandmark = LANDMARK_ROLES.includes(role) || tagName in LANDMARK_ELEMENT_MAP;
466
452
 
467
- if (isNavigation) {
468
- // Title attribute is valid for navigation landmarks
453
+ if (isLandmark) {
454
+ // Title attribute is valid for landmark elements
469
455
  if (element.hasAttribute('title')) {
470
456
  const titleValue = element.getAttribute('title');
471
457
  if (strlen(titleValue) > 0) {
472
458
  return titleValue;
473
459
  }
474
460
  }
475
- // Navigation landmarks do not get accessible name from text content
461
+ // Landmark elements do not get accessible name from text content
476
462
  return false;
477
463
  }
478
464
 
@@ -499,33 +485,11 @@ function isNotVisible(element) {
499
485
  return true;
500
486
  }
501
487
 
502
- // These elements are inherently not visible
503
- // Note: 'area' is NOT included here because area elements DO have accessible names
488
+ // Note: 'area' is filtered out because area elements DO have accessible names
504
489
  // via the alt attribute and should participate in accessible name calculation
505
- const nonVisibleSelectors = [
506
- 'base',
507
- 'head',
508
- 'meta',
509
- 'title',
510
- 'link',
511
- 'style',
512
- 'script',
513
- 'br',
514
- 'nobr',
515
- 'col',
516
- 'embed',
517
- 'input[type="hidden"]',
518
- 'keygen',
519
- 'source',
520
- 'track',
521
- 'wbr',
522
- 'datalist',
523
- 'param',
524
- 'noframes',
525
- 'ruby > rp',
526
- ];
527
-
528
- if (nonVisibleSelectors.some(selector => matchesSelector(element, selector))) {
490
+ const nonVisibleSelectorsWithoutArea = NON_VISIBLE_SELECTORS.filter(s => s !== 'area');
491
+
492
+ if (nonVisibleSelectorsWithoutArea.some(selector => matchesSelector(element, selector))) {
529
493
  return true; // Not visible in accessibility tree
530
494
  }
531
495
 
@@ -25,6 +25,8 @@ function extractMeaningfulContent(rawValue) {
25
25
  * not the element's own text content. Empty, whitespace-only, and blank content
26
26
  * values are filtered out as they do not convey meaningful information.
27
27
  *
28
+ * Use getGeneratedContent() if you need ::before + textContent + ::after combined.
29
+ *
28
30
  * @param {Element} el - The DOM element to check
29
31
  * @param {string} [pseudoElement='both'] - Which pseudo-element to check ('before', 'after', or 'both')
30
32
  * @returns {string|boolean} The generated content as a string or false if none exists
@@ -1,3 +1,5 @@
1
+ const { FOCUSABLE_SELECTORS } = require('./constants.js');
2
+
1
3
  /**
2
4
  * Gets all focusable elements within the specified container element.
3
5
  * @param {Element} el - the element to be tested
@@ -7,33 +9,22 @@ function getFocusableElements(el) {
7
9
  if (!el) {
8
10
  throw new Error('Container element is required');
9
11
  }
10
-
11
- const focusableSelectors = [
12
- "a[href]",
13
- "area",
14
- "button",
15
- "select",
16
- "textarea",
17
- 'input:not([type="hidden"])',
18
- "[tabindex]",
19
- ];
20
12
 
21
- return Array.from(
22
- el.querySelectorAll(focusableSelectors.join(", "))
23
- ).filter((element) => {
24
- const tabindex = element.getAttribute("tabindex");
13
+ return Array.from(el.querySelectorAll(FOCUSABLE_SELECTORS.join(', '))).filter(element => {
14
+ const tabindex = element.getAttribute('tabindex');
25
15
  const hasValidTabindex = tabindex === null || parseInt(tabindex, 10) >= 0;
26
-
16
+
27
17
  // Check visibility - handle JSDOM environment where offsetParent might not work correctly
28
- const isVisible = element.offsetParent !== null ||
29
- (typeof window !== 'undefined' &&
30
- window.navigator &&
31
- window.navigator.userAgent.includes('jsdom'));
32
-
18
+ const isVisible =
19
+ element.offsetParent !== null ||
20
+ (typeof window !== 'undefined' &&
21
+ window.navigator &&
22
+ window.navigator.userAgent.includes('jsdom'));
23
+
33
24
  return hasValidTabindex && isVisible;
34
25
  });
35
26
  }
36
27
 
37
28
  module.exports = {
38
- getFocusableElements
29
+ getFocusableElements,
39
30
  };
@@ -1,19 +1,26 @@
1
+ const { getCSSGeneratedContent } = require('./getCSSGeneratedContent.js');
2
+
1
3
  /**
2
- * Get the generated content for an element (`::before`, `::after`, and inner content).
4
+ * Get all visible text for an element, including CSS-generated ::before and ::after content
5
+ * combined with the element's own text content.
6
+ *
7
+ * Unlike getCSSGeneratedContent (which returns only pseudo-element content),
8
+ * this function returns the full visible text: ::before + textContent + ::after.
9
+ *
3
10
  * @param {Element} el - The DOM element.
4
- * @returns {string|boolean} The generated content or `false` if not available.
11
+ * @returns {string|false} The combined text or false if no content exists.
5
12
  */
6
13
  function getGeneratedContent(el) {
7
- if (!el) return false;
8
- const computedStyle = getComputedStyle(el);
9
- const before = computedStyle.getPropertyValue("content", "::before") || "";
10
- const inner = el.textContent || "";
11
- const after = computedStyle.getPropertyValue("content", "::after") || "";
12
- return before || inner || after
13
- ? `${before} ${inner} ${after}`.trim()
14
- : false;
14
+ if (!el) {
15
+ return false;
16
+ }
17
+ const before = getCSSGeneratedContent(el, 'before') || '';
18
+ const inner = el.textContent || '';
19
+ const after = getCSSGeneratedContent(el, 'after') || '';
20
+ const result = [before, inner, after].filter(Boolean).join(' ').trim();
21
+ return result || false;
15
22
  }
16
23
 
17
24
  module.exports = {
18
- getGeneratedContent
25
+ getGeneratedContent,
19
26
  };
@@ -1,17 +1,31 @@
1
1
  const Tesseract = require('tesseract.js');
2
2
 
3
+ /**
4
+ * Internal reference to Tesseract.recognize, exposed for test mocking.
5
+ * @private
6
+ */
7
+ const _internal = {
8
+ recognize: Tesseract.recognize,
9
+ };
10
+
3
11
  /**
4
12
  * Extracts text from an image using OCR.
5
13
  * @param {string} imagePath - The path or URL of the image.
14
+ * @param {Object} [options={}] - OCR options.
15
+ * @param {string} [options.lang='eng'] - Tesseract language(s) for OCR (e.g. 'fra', 'eng+deu').
16
+ * @param {Function|null} [options.logger=console.log] - Logger callback for Tesseract progress. Pass null to disable.
17
+ * @param {string} [options.corePath] - Custom path to Tesseract core WASM files.
18
+ * @param {string} [options.langPath] - Custom path to language training data.
19
+ * @param {string} [options.cachePath] - Custom cache directory path.
6
20
  * @returns {Promise<string|false>} Extracted text if found, otherwise false.
7
21
  */
8
- async function getImageText(imagePath) {
22
+ async function getImageText(imagePath, options = {}) {
23
+ const { lang = 'eng', logger = m => console.log(m), ...tesseractOptions } = options;
24
+
9
25
  try {
10
- const { data: { text } } = await Tesseract.recognize(
11
- imagePath,
12
- 'eng', // Language (English)
13
- { logger: m => console.log(m) } // Optional logging
14
- );
26
+ const {
27
+ data: { text },
28
+ } = await _internal.recognize(imagePath, lang, { logger, ...tesseractOptions });
15
29
 
16
30
  const extractedText = text.trim();
17
31
  return extractedText.length > 0 ? extractedText : false;
@@ -28,5 +42,6 @@ async function getImageText(imagePath) {
28
42
  // getImageText('path/to/image.jpg').then(result => console.log(result));
29
43
 
30
44
  module.exports = {
31
- getImageText
45
+ getImageText,
46
+ _internal,
32
47
  };
@@ -4,30 +4,22 @@
4
4
  * @param {HTMLElement} element - The DOM element to check.
5
5
  * @returns {boolean} True if the element has a valid ARIA role, false otherwise.
6
6
  */
7
- function hasValidAriaRole(element) {
8
- const validAriaRoles = new Set([
9
- "alert", "alertdialog", "button", "checkbox", "dialog", "gridcell", "link",
10
- "log", "marquee", "menuitem", "menuitemcheckbox", "menuitemradio", "option",
11
- "progressbar", "radio", "scrollbar", "searchbox", "slider", "spinbutton",
12
- "status", "switch", "tab", "tabpanel", "textbox", "tooltip", "treeitem",
13
- "combobox", "grid", "listbox", "menu", "menubar", "radiogroup", "tablist",
14
- "tree", "treegrid", "article", "cell", "columnheader", "definition",
15
- "directory", "document", "feed", "figure", "group", "heading", "img",
16
- "list", "listitem", "math", "none", "note", "presentation", "row",
17
- "rowgroup", "rowheader", "separator", "table", "term", "toolbar",
18
- "application", "banner", "complementary", "contentinfo", "form", "main",
19
- "navigation", "region", "search", "timer"
20
- ]);
7
+ const { VALID_ARIA_ROLES } = require('./constants.js');
21
8
 
22
- if (!element || typeof element.getAttribute !== 'function') return false;
9
+ function hasValidAriaRole(element) {
10
+ if (!element || typeof element.getAttribute !== 'function') {
11
+ return false;
12
+ }
23
13
 
24
14
  const roleAttr = element.getAttribute('role');
25
- if (!roleAttr) return false;
15
+ if (!roleAttr) {
16
+ return false;
17
+ }
26
18
 
27
19
  const roles = roleAttr.trim().split(/\s+/);
28
- return validAriaRoles.has(roles[0]); // Only check the first role
29
- }
20
+ return VALID_ARIA_ROLES.has(roles[0]); // Only check the first role
21
+ }
30
22
 
31
23
  module.exports = {
32
- hasValidAriaRole
24
+ hasValidAriaRole,
33
25
  };
package/src/index.js CHANGED
@@ -35,7 +35,7 @@ const isDataTable = require('./isDataTable.js');
35
35
  // Visibility and positioning
36
36
  const isHidden = require('./isHidden.js');
37
37
  const isOffScreen = require('./isOffScreen.js');
38
- const isVisible = require('./isVisible.js');
38
+ const isA11yVisible = require('./isA11yVisible.js');
39
39
  const hasHiddenParent = require('./hasHiddenParent.js');
40
40
 
41
41
  // Element relationships
@@ -50,7 +50,7 @@ const isFocusable = require('./isFocusable.js');
50
50
  const getComputedRole = require('./getComputedRole.js');
51
51
 
52
52
  // Image utilities
53
- const getImageText = require('./getImageText.js');
53
+ const { getImageText } = require('./getImageText.js');
54
54
 
55
55
  // Testing utilities
56
56
  const testContrast = require('./testContrast.js');
@@ -101,14 +101,14 @@ module.exports = {
101
101
  ...isDataTable,
102
102
  ...isHidden,
103
103
  ...isOffScreen,
104
- ...isVisible,
104
+ ...isA11yVisible,
105
105
  ...hasHiddenParent,
106
106
  ...hasParent,
107
107
  ...hasAttribute,
108
108
  ...getFocusableElements,
109
109
  ...isFocusable,
110
110
  ...getComputedRole,
111
- ...getImageText,
111
+ getImageText,
112
112
  ...testContrast,
113
113
  ...testLang,
114
114
  ...testOrder,
@@ -1,20 +1,3 @@
1
- const interactiveRoles = [
2
- "button",
3
- "checkbox",
4
- "combobox",
5
- "link",
6
- "menu",
7
- "menuitemcheckbox",
8
- "menuitemradio",
9
- "radio",
10
- "scrollbar",
11
- "slider",
12
- "spinbutton",
13
- "tablist",
14
- "textbox",
15
- "toolbar",
16
- "switch",
17
- "tree",
18
- ];
1
+ const { INTERACTIVE_ROLES } = require('./constants.js');
19
2
 
20
- module.exports = interactiveRoles;
3
+ module.exports = INTERACTIVE_ROLES;
@@ -0,0 +1,95 @@
1
+ const { NON_VISIBLE_SELECTORS } = require('./constants.js');
2
+ const isHidden = require('./isHidden.js');
3
+
4
+ /**
5
+ * Checks if an element is visible to assistive technologies (AT).
6
+ *
7
+ * Unlike isHidden (which checks simple visual hiding), this function determines
8
+ * whether an element is meaningful to screen readers and other AT. Key differences:
9
+ *
10
+ * - Elements referenced by aria-labelledby or aria-describedby are considered
11
+ * AT-visible even if visually hidden (display:none), because they provide
12
+ * accessible names/descriptions to other elements.
13
+ * - Inherently non-visible elements (script, style, input[type="hidden"], etc.)
14
+ * are treated as AT-visible since they carry semantic meaning.
15
+ * - Parent chain is walked to detect inherited hiding (CSS and aria-hidden in strict mode).
16
+ *
17
+ * @param {Element} element - The element to check.
18
+ * @param {boolean} [strict=false] - When true, also treats aria-hidden="true" (on the
19
+ * element or any ancestor) as hidden.
20
+ * @returns {boolean} True if the element is visible to assistive technologies.
21
+ */
22
+ function isA11yVisible(element, strict = false) {
23
+ // Add null check at the beginning
24
+ if (!element || !(element instanceof Element)) {
25
+ return false;
26
+ }
27
+
28
+ // Check if element is still connected to the DOM
29
+ if (!element.isConnected) {
30
+ return false;
31
+ }
32
+
33
+ const id = element.id;
34
+ let visible = true;
35
+
36
+ if (NON_VISIBLE_SELECTORS.some(selector => element.matches(selector))) {
37
+ return true;
38
+ }
39
+
40
+ const optionalAriaHidden = (el, strictCheck) =>
41
+ strictCheck && el.getAttribute('aria-hidden') === 'true';
42
+
43
+ // Use isHidden for the element-level check (covers display:none,
44
+ // visibility:hidden, opacity:0, and the hidden HTML attribute)
45
+ if (isHidden(element, { checkOpacity: true })) {
46
+ visible = false;
47
+ } else {
48
+ // Walk parent chain for inherited CSS hiding (display, visibility, opacity)
49
+ let parent = element.parentElement;
50
+ while (parent) {
51
+ const style = window.getComputedStyle(parent);
52
+ if (
53
+ style.display === 'none' ||
54
+ style.visibility === 'hidden' ||
55
+ style.opacity === '0'
56
+ ) {
57
+ visible = false;
58
+ break;
59
+ }
60
+ parent = parent.parentElement;
61
+ }
62
+ }
63
+
64
+ // Check element-level aria-hidden in strict mode (when not already hidden by CSS)
65
+ if (visible && optionalAriaHidden(element, strict)) {
66
+ visible = false;
67
+ }
68
+
69
+ // Check if element is referenced by aria-labelledby or aria-describedby
70
+ document
71
+ .querySelectorAll(`*[aria-labelledby~="${id}"], *[aria-describedby~="${id}"]`)
72
+ .forEach(referencingElement => {
73
+ if (window.getComputedStyle(referencingElement).display !== 'none') {
74
+ visible = true;
75
+ }
76
+ });
77
+
78
+ // Check if any parent has aria-hidden="true" when strict mode is on
79
+ if (visible && strict) {
80
+ let parent = element.parentElement;
81
+ while (parent) {
82
+ if (optionalAriaHidden(parent, strict)) {
83
+ visible = false;
84
+ break;
85
+ }
86
+ parent = parent.parentElement;
87
+ }
88
+ }
89
+
90
+ return visible;
91
+ }
92
+
93
+ module.exports = {
94
+ isA11yVisible,
95
+ };
@@ -1,63 +1,4 @@
1
- /**
2
- * List of valid ARIA attributes based on WAI-ARIA specification.
3
- * @type {Set<string>}
4
- */
5
- const validAriaAttributes = new Set([
6
- // ARIA States and Properties
7
- 'aria-activedescendant',
8
- 'aria-atomic',
9
- 'aria-autocomplete',
10
- 'aria-braillelabel',
11
- 'aria-brailleroledescription',
12
- 'aria-busy',
13
- 'aria-checked',
14
- 'aria-colcount',
15
- 'aria-colindex',
16
- 'aria-colindextext',
17
- 'aria-colspan',
18
- 'aria-controls',
19
- 'aria-current',
20
- 'aria-describedby',
21
- 'aria-description',
22
- 'aria-details',
23
- 'aria-disabled',
24
- 'aria-dropeffect',
25
- 'aria-errormessage',
26
- 'aria-expanded',
27
- 'aria-flowto',
28
- 'aria-grabbed',
29
- 'aria-haspopup',
30
- 'aria-hidden',
31
- 'aria-invalid',
32
- 'aria-keyshortcuts',
33
- 'aria-label',
34
- 'aria-labelledby',
35
- 'aria-level',
36
- 'aria-live',
37
- 'aria-modal',
38
- 'aria-multiline',
39
- 'aria-multiselectable',
40
- 'aria-orientation',
41
- 'aria-owns',
42
- 'aria-placeholder',
43
- 'aria-posinset',
44
- 'aria-pressed',
45
- 'aria-readonly',
46
- 'aria-relevant',
47
- 'aria-required',
48
- 'aria-roledescription',
49
- 'aria-rowcount',
50
- 'aria-rowindex',
51
- 'aria-rowindextext',
52
- 'aria-rowspan',
53
- 'aria-selected',
54
- 'aria-setsize',
55
- 'aria-sort',
56
- 'aria-valuemax',
57
- 'aria-valuemin',
58
- 'aria-valuenow',
59
- 'aria-valuetext'
60
- ]);
1
+ const { VALID_ARIA_ATTRIBUTES } = require('./constants.js');
61
2
 
62
3
  /**
63
4
  * Checks if a specific ARIA attribute is valid according to the WAI-ARIA specification.
@@ -69,10 +10,10 @@ function isAriaAttributeValid(attributeName) {
69
10
  if (!attributeName || typeof attributeName !== 'string') {
70
11
  return false;
71
12
  }
72
-
73
- return validAriaAttributes.has(attributeName.toLowerCase());
13
+
14
+ return VALID_ARIA_ATTRIBUTES.has(attributeName.toLowerCase());
74
15
  }
75
16
 
76
17
  module.exports = {
77
- isAriaAttributeValid
78
- };
18
+ isAriaAttributeValid,
19
+ };