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
@@ -1,7 +1,7 @@
1
- /* global dom */
1
+ /* global dom, VirtualNode */
2
2
 
3
3
  // split the page cells to group elements by the position
4
- const gridSize = 200; // arbitrary size, increase to reduce memory (less cells) use but increase time (more nodes per grid to check collision)
4
+ const gridSize = 200; // arbitrary size, increase to reduce memory use (less cells) but increase time (more nodes per grid to check collision)
5
5
 
6
6
  /**
7
7
  * Determine if node produces a stacking context.
@@ -11,31 +11,20 @@ const gridSize = 200; // arbitrary size, increase to reduce memory (less cells)
11
11
  * @param {VirtualNode} vNode
12
12
  * @return {Boolean}
13
13
  */
14
- function isStackingContext(vNode) {
15
- const node = vNode.actualNode;
14
+ function isStackingContext(vNode, parentVNode) {
15
+ const position = vNode.getComputedStylePropertyValue('position');
16
+ const zIndex = vNode.getComputedStylePropertyValue('z-index');
16
17
 
17
- //the root element (HTML)
18
- if (
19
- !node ||
20
- node.nodeName === 'HTML' ||
21
- node.nodeName === '#document-fragment'
22
- ) {
23
- return true;
24
- }
18
+ // the root element (HTML) is skipped since we always start with a
19
+ // stacking order of [0]
25
20
 
26
21
  // position: fixed or sticky
27
- if (
28
- vNode.getComputedStylePropertyValue('position') === 'fixed' ||
29
- vNode.getComputedStylePropertyValue('position') === 'sticky'
30
- ) {
22
+ if (position === 'fixed' || position === 'sticky') {
31
23
  return true;
32
24
  }
33
25
 
34
26
  // positioned (absolutely or relatively) with a z-index value other than "auto",
35
- if (
36
- vNode.getComputedStylePropertyValue('z-index') !== 'auto' &&
37
- vNode.getComputedStylePropertyValue('position') !== 'static'
38
- ) {
27
+ if (zIndex !== 'auto' && position !== 'static') {
39
28
  return true;
40
29
  }
41
30
 
@@ -56,34 +45,26 @@ function isStackingContext(vNode) {
56
45
  }
57
46
 
58
47
  // elements with a mix-blend-mode value other than "normal"
59
- if (
60
- vNode.getComputedStylePropertyValue('mix-blend-mode') &&
61
- vNode.getComputedStylePropertyValue('mix-blend-mode') !== 'normal'
62
- ) {
48
+ const mixBlendMode = vNode.getComputedStylePropertyValue('mix-blend-mode');
49
+ if (mixBlendMode && mixBlendMode !== 'normal') {
63
50
  return true;
64
51
  }
65
52
 
66
53
  // elements with a filter value other than "none"
67
- if (
68
- vNode.getComputedStylePropertyValue('filter') &&
69
- vNode.getComputedStylePropertyValue('filter') !== 'none'
70
- ) {
54
+ const filter = vNode.getComputedStylePropertyValue('filter');
55
+ if (filter && filter !== 'none') {
71
56
  return true;
72
57
  }
73
58
 
74
59
  // elements with a perspective value other than "none"
75
- if (
76
- vNode.getComputedStylePropertyValue('perspective') &&
77
- vNode.getComputedStylePropertyValue('perspective') !== 'none'
78
- ) {
60
+ const perspective = vNode.getComputedStylePropertyValue('perspective');
61
+ if (perspective && perspective !== 'none') {
79
62
  return true;
80
63
  }
81
64
 
82
65
  // element with a clip-path value other than "none"
83
- if (
84
- vNode.getComputedStylePropertyValue('clip-path') &&
85
- vNode.getComputedStylePropertyValue('clip-path') !== 'none'
86
- ) {
66
+ const clipPath = vNode.getComputedStylePropertyValue('clip-path');
67
+ if (clipPath && clipPath !== 'none') {
87
68
  return true;
88
69
  }
89
70
 
@@ -92,7 +73,6 @@ function isStackingContext(vNode) {
92
73
  vNode.getComputedStylePropertyValue('-webkit-mask') ||
93
74
  vNode.getComputedStylePropertyValue('mask') ||
94
75
  'none';
95
-
96
76
  if (mask !== 'none') {
97
77
  return true;
98
78
  }
@@ -102,7 +82,6 @@ function isStackingContext(vNode) {
102
82
  vNode.getComputedStylePropertyValue('-webkit-mask-image') ||
103
83
  vNode.getComputedStylePropertyValue('mask-image') ||
104
84
  'none';
105
-
106
85
  if (maskImage !== 'none') {
107
86
  return true;
108
87
  }
@@ -112,7 +91,6 @@ function isStackingContext(vNode) {
112
91
  vNode.getComputedStylePropertyValue('-webkit-mask-border') ||
113
92
  vNode.getComputedStylePropertyValue('mask-border') ||
114
93
  'none';
115
-
116
94
  if (maskBorder !== 'none') {
117
95
  return true;
118
96
  }
@@ -123,10 +101,8 @@ function isStackingContext(vNode) {
123
101
  }
124
102
 
125
103
  // transform or opacity in will-change even if you don't specify values for these attributes directly
126
- if (
127
- vNode.getComputedStylePropertyValue('will-change') === 'transform' ||
128
- vNode.getComputedStylePropertyValue('will-change') === 'opacity'
129
- ) {
104
+ const willChange = vNode.getComputedStylePropertyValue('will-change');
105
+ if (willChange === 'transform' || willChange === 'opacity') {
130
106
  return true;
131
107
  }
132
108
 
@@ -146,11 +122,8 @@ function isStackingContext(vNode) {
146
122
  }
147
123
 
148
124
  // a flex item or gird item with a z-index value other than "auto", that is the parent element display: flex|inline-flex|grid|inline-grid,
149
- if (
150
- vNode.getComputedStylePropertyValue('z-index') !== 'auto' &&
151
- vNode.parent
152
- ) {
153
- const parentDsiplay = vNode.parent.getComputedStylePropertyValue('display');
125
+ if (zIndex !== 'auto' && parentVNode) {
126
+ const parentDsiplay = parentVNode.getComputedStylePropertyValue('display');
154
127
  if (
155
128
  [
156
129
  'flex',
@@ -192,9 +165,7 @@ function getPositionOrder(vNode) {
192
165
  }
193
166
 
194
167
  // 3. the in-flow, non-inline-level, non-positioned descendants.
195
- if (vNode.getComputedStylePropertyValue('position') === 'static') {
196
- return 0;
197
- }
168
+ return 0;
198
169
  }
199
170
 
200
171
  // 6. the child stacking contexts with stack level 0 and the positioned descendants with stack level 0.
@@ -210,15 +181,6 @@ function getPositionOrder(vNode) {
210
181
  */
211
182
  function visuallySort(a, b) {
212
183
  /*eslint no-bitwise: 0 */
213
-
214
- // 1. The root element forms the root stacking context.
215
- if (a.actualNode.nodeName.toLowerCase() === 'html') {
216
- return 1;
217
- }
218
- if (b.actualNode.nodeName.toLowerCase() === 'html') {
219
- return -1;
220
- }
221
-
222
184
  for (let i = 0; i < a._stackingOrder.length; i++) {
223
185
  if (typeof b._stackingOrder[i] === 'undefined') {
224
186
  return -1;
@@ -236,9 +198,55 @@ function visuallySort(a, b) {
236
198
  }
237
199
 
238
200
  // nodes are the same stacking order
239
- const docPosition = a.actualNode.compareDocumentPosition(b.actualNode);
240
- const DOMOrder = docPosition & 4 ? 1 : -1;
241
- const isDescendant = docPosition & 8 || docPosition & 16;
201
+ let aNode = a.actualNode;
202
+ let bNode = b.actualNode;
203
+
204
+ // elements don't correctly calculate document position when comparing
205
+ // across shadow boundaries, so we need to compare the position of a
206
+ // shared host instead
207
+
208
+ // elements have different hosts
209
+ if (aNode.getRootNode && aNode.getRootNode() !== bNode.getRootNode()) {
210
+ // keep track of all parent hosts and find the one both nodes share
211
+ const boundaries = [];
212
+ while (aNode) {
213
+ boundaries.push({
214
+ root: aNode.getRootNode(),
215
+ node: aNode
216
+ });
217
+ aNode = aNode.getRootNode().host;
218
+ }
219
+
220
+ while (
221
+ bNode &&
222
+ !boundaries.find(boundary => boundary.root === bNode.getRootNode())
223
+ ) {
224
+ bNode = bNode.getRootNode().host;
225
+ }
226
+
227
+ // bNode is a node that shares a host with some part of the a parent
228
+ // shadow tree, find the aNode that shares the same host as bNode
229
+ aNode = boundaries.find(boundary => boundary.root === bNode.getRootNode())
230
+ .node;
231
+
232
+ // sort child of shadow to it's host node by finding which element is
233
+ // the child of the host and sorting it before the host
234
+ if (aNode === bNode) {
235
+ return a.actualNode.getRootNode() !== aNode.getRootNode() ? -1 : 1;
236
+ }
237
+ }
238
+
239
+ const {
240
+ DOCUMENT_POSITION_FOLLOWING,
241
+ DOCUMENT_POSITION_CONTAINS,
242
+ DOCUMENT_POSITION_CONTAINED_BY
243
+ } = window.Node;
244
+
245
+ const docPosition = aNode.compareDocumentPosition(bNode);
246
+ const DOMOrder = docPosition & DOCUMENT_POSITION_FOLLOWING ? 1 : -1;
247
+ const isDescendant =
248
+ docPosition & DOCUMENT_POSITION_CONTAINS ||
249
+ docPosition & DOCUMENT_POSITION_CONTAINED_BY;
242
250
  const aPosition = getPositionOrder(a);
243
251
  const bPosition = getPositionOrder(b);
244
252
 
@@ -256,17 +264,13 @@ function visuallySort(a, b) {
256
264
  * @param {VirtualNode}
257
265
  * @return {Number[]}
258
266
  */
259
- function getStackingOrder(vNode) {
260
- const stackingOrder = vNode.parent
261
- ? vNode.parent._stackingOrder.slice()
262
- : [0];
263
-
264
- if (vNode.getComputedStylePropertyValue('z-index') !== 'auto') {
265
- stackingOrder[stackingOrder.length - 1] = parseInt(
266
- vNode.getComputedStylePropertyValue('z-index')
267
- );
267
+ function getStackingOrder(vNode, parentVNode) {
268
+ const stackingOrder = parentVNode._stackingOrder.slice();
269
+ const zIndex = vNode.getComputedStylePropertyValue('z-index');
270
+ if (zIndex !== 'auto') {
271
+ stackingOrder[stackingOrder.length - 1] = parseInt(zIndex);
268
272
  }
269
- if (isStackingContext(vNode)) {
273
+ if (isStackingContext(vNode, parentVNode)) {
270
274
  stackingOrder.push(0);
271
275
  }
272
276
 
@@ -278,24 +282,25 @@ function getStackingOrder(vNode) {
278
282
  * @param {VirtualNode}
279
283
  * @return {VirtualNode|null}
280
284
  */
281
- function findScrollRegionParent(vNode) {
285
+ function findScrollRegionParent(vNode, parentVNode) {
282
286
  let scrollRegionParent = null;
283
- let vNodeParent = vNode.parent;
284
287
  let checkedNodes = [vNode];
285
288
 
286
- while (vNodeParent) {
287
- if (vNodeParent._scrollRegionParent) {
288
- scrollRegionParent = vNodeParent._scrollRegionParent;
289
+ while (parentVNode) {
290
+ if (parentVNode._scrollRegionParent) {
291
+ scrollRegionParent = parentVNode._scrollRegionParent;
289
292
  break;
290
293
  }
291
294
 
292
- if (axe.utils.getScroll(vNodeParent.actualNode)) {
293
- scrollRegionParent = vNodeParent;
295
+ if (axe.utils.getScroll(parentVNode.actualNode)) {
296
+ scrollRegionParent = parentVNode;
294
297
  break;
295
298
  }
296
299
 
297
- checkedNodes.push(vNodeParent);
298
- vNodeParent = vNodeParent.parent;
300
+ checkedNodes.push(parentVNode);
301
+ parentVNode = axe.utils.getNodeFromTree(
302
+ parentVNode.actualNode.parentElement || parentVNode.actualNode.parentNode
303
+ );
299
304
  }
300
305
 
301
306
  // cache result of parent scroll region so we don't have to look up the entire
@@ -306,21 +311,6 @@ function findScrollRegionParent(vNode) {
306
311
  return scrollRegionParent;
307
312
  }
308
313
 
309
- /**
310
- * Get the DOMRect x or y value. IE11 (and Phantom) does not support x/y
311
- * on DOMRect.
312
- * @param {DOMRect}
313
- * @param {String} pos 'x' or 'y'
314
- * @return {Number}
315
- */
316
- function getDomPosition(rect, pos) {
317
- if (pos === 'x') {
318
- return 'x' in rect ? rect.x : rect.left;
319
- }
320
-
321
- return 'y' in rect ? rect.y : rect.top;
322
- }
323
-
324
314
  /**
325
315
  * Add a node to every cell of the grid it intersects with.
326
316
  * @param {Grid}
@@ -332,15 +322,15 @@ function addNodeToGrid(grid, vNode) {
332
322
  vNode._grid = grid;
333
323
 
334
324
  vNode.clientRects.forEach(rect => {
335
- const startRow = Math.floor(getDomPosition(rect, 'y') / gridSize);
336
- const startCol = Math.floor(getDomPosition(rect, 'x') / gridSize);
325
+ const x = rect.left;
326
+ const y = rect.top;
337
327
 
338
- const endRow = Math.floor(
339
- (getDomPosition(rect, 'y') + rect.height) / gridSize
340
- );
341
- const endCol = Math.floor(
342
- (getDomPosition(rect, 'x') + rect.width) / gridSize
343
- );
328
+ // "| 0" is a faster way to do Math.floor
329
+ // @see https://jsperf.com/math-floor-vs-math-round-vs-parseint/152
330
+ const startRow = (y / gridSize) | 0;
331
+ const startCol = (x / gridSize) | 0;
332
+ const endRow = ((y + rect.height) / gridSize) | 0;
333
+ const endCol = ((x + rect.width) / gridSize) | 0;
344
334
 
345
335
  for (let row = startRow; row <= endRow; row++) {
346
336
  grid.cells[row] = grid.cells[row] || [];
@@ -357,72 +347,97 @@ function addNodeToGrid(grid, vNode) {
357
347
  }
358
348
 
359
349
  /**
360
- * Setup the 2d grid and add every element to it.
350
+ * Setup the 2d grid and add every element to it, even elements not
351
+ * included in the flat tree
361
352
  */
362
- function createGrid() {
363
- const rootGrid = {
353
+ function createGrid(
354
+ root = document.body,
355
+ rootGrid = {
364
356
  container: null,
365
357
  cells: []
366
- };
367
-
368
- axe.utils
369
- .querySelectorAll(axe._tree[0], '*')
370
- .filter(vNode => vNode.actualNode.parentElement !== document.head)
371
- .forEach(vNode => {
372
- if (vNode.actualNode.nodeType !== window.Node.ELEMENT_NODE) {
373
- return;
374
- }
358
+ },
359
+ parentVNode = null
360
+ ) {
361
+ // by not starting at the htmlElement we don't have to pass a custom
362
+ // filter function into the treeWalker to filter out head elements,
363
+ // which would be called for every node
364
+ if (!parentVNode) {
365
+ let vNode = axe.utils.getNodeFromTree(document.documentElement);
366
+ if (!vNode) {
367
+ vNode = new VirtualNode(document.documentElement);
368
+ }
375
369
 
376
- vNode._stackingOrder = getStackingOrder(vNode);
370
+ vNode._stackingOrder = [0];
371
+ addNodeToGrid(rootGrid, vNode);
377
372
 
378
- // filter out any elements with 0 width or height
379
- // (we don't do this before so we can calculate stacking context
380
- // of parents with 0 width/height)
381
- const rect = vNode.boundingClientRect;
382
- if (rect.width === 0 || rect.height === 0) {
383
- return;
384
- }
373
+ if (axe.utils.getScroll(vNode.actualNode)) {
374
+ const subGrid = {
375
+ container: vNode,
376
+ cells: []
377
+ };
378
+ vNode._subGrid = subGrid;
379
+ }
380
+ }
385
381
 
386
- const scrollRegionParent = findScrollRegionParent(vNode);
387
- const grid = scrollRegionParent ? scrollRegionParent._subGrid : rootGrid;
382
+ // IE11 requires the first 3 parameters
383
+ // @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createTreeWalker
384
+ const treeWalker = document.createTreeWalker(
385
+ root,
386
+ window.NodeFilter.SHOW_ELEMENT,
387
+ null,
388
+ false
389
+ );
390
+ let node = parentVNode ? treeWalker.nextNode() : treeWalker.currentNode;
391
+ while (node) {
392
+ let vNode = axe.utils.getNodeFromTree(node);
393
+
394
+ // an svg in IE11 does not have a parentElement but instead has a
395
+ // parentNode. but parentNode could be a shadow root so we need to
396
+ // verity it's in the tree first
397
+ if (node.parentElement) {
398
+ parentVNode = axe.utils.getNodeFromTree(node.parentElement);
399
+ } else if (node.parentNode && axe.utils.getNodeFromTree(node.parentNode)) {
400
+ parentVNode = axe.utils.getNodeFromTree(node.parentNode);
401
+ }
388
402
 
389
- if (axe.utils.getScroll(vNode.actualNode)) {
390
- const subGrid = {
391
- container: vNode,
392
- cells: []
393
- };
394
- vNode._subGrid = subGrid;
395
- }
403
+ if (!vNode) {
404
+ vNode = new VirtualNode(node, parentVNode);
405
+ }
396
406
 
397
- addNodeToGrid(grid, vNode);
398
- });
399
- }
407
+ vNode._stackingOrder = getStackingOrder(vNode, parentVNode);
400
408
 
401
- /**
402
- * Return all elements that are at the center point of the passed in virtual node.
403
- * @method getElementStack
404
- * @memberof axe.commons.dom
405
- * @param {VirtualNode} vNode
406
- * @param {Boolean} [recursed] If the function has been called recursively
407
- * @return {VirtualNode[]}
408
- */
409
- dom.getElementStack = function(vNode, recursed = false) {
410
- if (!axe._cache.get('gridCreated')) {
411
- createGrid();
412
- axe._cache.set('gridCreated', true);
413
- }
409
+ const scrollRegionParent = findScrollRegionParent(vNode, parentVNode);
410
+ const grid = scrollRegionParent ? scrollRegionParent._subGrid : rootGrid;
414
411
 
415
- const grid = vNode._grid;
412
+ if (axe.utils.getScroll(vNode.actualNode)) {
413
+ const subGrid = {
414
+ container: vNode,
415
+ cells: []
416
+ };
417
+ vNode._subGrid = subGrid;
418
+ }
416
419
 
417
- if (!grid) {
418
- return [];
419
- }
420
+ // filter out any elements with 0 width or height
421
+ // (we don't do this before so we can calculate stacking context
422
+ // of parents with 0 width/height)
423
+ const rect = vNode.boundingClientRect;
424
+ if (rect.width !== 0 && rect.height !== 0 && dom.isVisible(node)) {
425
+ addNodeToGrid(grid, vNode);
426
+ }
420
427
 
421
- const boundingRect = vNode.boundingClientRect;
428
+ // add shadow root elements to the grid
429
+ if (axe.utils.isShadowRoot(node)) {
430
+ createGrid(node.shadowRoot, grid, vNode);
431
+ }
422
432
 
433
+ node = treeWalker.nextNode();
434
+ }
435
+ }
436
+
437
+ function getRectStack(grid, rect, recursed = false) {
423
438
  // use center point of rect
424
- let x = getDomPosition(boundingRect, 'x') + boundingRect.width / 2;
425
- let y = getDomPosition(boundingRect, 'y') + boundingRect.height / 2;
439
+ let x = rect.left + rect.width / 2;
440
+ let y = rect.top + rect.height / 2;
426
441
 
427
442
  // NOTE: there is a very rare edge case in Chrome vs Firefox that can
428
443
  // return different results of `document.elementsFromPoint`. If the center
@@ -430,36 +445,119 @@ dom.getElementStack = function(vNode, recursed = false) {
430
445
  // Chrome appears to round the number up and return the element while Firefox
431
446
  // keeps the number as is and won't return the element. In this case, we
432
447
  // went with pixel perfect collision rather than rounding
433
- const row = Math.floor(y / gridSize);
434
- const col = Math.floor(x / gridSize);
448
+ const row = (y / gridSize) | 0;
449
+ const col = (x / gridSize) | 0;
435
450
  let stack = grid.cells[row][col].filter(gridCellNode => {
436
- return gridCellNode.clientRects.find(rect => {
437
- let pointX = x;
438
- let pointY = y;
439
-
440
- let rectWidth = rect.width;
441
- let rectHeight = rect.height;
442
- let rectX = getDomPosition(rect, 'x');
443
- let rectY = getDomPosition(rect, 'y');
451
+ return gridCellNode.clientRects.find(clientRect => {
452
+ let rectX = clientRect.left;
453
+ let rectY = clientRect.top;
444
454
 
445
455
  // perform an AABB (axis-aligned bounding box) collision check for the
446
456
  // point inside the rect
447
457
  return (
448
- pointX < rectX + rectWidth &&
449
- pointX > rectX &&
450
- pointY < rectY + rectHeight &&
451
- pointY > rectY
458
+ x <= rectX + clientRect.width &&
459
+ x >= rectX &&
460
+ y <= rectY + clientRect.height &&
461
+ y >= rectY
452
462
  );
453
463
  });
454
464
  });
455
465
 
456
- if (grid.container) {
457
- stack = dom.getElementStack(grid.container, true).concat(stack);
466
+ const gridContainer = grid.container;
467
+ if (gridContainer) {
468
+ stack = getRectStack(
469
+ gridContainer._grid,
470
+ gridContainer.boundingClientRect,
471
+ true
472
+ ).concat(stack);
458
473
  }
459
474
 
460
475
  if (!recursed) {
461
- stack.sort(visuallySort);
476
+ stack = stack
477
+ .sort(visuallySort)
478
+ .map(vNode => vNode.actualNode)
479
+ // always make sure html is the last element
480
+ .concat(document.documentElement)
481
+ // remove duplicates caused by adding client rects of the same node
482
+ .filter((node, index, array) => array.indexOf(node) === index);
462
483
  }
463
484
 
464
485
  return stack;
486
+ }
487
+
488
+ /**
489
+ * Return all elements that are at the center bounding rect of the passed in node.
490
+ * @method getElementStack
491
+ * @memberof axe.commons.dom
492
+ * @param {Node} node
493
+ * @return {Node[]}
494
+ */
495
+ dom.getElementStack = function(node) {
496
+ if (!axe._cache.get('gridCreated')) {
497
+ createGrid();
498
+ axe._cache.set('gridCreated', true);
499
+ }
500
+
501
+ const vNode = axe.utils.getNodeFromTree(node);
502
+ const grid = vNode._grid;
503
+
504
+ if (!grid) {
505
+ return [];
506
+ }
507
+
508
+ return getRectStack(grid, vNode.boundingClientRect);
509
+ };
510
+
511
+ /**
512
+ * Return all elements that are at the center of each text client rect of the passed in node.
513
+ * @method getTextElementStack
514
+ * @memberof axe.commons.dom
515
+ * @param {Node} node
516
+ * @return {Array<Node[]>}
517
+ */
518
+ dom.getTextElementStack = function(node) {
519
+ if (!axe._cache.get('gridCreated')) {
520
+ createGrid();
521
+ axe._cache.set('gridCreated', true);
522
+ }
523
+
524
+ const vNode = axe.utils.getNodeFromTree(node);
525
+ const grid = vNode._grid;
526
+
527
+ if (!grid) {
528
+ return [];
529
+ }
530
+
531
+ // for code blocks that use syntax highlighting, you can get a ton of client
532
+ // rects (See https://github.com/dequelabs/axe-core/issues/1985). they use
533
+ // a mixture of text nodes and other nodes (which will contain their own text
534
+ // nodes), but all we care about is checking the direct text nodes as the
535
+ // other nodes will have their own client rects checked. doing this speeds up
536
+ // color contrast significantly for large syntax highlighted code blocks
537
+ const clientRects = [];
538
+ Array.from(node.childNodes).forEach(elm => {
539
+ if (
540
+ elm.nodeType === 3 &&
541
+ axe.commons.text.sanitize(elm.textContent) !== ''
542
+ ) {
543
+ const range = document.createRange();
544
+ range.selectNodeContents(elm);
545
+ const rects = range.getClientRects();
546
+
547
+ for (let i = 0; i < rects.length; i++) {
548
+ const rect = rects[i];
549
+
550
+ // filter out 0 width and height rects (newline characters)
551
+ // ie11 has newline characters return 0.00998, so we'll say if the
552
+ // line is < 1 it shouldn't be counted
553
+ if (rect.width >= 1 && rect.height >= 1) {
554
+ clientRects.push(rect);
555
+ }
556
+ }
557
+ }
558
+ });
559
+
560
+ return clientRects.map(rect => {
561
+ return getRectStack(grid, rect);
562
+ });
465
563
  };