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.
- checksums.yaml +4 -4
- data/app/assets/stylesheets/govuk_publishing_components/components/govspeak/_button.scss +2 -1
- data/app/views/govuk_publishing_components/components/_character_count.html.erb +14 -13
- data/app/views/govuk_publishing_components/components/docs/character_count.yml +8 -0
- data/app/views/govuk_publishing_components/components/docs/govspeak.yml +17 -4
- data/lib/govuk_publishing_components/version.rb +1 -1
- data/node_modules/axe-core/CHANGELOG.md +37 -0
- data/node_modules/axe-core/README.md +2 -2
- data/node_modules/axe-core/axe.d.ts +1 -1
- data/node_modules/axe-core/axe.js +2430 -2242
- data/node_modules/axe-core/axe.min.js +3 -3
- data/node_modules/axe-core/bower.json +1 -1
- data/node_modules/axe-core/doc/API.md +10 -0
- data/node_modules/axe-core/doc/check-message-template.md +124 -0
- data/node_modules/axe-core/doc/examples/jasmine/package-lock.json +1489 -0
- data/node_modules/axe-core/doc/examples/jest_react/package-lock.json +7525 -0
- data/node_modules/axe-core/doc/examples/mocha/package-lock.json +1671 -0
- data/node_modules/axe-core/doc/examples/phantomjs/package-lock.json +862 -0
- data/node_modules/axe-core/doc/examples/qunit/package-lock.json +2951 -0
- data/node_modules/axe-core/doc/rule-descriptions.md +2 -2
- data/node_modules/axe-core/lib/checks/aria/allowed-attr.json +4 -1
- data/node_modules/axe-core/lib/checks/aria/aria-allowed-role.json +8 -2
- data/node_modules/axe-core/lib/checks/aria/errormessage.json +4 -1
- data/node_modules/axe-core/lib/checks/aria/implicit-role-fallback.json +1 -0
- data/node_modules/axe-core/lib/checks/aria/no-implicit-explicit-label.json +1 -1
- data/node_modules/axe-core/lib/checks/aria/required-attr.json +4 -1
- data/node_modules/axe-core/lib/checks/aria/required-children.json +8 -2
- data/node_modules/axe-core/lib/checks/aria/required-parent.json +4 -1
- data/node_modules/axe-core/lib/checks/aria/unsupportedattr.json +1 -1
- data/node_modules/axe-core/lib/checks/aria/unsupportedrole.json +1 -1
- data/node_modules/axe-core/lib/checks/aria/valid-attr-value.json +8 -2
- data/node_modules/axe-core/lib/checks/aria/valid-attr.json +5 -2
- data/node_modules/axe-core/lib/checks/color/color-contrast.js +1 -1
- data/node_modules/axe-core/lib/checks/color/color-contrast.json +4 -4
- data/node_modules/axe-core/lib/checks/color/link-in-text-block.js +2 -2
- data/node_modules/axe-core/lib/checks/color/link-in-text-block.json +2 -2
- data/node_modules/axe-core/lib/checks/forms/fieldset.js +1 -1
- data/node_modules/axe-core/lib/checks/forms/fieldset.json +8 -1
- data/node_modules/axe-core/lib/checks/forms/group-labelledby.js +2 -2
- data/node_modules/axe-core/lib/checks/forms/group-labelledby.json +6 -2
- data/node_modules/axe-core/lib/checks/keyboard/landmark-is-top-level.json +2 -2
- data/node_modules/axe-core/lib/checks/lists/listitem.js +3 -1
- data/node_modules/axe-core/lib/checks/lists/listitem.json +4 -1
- data/node_modules/axe-core/lib/checks/mobile/css-orientation-lock.js +216 -99
- data/node_modules/axe-core/lib/checks/mobile/css-orientation-lock.json +3 -0
- data/node_modules/axe-core/lib/checks/mobile/meta-viewport.json +1 -1
- data/node_modules/axe-core/lib/checks/parsing/duplicate-id-active.json +1 -1
- data/node_modules/axe-core/lib/checks/parsing/duplicate-id-aria.json +1 -1
- data/node_modules/axe-core/lib/checks/shared/aria-label.js +1 -1
- data/node_modules/axe-core/lib/checks/shared/avoid-inline-spacing.json +4 -1
- data/node_modules/axe-core/lib/checks/shared/non-empty-if-present.js +5 -1
- data/node_modules/axe-core/lib/checks/shared/non-empty-if-present.json +4 -1
- data/node_modules/axe-core/lib/checks/tables/has-caption.json +1 -0
- data/node_modules/axe-core/lib/checks/tables/has-summary.json +1 -0
- data/node_modules/axe-core/lib/checks/tables/has-th.json +1 -0
- data/node_modules/axe-core/lib/commons/aria/arialabel-text.js +7 -4
- data/node_modules/axe-core/lib/commons/aria/get-element-unallowed-roles.js +21 -0
- data/node_modules/axe-core/lib/commons/dom/get-element-stack.js +465 -0
- data/node_modules/axe-core/lib/commons/dom/shadow-elements-from-point.js +2 -1
- data/node_modules/axe-core/lib/core/base/audit.js +39 -17
- data/node_modules/axe-core/lib/core/base/virtual-node/serial-virtual-node.js +2 -0
- data/node_modules/axe-core/lib/core/base/virtual-node/virtual-node.js +43 -0
- data/node_modules/axe-core/lib/core/reporters/helpers/incomplete-fallback-msg.js +3 -1
- data/node_modules/axe-core/lib/core/utils/parse-crossorigin-stylesheet.js +1 -0
- data/node_modules/axe-core/lib/core/utils/preload-cssom.js +4 -3
- data/node_modules/axe-core/lib/core/utils/process-message.js +72 -0
- data/node_modules/axe-core/lib/core/utils/publish-metadata.js +20 -7
- data/node_modules/axe-core/lib/rules/aria-dpub-role-fallback.json +2 -1
- data/node_modules/axe-core/lib/rules/label-content-name-mismatch-matches.js +1 -1
- data/node_modules/axe-core/lib/rules/layout-table.json +2 -1
- data/node_modules/axe-core/locales/ja.json +1 -1
- data/node_modules/axe-core/package.json +16 -16
- data/node_modules/axe-core/sri-history.json +4 -0
- data/node_modules/axe-core/typings/axe-core/axe-core-tests.js +151 -0
- data/node_modules/axe-core/typings/axe-core/axe-core-tests.ts +18 -0
- metadata +11 -2
|
@@ -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": "{
|
|
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: {
|
|
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: {
|
|
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(
|
|
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":
|
|
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
|
-
|
|
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":
|
|
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
|
}
|
|
@@ -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
|
|
11
|
-
|
|
12
|
-
|
|
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.
|
|
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
|
+
};
|