govuk_publishing_components 21.22.1 → 21.22.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  3. data/app/assets/stylesheets/govuk_publishing_components/components/_breadcrumbs.scss +34 -0
  4. data/app/assets/stylesheets/govuk_publishing_components/components/_document-list.scss +2 -2
  5. data/app/views/govuk_publishing_components/components/_breadcrumbs.html.erb +0 -3
  6. data/app/views/govuk_publishing_components/components/_contextual_breadcrumbs.html.erb +0 -2
  7. data/lib/govuk_publishing_components/version.rb +1 -1
  8. data/node_modules/axe-core/CHANGELOG.md +70 -10
  9. data/node_modules/axe-core/axe.js +1295 -632
  10. data/node_modules/axe-core/axe.min.js +2 -2
  11. data/node_modules/axe-core/bower.json +1 -1
  12. data/node_modules/axe-core/doc/API.md +1 -0
  13. data/node_modules/axe-core/doc/aria-supported.md +0 -1
  14. data/node_modules/axe-core/doc/developer-guide.md +2 -2
  15. data/node_modules/axe-core/doc/rule-descriptions.md +91 -87
  16. data/node_modules/axe-core/lib/checks/aria/abstractrole.js +10 -1
  17. data/node_modules/axe-core/lib/checks/aria/abstractrole.json +4 -1
  18. data/node_modules/axe-core/lib/checks/aria/fallbackrole.js +1 -0
  19. data/node_modules/axe-core/lib/checks/aria/fallbackrole.json +11 -0
  20. data/node_modules/axe-core/lib/checks/aria/invalidrole.js +14 -3
  21. data/node_modules/axe-core/lib/checks/aria/invalidrole.json +4 -1
  22. data/node_modules/axe-core/lib/checks/aria/required-children.js +41 -30
  23. data/node_modules/axe-core/lib/checks/aria/valid-attr-value.js +24 -9
  24. data/node_modules/axe-core/lib/checks/aria/valid-attr-value.json +2 -2
  25. data/node_modules/axe-core/lib/checks/color/color-contrast.js +23 -7
  26. data/node_modules/axe-core/lib/checks/color/color-contrast.json +7 -1
  27. data/node_modules/axe-core/lib/checks/keyboard/focusable-disabled.js +5 -0
  28. data/node_modules/axe-core/lib/checks/keyboard/focusable-modal-open.js +14 -0
  29. data/node_modules/axe-core/lib/checks/keyboard/focusable-modal-open.json +11 -0
  30. data/node_modules/axe-core/lib/checks/keyboard/focusable-not-tabbable.js +5 -0
  31. data/node_modules/axe-core/lib/checks/keyboard/page-has-elm.js +5 -1
  32. data/node_modules/axe-core/lib/checks/keyboard/page-no-duplicate-after.js +2 -0
  33. data/node_modules/axe-core/lib/checks/keyboard/page-no-duplicate-banner.json +1 -0
  34. data/node_modules/axe-core/lib/checks/keyboard/page-no-duplicate-contentinfo.json +1 -0
  35. data/node_modules/axe-core/lib/checks/keyboard/page-no-duplicate-main.json +1 -0
  36. data/node_modules/axe-core/lib/checks/keyboard/page-no-duplicate.js +14 -2
  37. data/node_modules/axe-core/lib/checks/lists/only-listitems.js +43 -49
  38. data/node_modules/axe-core/lib/checks/lists/only-listitems.json +4 -1
  39. data/node_modules/axe-core/lib/checks/media/no-autoplay-audio.js +93 -0
  40. data/node_modules/axe-core/lib/checks/media/no-autoplay-audio.json +15 -0
  41. data/node_modules/axe-core/lib/checks/navigation/identical-links-same-purpose-after.js +100 -0
  42. data/node_modules/axe-core/lib/checks/navigation/identical-links-same-purpose.js +31 -0
  43. data/node_modules/axe-core/lib/checks/navigation/identical-links-same-purpose.json +12 -0
  44. data/node_modules/axe-core/lib/checks/navigation/region.js +42 -8
  45. data/node_modules/axe-core/lib/checks/navigation/region.json +0 -1
  46. data/node_modules/axe-core/lib/checks/shared/svg-non-empty-title.js +4 -0
  47. data/node_modules/axe-core/lib/checks/shared/svg-non-empty-title.json +11 -0
  48. data/node_modules/axe-core/lib/commons/aria/index.js +12 -4
  49. data/node_modules/axe-core/lib/commons/color/get-background-color.js +5 -157
  50. data/node_modules/axe-core/lib/commons/dom/get-element-stack.js +272 -174
  51. data/node_modules/axe-core/lib/commons/dom/is-focusable.js +3 -1
  52. data/node_modules/axe-core/lib/commons/dom/is-hidden-with-css.js +2 -2
  53. data/node_modules/axe-core/lib/commons/dom/is-modal-open.js +98 -0
  54. data/node_modules/axe-core/lib/commons/dom/is-visible.js +57 -2
  55. data/node_modules/axe-core/lib/commons/dom/url-props-from-attribute.js +143 -0
  56. data/node_modules/axe-core/lib/commons/dom/visually-contains.js +62 -12
  57. data/node_modules/axe-core/lib/commons/matches/attributes.js +12 -8
  58. data/node_modules/axe-core/lib/commons/matches/from-definition.js +15 -10
  59. data/node_modules/axe-core/lib/commons/matches/index.js +11 -9
  60. data/node_modules/axe-core/lib/commons/matches/node-name.js +11 -21
  61. data/node_modules/axe-core/lib/commons/matches/properties.js +12 -9
  62. data/node_modules/axe-core/lib/commons/text/unicode.js +27 -24
  63. data/node_modules/axe-core/lib/core/base/virtual-node/virtual-node.js +11 -3
  64. data/node_modules/axe-core/lib/core/constants.js +1 -1
  65. data/node_modules/axe-core/lib/core/reporters/v1.js +6 -3
  66. data/node_modules/axe-core/lib/core/utils/contains.js +1 -1
  67. data/node_modules/axe-core/lib/core/utils/is-hidden.js +6 -6
  68. data/node_modules/axe-core/lib/core/utils/matches.js +263 -0
  69. data/node_modules/axe-core/lib/core/utils/preload-media.js +65 -0
  70. data/node_modules/axe-core/lib/core/utils/preload.js +33 -24
  71. data/node_modules/axe-core/lib/core/utils/qsa.js +7 -208
  72. data/node_modules/axe-core/lib/rules/aria-hidden-focus.json +5 -1
  73. data/node_modules/axe-core/lib/rules/aria-roles.json +1 -1
  74. data/node_modules/axe-core/lib/rules/button-name.json +1 -1
  75. data/node_modules/axe-core/lib/rules/color-contrast-matches.js +1 -1
  76. data/node_modules/axe-core/lib/rules/color-contrast.json +0 -3
  77. data/node_modules/axe-core/lib/rules/html-namespace-matches.js +1 -0
  78. data/node_modules/axe-core/lib/rules/identical-links-same-purpose-matches.js +13 -0
  79. data/node_modules/axe-core/lib/rules/identical-links-same-purpose.json +14 -0
  80. data/node_modules/axe-core/lib/rules/landmark-no-duplicate-banner.json +1 -1
  81. data/node_modules/axe-core/lib/rules/landmark-no-duplicate-contentinfo.json +1 -1
  82. data/node_modules/axe-core/lib/rules/landmark-no-duplicate-main.json +12 -0
  83. data/node_modules/axe-core/lib/rules/landmark-one-main.json +2 -2
  84. data/node_modules/axe-core/lib/rules/link-name.json +2 -4
  85. data/node_modules/axe-core/lib/rules/meta-viewport.json +1 -1
  86. data/node_modules/axe-core/lib/rules/no-autoplay-audio-matches.js +18 -0
  87. data/node_modules/axe-core/lib/rules/no-autoplay-audio.json +15 -0
  88. data/node_modules/axe-core/lib/rules/region.json +1 -2
  89. data/node_modules/axe-core/lib/rules/role-img-alt.json +2 -1
  90. data/node_modules/axe-core/lib/rules/svg-img-alt.json +24 -0
  91. data/node_modules/axe-core/lib/rules/svg-namespace-matches.js +1 -0
  92. data/node_modules/axe-core/locales/da.json +782 -0
  93. data/node_modules/axe-core/locales/fr.json +221 -42
  94. data/node_modules/axe-core/locales/ja.json +124 -24
  95. data/node_modules/axe-core/package.json +29 -26
  96. data/node_modules/axe-core/sri-history.json +26 -10
  97. metadata +27 -9
  98. data/node_modules/axe-core/doc/examples/jasmine/package-lock.json +0 -1489
  99. data/node_modules/axe-core/doc/examples/jest_react/package-lock.json +0 -7525
  100. data/node_modules/axe-core/doc/examples/mocha/package-lock.json +0 -1671
  101. data/node_modules/axe-core/doc/examples/phantomjs/package-lock.json +0 -862
  102. data/node_modules/axe-core/doc/examples/qunit/package-lock.json +0 -2951
  103. data/node_modules/axe-core/lib/checks/navigation/region-after.js +0 -1
  104. data/node_modules/axe-core/typings/axe-core/axe-core-tests.js +0 -151
@@ -65,9 +65,11 @@ dom.isNativelyFocusable = function(el) {
65
65
  return el.type !== 'hidden';
66
66
  case 'TEXTAREA':
67
67
  case 'SELECT':
68
- case 'DETAILS':
68
+ case 'SUMMARY':
69
69
  case 'BUTTON':
70
70
  return true;
71
+ case 'DETAILS':
72
+ return !el.querySelector('summary');
71
73
  }
72
74
  return false;
73
75
  };
@@ -59,8 +59,8 @@ function _isHiddenWithCSS(el, descendentVisibilityValue) {
59
59
 
60
60
  if (
61
61
  HIDDEN_VISIBILITY_VALUES.includes(visibilityValue) &&
62
- (descendentVisibilityValue &&
63
- HIDDEN_VISIBILITY_VALUES.includes(descendentVisibilityValue))
62
+ descendentVisibilityValue &&
63
+ HIDDEN_VISIBILITY_VALUES.includes(descendentVisibilityValue)
64
64
  ) {
65
65
  return true;
66
66
  }
@@ -0,0 +1,98 @@
1
+ /* global dom, axe */
2
+
3
+ /**
4
+ * Determines if there is a modal currently open.
5
+ * @method isModalOpen
6
+ * @memberof axe.commons.dom
7
+ * @instance
8
+ * @return {Boolean|undefined} True if we know (or our best guess) that a modal is open, undefined if we can't tell (doesn't mean there isn't one open)
9
+ */
10
+ dom.isModalOpen = function isModalOpen(options) {
11
+ options = options || {};
12
+ let modalPercent = options.modalPercent || 0.75;
13
+
14
+ // there is no "definitive" way to code a modal so detecting when one is open
15
+ // is a bit of a guess. a modal won't always be accessible, so we can't rely
16
+ // on the `role` attribute, and relying on a class name as a convention is
17
+ // unreliable. we also cannot rely on the body/html not scrolling.
18
+ //
19
+ // because of this, we will look for two different types of modals:
20
+ // "definitely a modal" and "could be a modal."
21
+ //
22
+ // "definitely a modal" is any visible element that is coded to be a modal
23
+ // by using one of the following criteria:
24
+ //
25
+ // - has the attribute `role=dialog`
26
+ // - has the attribute `aria-modal=true`
27
+ // - is the dialog element
28
+ //
29
+ // "could be a modal" is a visible element that takes up more than 75% of
30
+ // the screen (though typically full width/height) and is the top-most element
31
+ // in the viewport. since we aren't sure if it is or is not a modal this is
32
+ // just our best guess of being one based on convention.
33
+
34
+ if (axe._cache.get('isModalOpen')) {
35
+ return axe._cache.get('isModalOpen');
36
+ }
37
+
38
+ const definiteModals = axe.utils.querySelectorAllFilter(
39
+ axe._tree[0],
40
+ 'dialog, [role=dialog], [aria-modal=true]',
41
+ vNode => dom.isVisible(vNode.actualNode)
42
+ );
43
+
44
+ if (definiteModals.length) {
45
+ axe._cache.set('isModalOpen', true);
46
+ return true;
47
+ }
48
+
49
+ // to find a "could be a modal" we will take the element stack from each of
50
+ // four corners and one from the middle of the viewport (total of 5). if each
51
+ // stack contains an element whose width/height is >= 75% of the screen, we
52
+ // found a "could be a modal"
53
+ const viewport = dom.getViewportSize(window);
54
+ const percentWidth = viewport.width * modalPercent;
55
+ const percentHeight = viewport.height * modalPercent;
56
+ const x = (viewport.width - percentWidth) / 2;
57
+ const y = (viewport.height - percentHeight) / 2;
58
+
59
+ const points = [
60
+ // top-left corner
61
+ { x, y },
62
+ // top-right corner
63
+ { x: viewport.width - x, y },
64
+ // center
65
+ { x: viewport.width / 2, y: viewport.height / 2 },
66
+ // bottom-left corner
67
+ { x, y: viewport.height - y },
68
+ // bottom-right corner
69
+ { x: viewport.width - x, y: viewport.height - y }
70
+ ];
71
+
72
+ const stacks = points.map(point => {
73
+ return Array.from(document.elementsFromPoint(point.x, point.y));
74
+ });
75
+
76
+ for (let i = 0; i < stacks.length; i++) {
77
+ // a modal isn't guaranteed to be the top most element so we'll have to
78
+ // find the first element in the stack that meets the modal criteria
79
+ // and make sure it's in the other stacks
80
+ const modalElement = stacks[i].find(elm => {
81
+ const style = window.getComputedStyle(elm);
82
+ return (
83
+ parseInt(style.width, 10) >= percentWidth &&
84
+ parseInt(style.height, 10) >= percentHeight &&
85
+ style.getPropertyValue('pointer-events') !== 'none' &&
86
+ (style.position === 'absolute' || style.position === 'fixed')
87
+ );
88
+ });
89
+
90
+ if (modalElement && stacks.every(stack => stack.includes(modalElement))) {
91
+ axe._cache.set('isModalOpen', true);
92
+ return true;
93
+ }
94
+ }
95
+
96
+ axe._cache.set('isModalOpen', undefined);
97
+ return undefined;
98
+ };
@@ -39,6 +39,50 @@ function isClipped(style) {
39
39
  return false;
40
40
  }
41
41
 
42
+ /**
43
+ * Check `AREA` element is visible
44
+ * - validate if it is a child of `map`
45
+ * - ensure `map` is referred by `img` using the `usemap` attribute
46
+ * @param {Element} areaEl `AREA` element
47
+ * @retruns {Boolean}
48
+ */
49
+ function isAreaVisible(el, screenReader, recursed) {
50
+ /**
51
+ * Note:
52
+ * - Verified that `map` element cannot refer to `area` elements across different document trees
53
+ * - Verified that `map` element does not get affected by altering `display` property
54
+ */
55
+ const mapEl = dom.findUp(el, 'map');
56
+ if (!mapEl) {
57
+ return false;
58
+ }
59
+
60
+ const mapElName = mapEl.getAttribute('name');
61
+ if (!mapElName) {
62
+ return false;
63
+ }
64
+
65
+ /**
66
+ * `map` element has to be in light DOM
67
+ */
68
+ const mapElRootNode = dom.getRootNode(el);
69
+ if (!mapElRootNode || mapElRootNode.nodeType !== 9) {
70
+ return false;
71
+ }
72
+
73
+ const refs = axe.utils.querySelectorAll(
74
+ axe._tree,
75
+ `img[usemap="#${axe.utils.escapeSelector(mapElName)}"]`
76
+ );
77
+ if (!refs || !refs.length) {
78
+ return false;
79
+ }
80
+
81
+ return refs.some(({ actualNode }) =>
82
+ dom.isVisible(actualNode, screenReader, recursed)
83
+ );
84
+ }
85
+
42
86
  /**
43
87
  * Determine whether an element is visible
44
88
  * @method isVisible
@@ -74,9 +118,13 @@ dom.isVisible = function(el, screenReader, recursed) {
74
118
  }
75
119
 
76
120
  const nodeName = el.nodeName.toUpperCase();
77
-
78
121
  if (
79
- style.getPropertyValue('display') === 'none' ||
122
+ /**
123
+ * Note:
124
+ * Firefox's user-agent always sets `AREA` element to `display:none`
125
+ * hence excluding the edge case, for visibility computation
126
+ */
127
+ (nodeName !== 'AREA' && style.getPropertyValue('display') === 'none') ||
80
128
  ['STYLE', 'SCRIPT', 'NOSCRIPT', 'TEMPLATE'].includes(nodeName) ||
81
129
  (!screenReader && isClipped(style)) ||
82
130
  (!recursed &&
@@ -89,6 +137,13 @@ dom.isVisible = function(el, screenReader, recursed) {
89
137
  return false;
90
138
  }
91
139
 
140
+ /**
141
+ * check visibility of `AREA`
142
+ */
143
+ if (nodeName === 'AREA') {
144
+ return isAreaVisible(el, screenReader, recursed);
145
+ }
146
+
92
147
  const parent = el.assignedSlot ? el.assignedSlot : el.parentNode;
93
148
  let isVisible = false;
94
149
  if (parent) {
@@ -0,0 +1,143 @@
1
+ /* global dom */
2
+
3
+ /**
4
+ * Parse resource object for a given node from a specified attribute
5
+ * @method urlPropsFromAttribute
6
+ * @param {HTMLElement} node given node
7
+ * @param {String} attribute attribute of the node from which resource should be parsed
8
+ * @returns {Object}
9
+ */
10
+ dom.urlPropsFromAttribute = function urlPropsFromAttribute(node, attribute) {
11
+ const value = node[attribute];
12
+ if (!value) {
13
+ return undefined;
14
+ }
15
+
16
+ const nodeName = node.nodeName.toUpperCase();
17
+ let parser = node;
18
+
19
+ /**
20
+ * Note:
21
+ * The need to create a parser, is to keep this function generic, to be able to parse resource from element like `iframe` with `src` attribute
22
+ */
23
+ if (!['A', 'AREA'].includes(nodeName)) {
24
+ parser = document.createElement('a');
25
+ parser.href = value;
26
+ }
27
+
28
+ /**
29
+ * Curate `https` and `ftps` to `http` and `ftp` as they will resolve to same resource
30
+ */
31
+ const protocol = [`https:`, `ftps:`].includes(parser.protocol)
32
+ ? parser.protocol.replace(/s:$/, ':')
33
+ : parser.protocol;
34
+
35
+ const { pathname, filename } = getPathnameOrFilename(parser.pathname);
36
+
37
+ return {
38
+ protocol,
39
+ hostname: parser.hostname,
40
+ port: getPort(parser.port),
41
+ pathname: /\/$/.test(pathname) ? pathname : `${pathname}/`,
42
+ search: getSearchPairs(parser.search),
43
+ hash: getHashRoute(parser.hash),
44
+ filename
45
+ };
46
+ };
47
+
48
+ /**
49
+ * Resolve given port excluding default port(s)
50
+ * @param {String} port port
51
+ * @returns {String}
52
+ */
53
+ function getPort(port) {
54
+ const excludePorts = [
55
+ `443`, // default `https` port
56
+ `80`
57
+ ];
58
+ return !excludePorts.includes(port) ? port : ``;
59
+ }
60
+
61
+ /**
62
+ * Resolve if a given pathname has filename & resolve the same as parts
63
+ * @method getPathnameOrFilename
64
+ * @param {String} pathname pathname part of a given uri
65
+ * @returns {Array<Object>}
66
+ */
67
+ function getPathnameOrFilename(pathname) {
68
+ const filename = pathname.split('/').pop();
69
+ if (!filename || filename.indexOf('.') === -1) {
70
+ return {
71
+ pathname,
72
+ filename: ``
73
+ };
74
+ }
75
+
76
+ return {
77
+ // remove `filename` from `pathname`
78
+ pathname: pathname.replace(filename, ''),
79
+
80
+ // ignore filename when index.*
81
+ filename: /index./.test(filename) ? `` : filename
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Parse a given query string to key/value pairs sorted alphabetically
87
+ * @param {String} searchStr search string
88
+ * @returns {Object}
89
+ */
90
+ function getSearchPairs(searchStr) {
91
+ const query = {};
92
+
93
+ if (!searchStr || !searchStr.length) {
94
+ return query;
95
+ }
96
+
97
+ // `substring` to remove `?` at the beginning of search string
98
+ const pairs = searchStr.substring(1).split(`&`);
99
+ if (!pairs || !pairs.length) {
100
+ return query;
101
+ }
102
+
103
+ for (let index = 0; index < pairs.length; index++) {
104
+ const pair = pairs[index];
105
+ const [key, value = ''] = pair.split(`=`);
106
+ query[decodeURIComponent(key)] = decodeURIComponent(value);
107
+ }
108
+
109
+ return query;
110
+ }
111
+
112
+ /**
113
+ * Interpret a given hash
114
+ * if `hash`
115
+ * -> is `hashbang` -or- `hash` is followed by `slash`
116
+ * -> it resolves to a different resource
117
+ * @method getHashRoute
118
+ * @param {String} hash hash component of a parsed uri
119
+ * @returns {String}
120
+ */
121
+ function getHashRoute(hash) {
122
+ if (!hash) {
123
+ return ``;
124
+ }
125
+
126
+ /**
127
+ * Check for any conventionally-formatted hashbang that may be present
128
+ * eg: `#, #/, #!, #!/`
129
+ */
130
+ const hashRegex = /#!?\/?/g;
131
+ const hasMatch = hash.match(hashRegex);
132
+ if (!hasMatch) {
133
+ return ``;
134
+ }
135
+
136
+ // do not resolve inline link as hash
137
+ const [matchedStr] = hasMatch;
138
+ if (matchedStr === '#') {
139
+ return ``;
140
+ }
141
+
142
+ return hash;
143
+ }
@@ -1,35 +1,51 @@
1
1
  /* global dom */
2
2
 
3
+ /**
4
+ * Return the ancestor node that is a scroll region.
5
+ * @param {VirtualNode}
6
+ * @return {VirtualNode|null}
7
+ */
8
+ function getScrollAncestor(node) {
9
+ const vNode = axe.utils.getNodeFromTree(node);
10
+ let ancestor = vNode.parent;
11
+
12
+ while (ancestor) {
13
+ if (axe.utils.getScroll(ancestor.actualNode)) {
14
+ return ancestor.actualNode;
15
+ }
16
+
17
+ ancestor = ancestor.parent;
18
+ }
19
+ }
20
+
3
21
  /**
4
22
  * Checks whether a parent element visually contains its child, either directly or via scrolling.
5
23
  * Assumes that |parent| is an ancestor of |node|.
6
- * @method visuallyContains
7
- * @memberof axe.commons.dom
8
- * @instance
9
24
  * @param {Element} node
10
25
  * @param {Element} parent
11
26
  * @return {boolean} True if node is visually contained within parent
12
27
  */
13
- dom.visuallyContains = function(node, parent) {
14
- var rectBound = node.getBoundingClientRect();
15
- var margin = 0.01;
16
- var rect = {
28
+ function contains(node, parent) {
29
+ const rectBound = node.getBoundingClientRect();
30
+ const margin = 0.01;
31
+ const rect = {
17
32
  top: rectBound.top + margin,
18
33
  bottom: rectBound.bottom - margin,
19
34
  left: rectBound.left + margin,
20
35
  right: rectBound.right - margin
21
36
  };
22
- var parentRect = parent.getBoundingClientRect();
23
- var parentTop = parentRect.top;
24
- var parentLeft = parentRect.left;
25
- var parentScrollArea = {
37
+
38
+ const parentRect = parent.getBoundingClientRect();
39
+ const parentTop = parentRect.top;
40
+ const parentLeft = parentRect.left;
41
+ const parentScrollArea = {
26
42
  top: parentTop - parent.scrollTop,
27
43
  bottom: parentTop - parent.scrollTop + parent.scrollHeight,
28
44
  left: parentLeft - parent.scrollLeft,
29
45
  right: parentLeft - parent.scrollLeft + parent.scrollWidth
30
46
  };
31
47
 
32
- var style = window.getComputedStyle(parent);
48
+ const style = window.getComputedStyle(parent);
33
49
 
34
50
  // if parent element is inline, scrollArea will be too unpredictable
35
51
  if (style.getPropertyValue('display') === 'inline') {
@@ -58,4 +74,38 @@ dom.visuallyContains = function(node, parent) {
58
74
  }
59
75
 
60
76
  return true;
77
+ }
78
+
79
+ /**
80
+ * Checks whether a parent element visually contains its child, either directly or via scrolling.
81
+ * Assumes that |parent| is an ancestor of |node|.
82
+ * @method visuallyContains
83
+ * @memberof axe.commons.dom
84
+ * @instance
85
+ * @param {Element} node
86
+ * @param {Element} parent
87
+ * @return {boolean} True if node is visually contained within parent
88
+ */
89
+ dom.visuallyContains = function visuallyContains(node, parent) {
90
+ const parentScrollAncestor = getScrollAncestor(parent);
91
+
92
+ // if the elements share a common scroll parent, we can check if the
93
+ // parent visually contains the node. otherwise we need to check each
94
+ // scroll parent in between the node and the parent since if the
95
+ // element is off screen due to the scroll, it won't be visually contained
96
+ // by the parent
97
+ do {
98
+ const nextScrollAncestor = getScrollAncestor(node);
99
+
100
+ if (
101
+ nextScrollAncestor === parentScrollAncestor ||
102
+ nextScrollAncestor === parent
103
+ ) {
104
+ return contains(node, parent);
105
+ }
106
+
107
+ node = nextScrollAncestor;
108
+ } while (node);
109
+
110
+ return false;
61
111
  };