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