region-evaluate.js 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. import * as dom from '../../commons/dom';
  2. import * as aria from '../../commons/aria';
  3. import * as standards from '../../commons/standards';
  4. import matches from '../../commons/matches';
  5. import cache from '../../core/base/cache';
  6. const landmarkRoles = standards.getAriaRolesByType('landmark');
  7. const implicitAriaLiveRoles = ['alert', 'log', 'status'];
  8. // Check if the current element is a landmark
  9. function isRegion(virtualNode, options) {
  10. const node = virtualNode.actualNode;
  11. const role = aria.getRole(virtualNode);
  12. const ariaLive = (node.getAttribute('aria-live') || '').toLowerCase().trim();
  13. // Ignore content inside of aria-live
  14. if (
  15. ['assertive', 'polite'].includes(ariaLive) ||
  16. implicitAriaLiveRoles.includes(role)
  17. ) {
  18. return true;
  19. }
  20. // Check if the node matches a landmark role
  21. if (landmarkRoles.includes(role)) {
  22. return true;
  23. }
  24. // Check if node matches an option
  25. if (options.regionMatcher && matches(virtualNode, options.regionMatcher)) {
  26. return true;
  27. }
  28. return false;
  29. }
  30. /**
  31. * Find all visible elements not wrapped inside a landmark or skiplink
  32. */
  33. function findRegionlessElms(virtualNode, options) {
  34. const node = virtualNode.actualNode;
  35. // End recursion if the element is a landmark, skiplink, or hidden content
  36. if (
  37. isRegion(virtualNode, options) ||
  38. (dom.isSkipLink(virtualNode.actualNode) &&
  39. dom.getElementByReference(virtualNode.actualNode, 'href')) ||
  40. !dom.isVisible(node, true)
  41. ) {
  42. // Mark each parent node as having region descendant
  43. let vNode = virtualNode;
  44. while (vNode) {
  45. vNode._hasRegionDescendant = true;
  46. vNode = vNode.parent;
  47. }
  48. return [];
  49. // Return the node is a content element. Ignore any direct text children
  50. // of body so we don't report body as being outside of a landmark.
  51. // @see https://github.com/dequelabs/axe-core/issues/2049
  52. } else if (
  53. node !== document.body &&
  54. dom.hasContent(node, /* noRecursion: */ true)
  55. ) {
  56. return [virtualNode];
  57. // Recursively look at all child elements
  58. } else {
  59. return virtualNode.children
  60. .filter(({ actualNode }) => actualNode.nodeType === 1)
  61. .map(vNode => findRegionlessElms(vNode, options))
  62. .reduce((a, b) => a.concat(b), []); // flatten the results
  63. }
  64. }
  65. function regionEvaluate(node, options, virtualNode) {
  66. let regionlessNodes = cache.get('regionlessNodes');
  67. if (regionlessNodes) {
  68. return !regionlessNodes.includes(virtualNode);
  69. }
  70. const tree = axe._tree;
  71. regionlessNodes = findRegionlessElms(tree[0], options)
  72. // Find first parent marked as having region descendant (or body) and
  73. // return the node right before it as the "outer" element
  74. .map(vNode => {
  75. while (
  76. vNode.parent &&
  77. !vNode.parent._hasRegionDescendant &&
  78. vNode.parent.actualNode !== document.body
  79. ) {
  80. vNode = vNode.parent;
  81. }
  82. return vNode;
  83. })
  84. // Remove duplicate containers
  85. .filter((vNode, index, array) => {
  86. return array.indexOf(vNode) === index;
  87. });
  88. cache.set('regionlessNodes', regionlessNodes);
  89. return !regionlessNodes.includes(virtualNode);
  90. }
  91. export default regionEvaluate;