@vaadin/a11y-base 24.1.0-alpha1 → 24.1.0-alpha10
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.
- package/package.json +3 -3
- package/src/aria-id-reference.d.ts +41 -0
- package/src/aria-id-reference.js +156 -0
- package/src/field-aria-controller.d.ts +8 -1
- package/src/field-aria-controller.js +53 -50
- package/src/focus-utils.d.ts +6 -0
- package/src/focus-utils.js +17 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vaadin/a11y-base",
|
|
3
|
-
"version": "24.1.0-
|
|
3
|
+
"version": "24.1.0-alpha10",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@open-wc/dedupe-mixin": "^1.3.0",
|
|
34
34
|
"@polymer/polymer": "^3.0.0",
|
|
35
|
-
"@vaadin/component-base": "24.1.0-
|
|
35
|
+
"@vaadin/component-base": "24.1.0-alpha10",
|
|
36
36
|
"lit": "^2.0.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
@@ -40,5 +40,5 @@
|
|
|
40
40
|
"@vaadin/testing-helpers": "^0.4.0",
|
|
41
41
|
"sinon": "^13.0.2"
|
|
42
42
|
},
|
|
43
|
-
"gitHead": "
|
|
43
|
+
"gitHead": "12e39be7eb3b49c68708e8ca3de2fb22e91051a1"
|
|
44
44
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2023 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type AriaIDReferenceConfig = {
|
|
8
|
+
newId: string | null;
|
|
9
|
+
oldId: string | null;
|
|
10
|
+
fromUser: boolean | null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Sets a new ID reference for a target element and an ARIA attribute.
|
|
15
|
+
*
|
|
16
|
+
* @param config.newId
|
|
17
|
+
* The new ARIA ID reference to set. If `null`, the attribute is removed,
|
|
18
|
+
* and `config.fromUser` is `true`, any stored values are restored. If there
|
|
19
|
+
* are stored values and `config.fromUser` is `false`, then `config.newId`
|
|
20
|
+
* is added to the stored values set.
|
|
21
|
+
* @param config.oldId
|
|
22
|
+
* The ARIA ID reference to be removed from the attribute. If there are stored
|
|
23
|
+
* values and `config.fromUser` is `false`, then `config.oldId` is removed from
|
|
24
|
+
* the stored values set.
|
|
25
|
+
* @param config.fromUser
|
|
26
|
+
* Indicates whether the function is called by the user or internally.
|
|
27
|
+
* When `config.fromUser` is called with `true` for the first time,
|
|
28
|
+
* the function will clear and store the attribute value for the given element.
|
|
29
|
+
*/
|
|
30
|
+
export function setAriaIDReference(target: HTMLElement, attr: string, config: AriaIDReferenceConfig): void;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Removes the attribute value of the given target element.
|
|
34
|
+
* It also stores the current value, if no stored values are present.
|
|
35
|
+
*/
|
|
36
|
+
export function removeAriaIDReference(target: HTMLElement, attr: string): void;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Restores the generated values of the attribute to the given target element.
|
|
40
|
+
*/
|
|
41
|
+
export function restoreGeneratedAriaIDReference(target: HTMLElement, attr: string): void;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2023 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
addValueToAttribute,
|
|
8
|
+
deserializeAttributeValue,
|
|
9
|
+
removeValueFromAttribute,
|
|
10
|
+
serializeAttributeValue,
|
|
11
|
+
} from '@vaadin/component-base/src/dom-utils.js';
|
|
12
|
+
|
|
13
|
+
const attributeToTargets = new Map();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Gets or creates a Set with the stored values for each element controlled by this helper
|
|
17
|
+
*
|
|
18
|
+
* @param {string} attr the attribute name used as key in the map
|
|
19
|
+
*
|
|
20
|
+
* @returns {WeakMap<HTMLElement, Set<string>} a weak map with the stored values for the elements being controlled by the helper
|
|
21
|
+
*/
|
|
22
|
+
function getAttrMap(attr) {
|
|
23
|
+
if (!attributeToTargets.has(attr)) {
|
|
24
|
+
attributeToTargets.set(attr, new WeakMap());
|
|
25
|
+
}
|
|
26
|
+
return attributeToTargets.get(attr);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Cleans the values set on the attribute to the given element.
|
|
31
|
+
* It also stores the current values in the map, if `storeValue` is `true`.
|
|
32
|
+
*
|
|
33
|
+
* @param {HTMLElement} target
|
|
34
|
+
* @param {string} attr the attribute to be cleared
|
|
35
|
+
* @param {boolean} storeValue whether or not the current value of the attribute should be stored on the map
|
|
36
|
+
* @returns
|
|
37
|
+
*/
|
|
38
|
+
function cleanAriaIDReference(target, attr) {
|
|
39
|
+
if (!target) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
target.removeAttribute(attr);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Storing values of the accessible attributes in a Set inside of the WeakMap.
|
|
48
|
+
*
|
|
49
|
+
* @param {HTMLElement} target
|
|
50
|
+
* @param {string} attr the attribute to be stored
|
|
51
|
+
*/
|
|
52
|
+
function storeAriaIDReference(target, attr) {
|
|
53
|
+
if (!target || !attr) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const attributeMap = getAttrMap(attr);
|
|
57
|
+
if (attributeMap.has(target)) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const values = deserializeAttributeValue(target.getAttribute(attr));
|
|
61
|
+
attributeMap.set(target, new Set(values));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Restores the generated values of the attribute to the given element.
|
|
66
|
+
*
|
|
67
|
+
* @param {HTMLElement} target
|
|
68
|
+
* @param {string} attr
|
|
69
|
+
*/
|
|
70
|
+
export function restoreGeneratedAriaIDReference(target, attr) {
|
|
71
|
+
if (!target || !attr) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const attributeMap = getAttrMap(attr);
|
|
75
|
+
const values = attributeMap.get(target);
|
|
76
|
+
if (!values || values.size === 0) {
|
|
77
|
+
target.removeAttribute(attr);
|
|
78
|
+
} else {
|
|
79
|
+
addValueToAttribute(target, attr, serializeAttributeValue(values));
|
|
80
|
+
}
|
|
81
|
+
attributeMap.delete(target);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Sets a new ID reference for a target element and an ARIA attribute.
|
|
86
|
+
*
|
|
87
|
+
* @typedef {Object} AriaIdReferenceConfig
|
|
88
|
+
* @property {string | null | undefined} newId
|
|
89
|
+
* @property {string | null | undefined} oldId
|
|
90
|
+
* @property {boolean | null | undefined} fromUser
|
|
91
|
+
* @param {HTMLElement} target
|
|
92
|
+
* @param {string} attr
|
|
93
|
+
* @param {AriaIdReferenceConfig | null | undefined} config
|
|
94
|
+
* @param config.newId The new ARIA ID reference to set. If `null`, the attribute is removed,
|
|
95
|
+
* and `config.fromUser` is true, any stored values are restored. If there are stored values
|
|
96
|
+
* and `config.fromUser` is `false`, then `config.newId` is added to the stored values set.
|
|
97
|
+
* @param config.oldId The ARIA ID reference to be removed from the attribute. If there are
|
|
98
|
+
* stored values and `config.fromUser` is `false`, then `config.oldId` is removed from the
|
|
99
|
+
* stored values set.
|
|
100
|
+
* @param config.fromUser Indicates whether the function is called by the user or internally.
|
|
101
|
+
* When `config.fromUser` is called with `true` for the first time, the function will clear
|
|
102
|
+
* and store the attribute value for the given element.
|
|
103
|
+
*/
|
|
104
|
+
export function setAriaIDReference(target, attr, config = { newId: null, oldId: null, fromUser: false }) {
|
|
105
|
+
if (!target || !attr) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const { newId, oldId, fromUser } = config;
|
|
110
|
+
|
|
111
|
+
const attributeMap = getAttrMap(attr);
|
|
112
|
+
const storedValues = attributeMap.get(target);
|
|
113
|
+
|
|
114
|
+
if (!fromUser && !!storedValues) {
|
|
115
|
+
// If there's any stored values, it means the attribute is being handled by the user
|
|
116
|
+
// Replace the "oldId" with "newId" on the stored values set and leave
|
|
117
|
+
oldId && storedValues.delete(oldId);
|
|
118
|
+
newId && storedValues.add(newId);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (fromUser) {
|
|
123
|
+
if (!storedValues) {
|
|
124
|
+
// If it's called from user and there's no stored values for the attribute,
|
|
125
|
+
// then store the current value
|
|
126
|
+
storeAriaIDReference(target, attr);
|
|
127
|
+
} else if (!newId) {
|
|
128
|
+
// If called from user with newId == null, it means the attribute will no longer
|
|
129
|
+
// be in control of the user and the stored values should be restored
|
|
130
|
+
// Removing the entry on the map for this target
|
|
131
|
+
attributeMap.delete(target);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// If it's from user, then clear the attribute value before setting newId
|
|
135
|
+
cleanAriaIDReference(target, attr);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
removeValueFromAttribute(target, attr, oldId);
|
|
139
|
+
|
|
140
|
+
const attributeValue = !newId ? serializeAttributeValue(storedValues) : newId;
|
|
141
|
+
if (attributeValue) {
|
|
142
|
+
addValueToAttribute(target, attr, attributeValue);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Removes the {@link attr | attribute} value of the given {@link target} element.
|
|
148
|
+
* It also stores the current value, if no stored values are present.
|
|
149
|
+
*
|
|
150
|
+
* @param {HTMLElement} target
|
|
151
|
+
* @param {string} attr
|
|
152
|
+
*/
|
|
153
|
+
export function removeAriaIDReference(target, attr) {
|
|
154
|
+
storeAriaIDReference(target, attr);
|
|
155
|
+
cleanAriaIDReference(target, attr);
|
|
156
|
+
}
|
|
@@ -28,13 +28,20 @@ export class FieldAriaController {
|
|
|
28
28
|
*/
|
|
29
29
|
setRequired(required: boolean): void;
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Defines the `aria-label` attribute of the target element.
|
|
33
|
+
*
|
|
34
|
+
* To remove the attribute, pass `null` as `label`.
|
|
35
|
+
*/
|
|
36
|
+
setAriaLabel(label: string | null): void;
|
|
37
|
+
|
|
31
38
|
/**
|
|
32
39
|
* Links the target element with a slotted label element
|
|
33
40
|
* via the target's attribute `aria-labelledby`.
|
|
34
41
|
*
|
|
35
42
|
* To unlink the previous slotted label element, pass `null` as `labelId`.
|
|
36
43
|
*/
|
|
37
|
-
setLabelId(labelId: string | null): void;
|
|
44
|
+
setLabelId(labelId: string | null, fromUser: boolean | null): void;
|
|
38
45
|
|
|
39
46
|
/**
|
|
40
47
|
* Links the target element with a slotted error element via the target's attribute:
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
* Copyright (c) 2021 - 2023 Vaadin Ltd.
|
|
4
4
|
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
removeAriaIDReference,
|
|
8
|
+
restoreGeneratedAriaIDReference,
|
|
9
|
+
setAriaIDReference,
|
|
10
|
+
} from '@vaadin/a11y-base/src/aria-id-reference.js';
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
13
|
* A controller for managing ARIA attributes for a field element:
|
|
@@ -15,16 +19,6 @@ export class FieldAriaController {
|
|
|
15
19
|
this.__required = false;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
|
-
/**
|
|
19
|
-
* `true` if the target element is the host component itself, `false` otherwise.
|
|
20
|
-
*
|
|
21
|
-
* @return {boolean}
|
|
22
|
-
* @private
|
|
23
|
-
*/
|
|
24
|
-
get __isGroupField() {
|
|
25
|
-
return this.__target === this.host;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
22
|
/**
|
|
29
23
|
* Sets a target element to which ARIA attributes are added.
|
|
30
24
|
*
|
|
@@ -33,9 +27,14 @@ export class FieldAriaController {
|
|
|
33
27
|
setTarget(target) {
|
|
34
28
|
this.__target = target;
|
|
35
29
|
this.__setAriaRequiredAttribute(this.__required);
|
|
36
|
-
|
|
30
|
+
// We need to make sure that value in __labelId is stored
|
|
31
|
+
this.__setLabelIdToAriaAttribute(this.__labelId, this.__labelId);
|
|
32
|
+
if (this.__labelIdFromUser != null) {
|
|
33
|
+
this.__setLabelIdToAriaAttribute(this.__labelIdFromUser, this.__labelIdFromUser, true);
|
|
34
|
+
}
|
|
37
35
|
this.__setErrorIdToAriaAttribute(this.__errorId);
|
|
38
36
|
this.__setHelperIdToAriaAttribute(this.__helperId);
|
|
37
|
+
this.setAriaLabel(this.__label);
|
|
39
38
|
}
|
|
40
39
|
|
|
41
40
|
/**
|
|
@@ -50,6 +49,18 @@ export class FieldAriaController {
|
|
|
50
49
|
this.__required = required;
|
|
51
50
|
}
|
|
52
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Defines the `aria-label` attribute of the target element.
|
|
54
|
+
*
|
|
55
|
+
* To remove the attribute, pass `null` as `label`.
|
|
56
|
+
*
|
|
57
|
+
* @param {string | null | undefined} label
|
|
58
|
+
*/
|
|
59
|
+
setAriaLabel(label) {
|
|
60
|
+
this.__setAriaLabelToAttribute(label);
|
|
61
|
+
this.__label = label;
|
|
62
|
+
}
|
|
63
|
+
|
|
53
64
|
/**
|
|
54
65
|
* Links the target element with a slotted label element
|
|
55
66
|
* via the target's attribute `aria-labelledby`.
|
|
@@ -58,9 +69,14 @@ export class FieldAriaController {
|
|
|
58
69
|
*
|
|
59
70
|
* @param {string | null} labelId
|
|
60
71
|
*/
|
|
61
|
-
setLabelId(labelId) {
|
|
62
|
-
this.
|
|
63
|
-
this.
|
|
72
|
+
setLabelId(labelId, fromUser = false) {
|
|
73
|
+
const oldLabelId = fromUser ? this.__labelIdFromUser : this.__labelId;
|
|
74
|
+
this.__setLabelIdToAriaAttribute(labelId, oldLabelId, fromUser);
|
|
75
|
+
if (fromUser) {
|
|
76
|
+
this.__labelIdFromUser = labelId;
|
|
77
|
+
} else {
|
|
78
|
+
this.__labelId = labelId;
|
|
79
|
+
}
|
|
64
80
|
}
|
|
65
81
|
|
|
66
82
|
/**
|
|
@@ -91,13 +107,31 @@ export class FieldAriaController {
|
|
|
91
107
|
this.__helperId = helperId;
|
|
92
108
|
}
|
|
93
109
|
|
|
110
|
+
/**
|
|
111
|
+
* @param {string | null | undefined} label
|
|
112
|
+
* @private
|
|
113
|
+
* */
|
|
114
|
+
__setAriaLabelToAttribute(label) {
|
|
115
|
+
if (!this.__target) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (label) {
|
|
119
|
+
removeAriaIDReference(this.__target, 'aria-labelledby');
|
|
120
|
+
this.__target.setAttribute('aria-label', label);
|
|
121
|
+
} else if (this.__label) {
|
|
122
|
+
restoreGeneratedAriaIDReference(this.__target, 'aria-labelledby');
|
|
123
|
+
this.__target.removeAttribute('aria-label');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
94
127
|
/**
|
|
95
128
|
* @param {string | null | undefined} labelId
|
|
96
129
|
* @param {string | null | undefined} oldLabelId
|
|
130
|
+
* @param {boolean | null | undefined} fromUser
|
|
97
131
|
* @private
|
|
98
132
|
*/
|
|
99
|
-
__setLabelIdToAriaAttribute(labelId, oldLabelId) {
|
|
100
|
-
this.
|
|
133
|
+
__setLabelIdToAriaAttribute(labelId, oldLabelId, fromUser) {
|
|
134
|
+
setAriaIDReference(this.__target, 'aria-labelledby', { newId: labelId, oldId: oldLabelId, fromUser });
|
|
101
135
|
}
|
|
102
136
|
|
|
103
137
|
/**
|
|
@@ -106,13 +140,7 @@ export class FieldAriaController {
|
|
|
106
140
|
* @private
|
|
107
141
|
*/
|
|
108
142
|
__setErrorIdToAriaAttribute(errorId, oldErrorId) {
|
|
109
|
-
|
|
110
|
-
// that should guarantee that it's announced when the group is entered.
|
|
111
|
-
if (this.__isGroupField) {
|
|
112
|
-
this.__setAriaAttributeId('aria-labelledby', errorId, oldErrorId);
|
|
113
|
-
} else {
|
|
114
|
-
this.__setAriaAttributeId('aria-describedby', errorId, oldErrorId);
|
|
115
|
-
}
|
|
143
|
+
setAriaIDReference(this.__target, 'aria-describedby', { newId: errorId, oldId: oldErrorId, fromUser: false });
|
|
116
144
|
}
|
|
117
145
|
|
|
118
146
|
/**
|
|
@@ -121,13 +149,7 @@ export class FieldAriaController {
|
|
|
121
149
|
* @private
|
|
122
150
|
*/
|
|
123
151
|
__setHelperIdToAriaAttribute(helperId, oldHelperId) {
|
|
124
|
-
|
|
125
|
-
// that should guarantee that it's announced when the group is entered.
|
|
126
|
-
if (this.__isGroupField) {
|
|
127
|
-
this.__setAriaAttributeId('aria-labelledby', helperId, oldHelperId);
|
|
128
|
-
} else {
|
|
129
|
-
this.__setAriaAttributeId('aria-describedby', helperId, oldHelperId);
|
|
130
|
-
}
|
|
152
|
+
setAriaIDReference(this.__target, 'aria-describedby', { newId: helperId, oldId: oldHelperId, fromUser: false });
|
|
131
153
|
}
|
|
132
154
|
|
|
133
155
|
/**
|
|
@@ -150,23 +172,4 @@ export class FieldAriaController {
|
|
|
150
172
|
this.__target.removeAttribute('aria-required');
|
|
151
173
|
}
|
|
152
174
|
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* @param {string | null | undefined} newId
|
|
156
|
-
* @param {string | null | undefined} oldId
|
|
157
|
-
* @private
|
|
158
|
-
*/
|
|
159
|
-
__setAriaAttributeId(attr, newId, oldId) {
|
|
160
|
-
if (!this.__target) {
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (oldId) {
|
|
165
|
-
removeValueFromAttribute(this.__target, attr, oldId);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (newId) {
|
|
169
|
-
addValueToAttribute(this.__target, attr, newId);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
175
|
}
|
package/src/focus-utils.d.ts
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Returns the actually focused element by traversing shadow
|
|
9
|
+
* trees recursively to ensure it's the leaf element.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getDeepActiveElement(): Element;
|
|
12
|
+
|
|
7
13
|
/**
|
|
8
14
|
* Returns true if the window has received a keydown
|
|
9
15
|
* event since the last mousedown event.
|
package/src/focus-utils.js
CHANGED
|
@@ -26,6 +26,20 @@ window.addEventListener(
|
|
|
26
26
|
{ capture: true },
|
|
27
27
|
);
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Returns the actually focused element by traversing shadow
|
|
31
|
+
* trees recursively to ensure it's the leaf element.
|
|
32
|
+
*
|
|
33
|
+
* @return {Element}
|
|
34
|
+
*/
|
|
35
|
+
export function getDeepActiveElement() {
|
|
36
|
+
let host = document.activeElement || document.body;
|
|
37
|
+
while (host.shadowRoot && host.shadowRoot.activeElement) {
|
|
38
|
+
host = host.shadowRoot.activeElement;
|
|
39
|
+
}
|
|
40
|
+
return host;
|
|
41
|
+
}
|
|
42
|
+
|
|
29
43
|
/**
|
|
30
44
|
* Returns true if the window has received a keydown
|
|
31
45
|
* event since the last mousedown event.
|
|
@@ -134,7 +148,9 @@ export function isElementHidden(element) {
|
|
|
134
148
|
// `offsetParent` is `null` when the element itself
|
|
135
149
|
// or one of its ancestors is hidden with `display: none`.
|
|
136
150
|
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
|
|
137
|
-
|
|
151
|
+
// However `offsetParent` is also null when the element is using fixed
|
|
152
|
+
// positioning, so additionally check if the element takes up layout space.
|
|
153
|
+
if (element.offsetParent === null && element.clientWidth === 0 && element.clientHeight === 0) {
|
|
138
154
|
return true;
|
|
139
155
|
}
|
|
140
156
|
|