govuk_publishing_components 21.22.0 → 21.22.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/govuk_publishing_components/components/govspeak/_button.scss +2 -1
  3. data/app/views/govuk_publishing_components/components/_character_count.html.erb +14 -13
  4. data/app/views/govuk_publishing_components/components/docs/character_count.yml +8 -0
  5. data/app/views/govuk_publishing_components/components/docs/govspeak.yml +17 -4
  6. data/lib/govuk_publishing_components/version.rb +1 -1
  7. data/node_modules/axe-core/CHANGELOG.md +37 -0
  8. data/node_modules/axe-core/README.md +2 -2
  9. data/node_modules/axe-core/axe.d.ts +1 -1
  10. data/node_modules/axe-core/axe.js +2430 -2242
  11. data/node_modules/axe-core/axe.min.js +3 -3
  12. data/node_modules/axe-core/bower.json +1 -1
  13. data/node_modules/axe-core/doc/API.md +10 -0
  14. data/node_modules/axe-core/doc/check-message-template.md +124 -0
  15. data/node_modules/axe-core/doc/examples/jasmine/package-lock.json +1489 -0
  16. data/node_modules/axe-core/doc/examples/jest_react/package-lock.json +7525 -0
  17. data/node_modules/axe-core/doc/examples/mocha/package-lock.json +1671 -0
  18. data/node_modules/axe-core/doc/examples/phantomjs/package-lock.json +862 -0
  19. data/node_modules/axe-core/doc/examples/qunit/package-lock.json +2951 -0
  20. data/node_modules/axe-core/doc/rule-descriptions.md +2 -2
  21. data/node_modules/axe-core/lib/checks/aria/allowed-attr.json +4 -1
  22. data/node_modules/axe-core/lib/checks/aria/aria-allowed-role.json +8 -2
  23. data/node_modules/axe-core/lib/checks/aria/errormessage.json +4 -1
  24. data/node_modules/axe-core/lib/checks/aria/implicit-role-fallback.json +1 -0
  25. data/node_modules/axe-core/lib/checks/aria/no-implicit-explicit-label.json +1 -1
  26. data/node_modules/axe-core/lib/checks/aria/required-attr.json +4 -1
  27. data/node_modules/axe-core/lib/checks/aria/required-children.json +8 -2
  28. data/node_modules/axe-core/lib/checks/aria/required-parent.json +4 -1
  29. data/node_modules/axe-core/lib/checks/aria/unsupportedattr.json +1 -1
  30. data/node_modules/axe-core/lib/checks/aria/unsupportedrole.json +1 -1
  31. data/node_modules/axe-core/lib/checks/aria/valid-attr-value.json +8 -2
  32. data/node_modules/axe-core/lib/checks/aria/valid-attr.json +5 -2
  33. data/node_modules/axe-core/lib/checks/color/color-contrast.js +1 -1
  34. data/node_modules/axe-core/lib/checks/color/color-contrast.json +4 -4
  35. data/node_modules/axe-core/lib/checks/color/link-in-text-block.js +2 -2
  36. data/node_modules/axe-core/lib/checks/color/link-in-text-block.json +2 -2
  37. data/node_modules/axe-core/lib/checks/forms/fieldset.js +1 -1
  38. data/node_modules/axe-core/lib/checks/forms/fieldset.json +8 -1
  39. data/node_modules/axe-core/lib/checks/forms/group-labelledby.js +2 -2
  40. data/node_modules/axe-core/lib/checks/forms/group-labelledby.json +6 -2
  41. data/node_modules/axe-core/lib/checks/keyboard/landmark-is-top-level.json +2 -2
  42. data/node_modules/axe-core/lib/checks/lists/listitem.js +3 -1
  43. data/node_modules/axe-core/lib/checks/lists/listitem.json +4 -1
  44. data/node_modules/axe-core/lib/checks/mobile/css-orientation-lock.js +216 -99
  45. data/node_modules/axe-core/lib/checks/mobile/css-orientation-lock.json +3 -0
  46. data/node_modules/axe-core/lib/checks/mobile/meta-viewport.json +1 -1
  47. data/node_modules/axe-core/lib/checks/parsing/duplicate-id-active.json +1 -1
  48. data/node_modules/axe-core/lib/checks/parsing/duplicate-id-aria.json +1 -1
  49. data/node_modules/axe-core/lib/checks/shared/aria-label.js +1 -1
  50. data/node_modules/axe-core/lib/checks/shared/avoid-inline-spacing.json +4 -1
  51. data/node_modules/axe-core/lib/checks/shared/non-empty-if-present.js +5 -1
  52. data/node_modules/axe-core/lib/checks/shared/non-empty-if-present.json +4 -1
  53. data/node_modules/axe-core/lib/checks/tables/has-caption.json +1 -0
  54. data/node_modules/axe-core/lib/checks/tables/has-summary.json +1 -0
  55. data/node_modules/axe-core/lib/checks/tables/has-th.json +1 -0
  56. data/node_modules/axe-core/lib/commons/aria/arialabel-text.js +7 -4
  57. data/node_modules/axe-core/lib/commons/aria/get-element-unallowed-roles.js +21 -0
  58. data/node_modules/axe-core/lib/commons/dom/get-element-stack.js +465 -0
  59. data/node_modules/axe-core/lib/commons/dom/shadow-elements-from-point.js +2 -1
  60. data/node_modules/axe-core/lib/core/base/audit.js +39 -17
  61. data/node_modules/axe-core/lib/core/base/virtual-node/serial-virtual-node.js +2 -0
  62. data/node_modules/axe-core/lib/core/base/virtual-node/virtual-node.js +43 -0
  63. data/node_modules/axe-core/lib/core/reporters/helpers/incomplete-fallback-msg.js +3 -1
  64. data/node_modules/axe-core/lib/core/utils/parse-crossorigin-stylesheet.js +1 -0
  65. data/node_modules/axe-core/lib/core/utils/preload-cssom.js +4 -3
  66. data/node_modules/axe-core/lib/core/utils/process-message.js +72 -0
  67. data/node_modules/axe-core/lib/core/utils/publish-metadata.js +20 -7
  68. data/node_modules/axe-core/lib/rules/aria-dpub-role-fallback.json +2 -1
  69. data/node_modules/axe-core/lib/rules/label-content-name-mismatch-matches.js +1 -1
  70. data/node_modules/axe-core/lib/rules/layout-table.json +2 -1
  71. data/node_modules/axe-core/locales/ja.json +1 -1
  72. data/node_modules/axe-core/package.json +16 -16
  73. data/node_modules/axe-core/sri-history.json +4 -0
  74. data/node_modules/axe-core/typings/axe-core/axe-core-tests.js +151 -0
  75. data/node_modules/axe-core/typings/axe-core/axe-core-tests.ts +18 -0
  76. metadata +11 -2
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "id": "css-orientation-lock",
3
3
  "evaluate": "css-orientation-lock.js",
4
+ "options": {
5
+ "degreeThreshold": 2
6
+ },
4
7
  "metadata": {
5
8
  "impact": "serious",
6
9
  "messages": {
@@ -8,7 +8,7 @@
8
8
  "impact": "critical",
9
9
  "messages": {
10
10
  "pass": "<meta> tag does not disable zooming on mobile devices",
11
- "fail": "{{=it.data}} on <meta> tag disables zooming on mobile devices"
11
+ "fail": "${data} on <meta> tag disables zooming on mobile devices"
12
12
  }
13
13
  }
14
14
  }
@@ -6,7 +6,7 @@
6
6
  "impact": "serious",
7
7
  "messages": {
8
8
  "pass": "Document has no active elements that share the same id attribute",
9
- "fail": "Document has active elements with the same id attribute: {{=it.data}}"
9
+ "fail": "Document has active elements with the same id attribute: ${data}"
10
10
  }
11
11
  }
12
12
  }
@@ -6,7 +6,7 @@
6
6
  "impact": "critical",
7
7
  "messages": {
8
8
  "pass": "Document has no elements referenced with ARIA or labels that share the same id attribute",
9
- "fail": "Document has multiple elements referenced with ARIA with the same id attribute: {{=it.data}}"
9
+ "fail": "Document has multiple elements referenced with ARIA with the same id attribute: ${data}"
10
10
  }
11
11
  }
12
12
  }
@@ -1,2 +1,2 @@
1
1
  const { text, aria } = axe.commons;
2
- return !!text.sanitize(aria.arialabelText(node));
2
+ return !!text.sanitize(aria.arialabelText(virtualNode));
@@ -5,7 +5,10 @@
5
5
  "impact": "serious",
6
6
  "messages": {
7
7
  "pass": "No inline styles with '!important' that affect text spacing has been specified",
8
- "fail": "Remove '!important' from inline style{{=it.data && it.data.length > 1 ? 's' : ''}} {{=it.data.join(', ')}}, as overriding this is not supported by most browsers"
8
+ "fail": {
9
+ "singular": "Remove '!important' from inline style ${data.values}, as overriding this is not supported by most browsers",
10
+ "plural": "Remove '!important' from inline styles ${data.values}, as overriding this is not supported by most browsers"
11
+ }
9
12
  }
10
13
  }
11
14
  }
@@ -3,7 +3,11 @@ let nodeName = node.nodeName.toUpperCase();
3
3
  let type = (node.getAttribute('type') || '').toLowerCase();
4
4
  let label = node.getAttribute('value');
5
5
 
6
- this.data(label);
6
+ if (label) {
7
+ this.data({
8
+ messageKey: 'has-label'
9
+ });
10
+ }
7
11
 
8
12
  if (nodeName === 'INPUT' && ['submit', 'reset'].includes(type)) {
9
13
  return label === null;
@@ -4,7 +4,10 @@
4
4
  "metadata": {
5
5
  "impact": "critical",
6
6
  "messages": {
7
- "pass": "Element {{?it.data}}has a non-empty value attribute{{??}}does not have a value attribute{{?}}",
7
+ "pass": {
8
+ "default": "Element does not have a value attribute",
9
+ "has-label": "Element has a non-empty value attribute"
10
+ },
8
11
  "fail": "Element has a value attribute and the value attribute is empty"
9
12
  }
10
13
  }
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "id": "has-caption",
3
3
  "evaluate": "has-caption.js",
4
+ "deprecated": true,
4
5
  "metadata": {
5
6
  "impact": "serious",
6
7
  "messages": {
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "id": "has-summary",
3
3
  "evaluate": "has-summary.js",
4
+ "deprecated": true,
4
5
  "metadata": {
5
6
  "impact": "serious",
6
7
  "messages": {
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "id": "has-th",
3
3
  "evaluate": "has-th.js",
4
+ "deprecated": true,
4
5
  "metadata": {
5
6
  "impact": "serious",
6
7
  "messages": {
@@ -3,13 +3,16 @@
3
3
  /**
4
4
  * Get the text value of aria-label, if any
5
5
  *
6
+ * @deprecated Do not use Element directly. Pass VirtualNode instead
6
7
  * @param {VirtualNode|Element} element
7
8
  * @return {string} ARIA label
8
9
  */
9
10
  aria.arialabelText = function arialabelText(node) {
10
- node = node.actualNode || node;
11
- if (node.nodeType !== 1) {
12
- return '';
11
+ if (node instanceof axe.AbstractVirtualNode === false) {
12
+ if (node.nodeType !== 1) {
13
+ return '';
14
+ }
15
+ node = axe.utils.getNodeFromTree(node);
13
16
  }
14
- return node.getAttribute('aria-label') || '';
17
+ return node.attr('aria-label') || '';
15
18
  };
@@ -1,5 +1,17 @@
1
1
  /* global aria */
2
2
 
3
+ // dpub roles which are subclassing roles that are implicit on some native
4
+ // HTML elements (img, link, etc.)
5
+ const dpubRoles = [
6
+ 'doc-backlink',
7
+ 'doc-biblioentry',
8
+ 'doc-biblioref',
9
+ 'doc-cover',
10
+ 'doc-endnote',
11
+ 'doc-glossref',
12
+ 'doc-noteref'
13
+ ];
14
+
3
15
  /**
4
16
  * Returns all roles applicable to element in a list
5
17
  *
@@ -71,6 +83,15 @@ aria.getElementUnallowedRoles = function getElementUnallowedRoles(
71
83
  return false;
72
84
  }
73
85
 
86
+ // if role is a dpub role make sure it's used on an element with a valid
87
+ // implicit role fallback
88
+ if (allowImplicit && dpubRoles.includes(role)) {
89
+ const roleType = axe.commons.aria.getRoleType(role);
90
+ if (implicitRole !== roleType) {
91
+ return true;
92
+ }
93
+ }
94
+
74
95
  // Edge case:
75
96
  // setting implicit role row on tr element is allowed when child of table[role='grid']
76
97
  if (
@@ -0,0 +1,465 @@
1
+ /* global dom */
2
+
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)
5
+
6
+ /**
7
+ * Determine if node produces a stacking context.
8
+ * References:
9
+ * https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context
10
+ * https://github.com/gwwar/z-context/blob/master/devtools/index.js
11
+ * @param {VirtualNode} vNode
12
+ * @return {Boolean}
13
+ */
14
+ function isStackingContext(vNode) {
15
+ const node = vNode.actualNode;
16
+
17
+ //the root element (HTML)
18
+ if (
19
+ !node ||
20
+ node.nodeName === 'HTML' ||
21
+ node.nodeName === '#document-fragment'
22
+ ) {
23
+ return true;
24
+ }
25
+
26
+ // position: fixed or sticky
27
+ if (
28
+ vNode.getComputedStylePropertyValue('position') === 'fixed' ||
29
+ vNode.getComputedStylePropertyValue('position') === 'sticky'
30
+ ) {
31
+ return true;
32
+ }
33
+
34
+ // 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
+ ) {
39
+ return true;
40
+ }
41
+
42
+ // elements with an opacity value less than 1.
43
+ if (vNode.getComputedStylePropertyValue('opacity') !== '1') {
44
+ return true;
45
+ }
46
+
47
+ // elements with a transform value other than "none"
48
+ const transform =
49
+ vNode.getComputedStylePropertyValue('-webkit-transform') ||
50
+ vNode.getComputedStylePropertyValue('-ms-transform') ||
51
+ vNode.getComputedStylePropertyValue('transform') ||
52
+ 'none';
53
+
54
+ if (transform !== 'none') {
55
+ return true;
56
+ }
57
+
58
+ // 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
+ ) {
63
+ return true;
64
+ }
65
+
66
+ // elements with a filter value other than "none"
67
+ if (
68
+ vNode.getComputedStylePropertyValue('filter') &&
69
+ vNode.getComputedStylePropertyValue('filter') !== 'none'
70
+ ) {
71
+ return true;
72
+ }
73
+
74
+ // elements with a perspective value other than "none"
75
+ if (
76
+ vNode.getComputedStylePropertyValue('perspective') &&
77
+ vNode.getComputedStylePropertyValue('perspective') !== 'none'
78
+ ) {
79
+ return true;
80
+ }
81
+
82
+ // element with a clip-path value other than "none"
83
+ if (
84
+ vNode.getComputedStylePropertyValue('clip-path') &&
85
+ vNode.getComputedStylePropertyValue('clip-path') !== 'none'
86
+ ) {
87
+ return true;
88
+ }
89
+
90
+ // element with a mask value other than "none"
91
+ const mask =
92
+ vNode.getComputedStylePropertyValue('-webkit-mask') ||
93
+ vNode.getComputedStylePropertyValue('mask') ||
94
+ 'none';
95
+
96
+ if (mask !== 'none') {
97
+ return true;
98
+ }
99
+
100
+ // element with a mask-image value other than "none"
101
+ const maskImage =
102
+ vNode.getComputedStylePropertyValue('-webkit-mask-image') ||
103
+ vNode.getComputedStylePropertyValue('mask-image') ||
104
+ 'none';
105
+
106
+ if (maskImage !== 'none') {
107
+ return true;
108
+ }
109
+
110
+ // element with a mask-border value other than "none"
111
+ const maskBorder =
112
+ vNode.getComputedStylePropertyValue('-webkit-mask-border') ||
113
+ vNode.getComputedStylePropertyValue('mask-border') ||
114
+ 'none';
115
+
116
+ if (maskBorder !== 'none') {
117
+ return true;
118
+ }
119
+
120
+ // elements with isolation set to "isolate"
121
+ if (vNode.getComputedStylePropertyValue('isolation') === 'isolate') {
122
+ return true;
123
+ }
124
+
125
+ // 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
+ ) {
130
+ return true;
131
+ }
132
+
133
+ // elements with -webkit-overflow-scrolling set to "touch"
134
+ if (
135
+ vNode.getComputedStylePropertyValue('-webkit-overflow-scrolling') ===
136
+ 'touch'
137
+ ) {
138
+ return true;
139
+ }
140
+
141
+ // element with a contain value of "layout" or "paint" or a composite value
142
+ // that includes either of them (i.e. contain: strict, contain: content).
143
+ const contain = vNode.getComputedStylePropertyValue('contain');
144
+ if (['layout', 'paint', 'strict', 'content'].includes(contain)) {
145
+ return true;
146
+ }
147
+
148
+ // 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');
154
+ if (
155
+ [
156
+ 'flex',
157
+ 'inline-flex',
158
+ 'inline flex',
159
+ 'grid',
160
+ 'inline-grid',
161
+ 'inline grid'
162
+ ].includes(parentDsiplay)
163
+ ) {
164
+ return true;
165
+ }
166
+ }
167
+
168
+ return false;
169
+ }
170
+
171
+ /**
172
+ * Return the index order of how to position this element. return nodes in non-positioned, floating, positioned order
173
+ * References:
174
+ * https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_without_z-index
175
+ * https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_and_float
176
+ * https://drafts.csswg.org/css2/visuren.html#layers
177
+ * @param {VirtualNode} vNode
178
+ * @return {Number}
179
+ */
180
+ function getPositionOrder(vNode) {
181
+ if (vNode.getComputedStylePropertyValue('position') === 'static') {
182
+ // 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks.
183
+ if (
184
+ vNode.getComputedStylePropertyValue('display').indexOf('inline') !== -1
185
+ ) {
186
+ return 2;
187
+ }
188
+
189
+ // 4. the non-positioned floats.
190
+ if (vNode.getComputedStylePropertyValue('float') !== 'none') {
191
+ return 1;
192
+ }
193
+
194
+ // 3. the in-flow, non-inline-level, non-positioned descendants.
195
+ if (vNode.getComputedStylePropertyValue('position') === 'static') {
196
+ return 0;
197
+ }
198
+ }
199
+
200
+ // 6. the child stacking contexts with stack level 0 and the positioned descendants with stack level 0.
201
+ return 3;
202
+ }
203
+
204
+ /**
205
+ * Visually sort nodes based on their stack order
206
+ * References:
207
+ * https://drafts.csswg.org/css2/visuren.html#layers
208
+ * @param {VirtualNode}
209
+ * @param {VirtualNode}
210
+ */
211
+ function visuallySort(a, b) {
212
+ /*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
+ for (let i = 0; i < a._stackingOrder.length; i++) {
223
+ if (typeof b._stackingOrder[i] === 'undefined') {
224
+ return -1;
225
+ }
226
+
227
+ // 7. the child stacking contexts with positive stack levels (least positive first).
228
+ if (b._stackingOrder[i] > a._stackingOrder[i]) {
229
+ return 1;
230
+ }
231
+
232
+ // 2. the child stacking contexts with negative stack levels (most negative first).
233
+ if (b._stackingOrder[i] < a._stackingOrder[i]) {
234
+ return -1;
235
+ }
236
+ }
237
+
238
+ // 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;
242
+ const aPosition = getPositionOrder(a);
243
+ const bPosition = getPositionOrder(b);
244
+
245
+ // a child of a positioned element should also be on top of the parent
246
+ if (aPosition === bPosition || isDescendant) {
247
+ return DOMOrder;
248
+ }
249
+
250
+ return bPosition - aPosition;
251
+ }
252
+
253
+ /**
254
+ * Determine the stacking order of an element. The stacking order is an array of
255
+ * zIndex values for each stacking context parent.
256
+ * @param {VirtualNode}
257
+ * @return {Number[]}
258
+ */
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
+ );
268
+ }
269
+ if (isStackingContext(vNode)) {
270
+ stackingOrder.push(0);
271
+ }
272
+
273
+ return stackingOrder;
274
+ }
275
+
276
+ /**
277
+ * Return the parent node that is a scroll region.
278
+ * @param {VirtualNode}
279
+ * @return {VirtualNode|null}
280
+ */
281
+ function findScrollRegionParent(vNode) {
282
+ let scrollRegionParent = null;
283
+ let vNodeParent = vNode.parent;
284
+ let checkedNodes = [vNode];
285
+
286
+ while (vNodeParent) {
287
+ if (vNodeParent._scrollRegionParent) {
288
+ scrollRegionParent = vNodeParent._scrollRegionParent;
289
+ break;
290
+ }
291
+
292
+ if (axe.utils.getScroll(vNodeParent.actualNode)) {
293
+ scrollRegionParent = vNodeParent;
294
+ break;
295
+ }
296
+
297
+ checkedNodes.push(vNodeParent);
298
+ vNodeParent = vNodeParent.parent;
299
+ }
300
+
301
+ // cache result of parent scroll region so we don't have to look up the entire
302
+ // tree again for a child node
303
+ checkedNodes.forEach(
304
+ vNode => (vNode._scrollRegionParent = scrollRegionParent)
305
+ );
306
+ return scrollRegionParent;
307
+ }
308
+
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
+ /**
325
+ * Add a node to every cell of the grid it intersects with.
326
+ * @param {Grid}
327
+ * @param {VirtualNode}
328
+ */
329
+ function addNodeToGrid(grid, vNode) {
330
+ // save a reference to where this element is in the grid so we
331
+ // can find it even if it's in a subgrid
332
+ vNode._grid = grid;
333
+
334
+ vNode.clientRects.forEach(rect => {
335
+ const startRow = Math.floor(getDomPosition(rect, 'y') / gridSize);
336
+ const startCol = Math.floor(getDomPosition(rect, 'x') / gridSize);
337
+
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
+ );
344
+
345
+ for (let row = startRow; row <= endRow; row++) {
346
+ grid.cells[row] = grid.cells[row] || [];
347
+
348
+ for (let col = startCol; col <= endCol; col++) {
349
+ grid.cells[row][col] = grid.cells[row][col] || [];
350
+
351
+ if (!grid.cells[row][col].includes(vNode)) {
352
+ grid.cells[row][col].push(vNode);
353
+ }
354
+ }
355
+ }
356
+ });
357
+ }
358
+
359
+ /**
360
+ * Setup the 2d grid and add every element to it.
361
+ */
362
+ function createGrid() {
363
+ const rootGrid = {
364
+ container: null,
365
+ 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
+ }
375
+
376
+ vNode._stackingOrder = getStackingOrder(vNode);
377
+
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
+ }
385
+
386
+ const scrollRegionParent = findScrollRegionParent(vNode);
387
+ const grid = scrollRegionParent ? scrollRegionParent._subGrid : rootGrid;
388
+
389
+ if (axe.utils.getScroll(vNode.actualNode)) {
390
+ const subGrid = {
391
+ container: vNode,
392
+ cells: []
393
+ };
394
+ vNode._subGrid = subGrid;
395
+ }
396
+
397
+ addNodeToGrid(grid, vNode);
398
+ });
399
+ }
400
+
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
+ }
414
+
415
+ const grid = vNode._grid;
416
+
417
+ if (!grid) {
418
+ return [];
419
+ }
420
+
421
+ const boundingRect = vNode.boundingClientRect;
422
+
423
+ // use center point of rect
424
+ let x = getDomPosition(boundingRect, 'x') + boundingRect.width / 2;
425
+ let y = getDomPosition(boundingRect, 'y') + boundingRect.height / 2;
426
+
427
+ // NOTE: there is a very rare edge case in Chrome vs Firefox that can
428
+ // return different results of `document.elementsFromPoint`. If the center
429
+ // point of the element is <1px outside of another elements bounding rect,
430
+ // Chrome appears to round the number up and return the element while Firefox
431
+ // keeps the number as is and won't return the element. In this case, we
432
+ // went with pixel perfect collision rather than rounding
433
+ const row = Math.floor(y / gridSize);
434
+ const col = Math.floor(x / gridSize);
435
+ 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');
444
+
445
+ // perform an AABB (axis-aligned bounding box) collision check for the
446
+ // point inside the rect
447
+ return (
448
+ pointX < rectX + rectWidth &&
449
+ pointX > rectX &&
450
+ pointY < rectY + rectHeight &&
451
+ pointY > rectY
452
+ );
453
+ });
454
+ });
455
+
456
+ if (grid.container) {
457
+ stack = dom.getElementStack(grid.container, true).concat(stack);
458
+ }
459
+
460
+ if (!recursed) {
461
+ stack.sort(visuallySort);
462
+ }
463
+
464
+ return stack;
465
+ };