govuk_publishing_components 21.22.0 → 21.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ };