visually-contains.js 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. import { getNodeFromTree, getScroll } from '../../core/utils';
  2. /**
  3. * Return the ancestor node that is a scroll region.
  4. * @param {VirtualNode}
  5. * @return {VirtualNode|null}
  6. */
  7. function getScrollAncestor(node) {
  8. const vNode = getNodeFromTree(node);
  9. let ancestor = vNode.parent;
  10. while (ancestor) {
  11. if (getScroll(ancestor.actualNode)) {
  12. return ancestor.actualNode;
  13. }
  14. ancestor = ancestor.parent;
  15. }
  16. }
  17. /**
  18. * Checks whether a parent element visually contains its child, either directly or via scrolling.
  19. * Assumes that |parent| is an ancestor of |node|.
  20. * @param {Element} node
  21. * @param {Element} parent
  22. * @return {boolean} True if node is visually contained within parent
  23. */
  24. function contains(node, parent) {
  25. const rectBound = node.getBoundingClientRect();
  26. const margin = 0.01;
  27. const rect = {
  28. top: rectBound.top + margin,
  29. bottom: rectBound.bottom - margin,
  30. left: rectBound.left + margin,
  31. right: rectBound.right - margin
  32. };
  33. const parentRect = parent.getBoundingClientRect();
  34. const parentTop = parentRect.top;
  35. const parentLeft = parentRect.left;
  36. const parentScrollArea = {
  37. top: parentTop - parent.scrollTop,
  38. bottom: parentTop - parent.scrollTop + parent.scrollHeight,
  39. left: parentLeft - parent.scrollLeft,
  40. right: parentLeft - parent.scrollLeft + parent.scrollWidth
  41. };
  42. const style = window.getComputedStyle(parent);
  43. // if parent element is inline, scrollArea will be too unpredictable
  44. if (style.getPropertyValue('display') === 'inline') {
  45. return true;
  46. }
  47. //In theory, we should just be able to look at the scroll area as a superset of the parentRect,
  48. //but that's not true in Firefox
  49. if (
  50. (rect.left < parentScrollArea.left && rect.left < parentRect.left) ||
  51. (rect.top < parentScrollArea.top && rect.top < parentRect.top) ||
  52. (rect.right > parentScrollArea.right && rect.right > parentRect.right) ||
  53. (rect.bottom > parentScrollArea.bottom && rect.bottom > parentRect.bottom)
  54. ) {
  55. return false;
  56. }
  57. if (rect.right > parentRect.right || rect.bottom > parentRect.bottom) {
  58. return (
  59. style.overflow === 'scroll' ||
  60. style.overflow === 'auto' ||
  61. style.overflow === 'hidden' ||
  62. parent instanceof window.HTMLBodyElement ||
  63. parent instanceof window.HTMLHtmlElement
  64. );
  65. }
  66. return true;
  67. }
  68. /**
  69. * Checks whether a parent element visually contains its child, either directly or via scrolling.
  70. * Assumes that |parent| is an ancestor of |node|.
  71. * @method visuallyContains
  72. * @memberof axe.commons.dom
  73. * @instance
  74. * @param {Element} node
  75. * @param {Element} parent
  76. * @return {boolean} True if node is visually contained within parent
  77. */
  78. function visuallyContains(node, parent) {
  79. const parentScrollAncestor = getScrollAncestor(parent);
  80. // if the elements share a common scroll parent, we can check if the
  81. // parent visually contains the node. otherwise we need to check each
  82. // scroll parent in between the node and the parent since if the
  83. // element is off screen due to the scroll, it won't be visually contained
  84. // by the parent
  85. do {
  86. const nextScrollAncestor = getScrollAncestor(node);
  87. if (
  88. nextScrollAncestor === parentScrollAncestor ||
  89. nextScrollAncestor === parent
  90. ) {
  91. return contains(node, parent);
  92. }
  93. node = nextScrollAncestor;
  94. } while (node);
  95. return false;
  96. }
  97. export default visuallyContains;