-
Notifications
You must be signed in to change notification settings - Fork 742
/
is-visible.js
158 lines (139 loc) · 3.91 KB
/
is-visible.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
/* global dom */
const clipRegex = /rect\s*\(([0-9]+)px,?\s*([0-9]+)px,?\s*([0-9]+)px,?\s*([0-9]+)px\s*\)/;
const clipPathRegex = /(\w+)\((\d+)/;
/**
* Determines if an element is hidden with a clip or clip-path technique
* @method isClipped
* @memberof axe.commons.dom
* @private
* @param {CSSStyleDeclaration} style Computed style
* @return {Boolean}
*/
function isClipped(style) {
'use strict';
const matchesClip = style.getPropertyValue('clip').match(clipRegex);
const matchesClipPath = style
.getPropertyValue('clip-path')
.match(clipPathRegex);
if (matchesClip && matchesClip.length === 5) {
return (
matchesClip[3] - matchesClip[1] <= 0 &&
matchesClip[2] - matchesClip[4] <= 0
);
}
if (matchesClipPath) {
const type = matchesClipPath[1];
const value = parseInt(matchesClipPath[2], 10);
switch (type) {
case 'inset':
return value >= 50;
case 'circle':
return value === 0;
default:
}
}
return false;
}
/**
* Check `AREA` element is visible
* - validate if it is a child of `map`
* - ensure `map` is referred by `img` using the `usemap` attribute
* @param {Element} areaEl `AREA` element
* @retruns {Boolean}
*/
function isAreaVisible(el, screenReader, recursed) {
/**
* Note:
* - Verified that `map` element cannot refer to `area` elements across different document trees
* - Verified that `map` element does not get affected by altering `display` property
*/
const mapEl = dom.findUp(el, 'map');
if (!mapEl) {
return false;
}
const mapElName = mapEl.getAttribute('name');
if (!mapElName) {
return false;
}
/**
* `map` element has to be in light DOM
*/
const mapElRootNode = dom.getRootNode(el);
if (!mapElRootNode || mapElRootNode.nodeType !== 9) {
return false;
}
const refs = axe.utils.querySelectorAll(
axe._tree,
`img[usemap="#${axe.utils.escapeSelector(mapElName)}"]`
);
if (!refs || !refs.length) {
return false;
}
return refs.some(({ actualNode }) =>
dom.isVisible(actualNode, screenReader, recursed)
);
}
/**
* Determine whether an element is visible
* @method isVisible
* @memberof axe.commons.dom
* @instance
* @param {HTMLElement} el The HTMLElement
* @param {Boolean} screenReader When provided, will evaluate visibility from the perspective of a screen reader
* @param {Boolean} recursed
* @return {Boolean} The element's visibilty status
*/
dom.isVisible = function(el, screenReader, recursed) {
'use strict';
const node = axe.utils.getNodeFromTree(el);
const cacheName = '_isVisible' + (screenReader ? 'ScreenReader' : '');
// 9 === Node.DOCUMENT
if (el.nodeType === 9) {
return true;
}
// 11 === Node.DOCUMENT_FRAGMENT_NODE
if (el.nodeType === 11) {
el = el.host; // grab the host Node
}
if (node && typeof node[cacheName] !== 'undefined') {
return node[cacheName];
}
const style = window.getComputedStyle(el, null);
if (style === null) {
return false;
}
const nodeName = el.nodeName.toUpperCase();
if (
/**
* Note:
* Firefox's user-agent always sets `AREA` element to `display:none`
* hence excluding the edge case, for visibility computation
*/
(nodeName !== 'AREA' && style.getPropertyValue('display') === 'none') ||
['STYLE', 'SCRIPT', 'NOSCRIPT', 'TEMPLATE'].includes(nodeName) ||
(!screenReader && isClipped(style)) ||
(!recursed &&
// visibility is only accurate on the first element
(style.getPropertyValue('visibility') === 'hidden' ||
// position does not matter if it was already calculated
(!screenReader && dom.isOffscreen(el)))) ||
(screenReader && el.getAttribute('aria-hidden') === 'true')
) {
return false;
}
/**
* check visibility of `AREA`
*/
if (nodeName === 'AREA') {
return isAreaVisible(el, screenReader, recursed);
}
const parent = el.assignedSlot ? el.assignedSlot : el.parentNode;
let isVisible = false;
if (parent) {
isVisible = dom.isVisible(parent, screenReader, true);
}
if (node) {
node[cacheName] = isVisible;
}
return isVisible;
};