Skip to content

Commit 6091367

Browse files
strakerWilcoFiers
andauthored
fix(target-size): ignore descendant elements in shadow dom (#4410)
This also adds to the `.eslintrc` to error if `node.contains()` or `vNode.actualNode.contains()` or used. (also upgraded the `node.attributes` error to account for `vNode.actualNode.attributes` as I noticed it was missing). Closes: #4194 --------- Co-authored-by: Wilco Fiers <[email protected]>
1 parent 3a90bb7 commit 6091367

File tree

8 files changed

+222
-63
lines changed

8 files changed

+222
-63
lines changed

.eslintrc.js

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,37 @@ module.exports = {
5959
selector: 'MemberExpression[property.name=tagName]',
6060
message: "Don't use node.tagName, use node.nodeName instead."
6161
},
62+
// node.attributes can be clobbered so is unsafe to use
63+
// @see https://github.com/dequelabs/axe-core/pull/1432
6264
{
63-
// node.attributes can be clobbered so is unsafe to use
64-
// @see https://github.com/dequelabs/axe-core/pull/1432
65+
// node.attributes
6566
selector:
6667
'MemberExpression[object.name=node][property.name=attributes]',
6768
message:
6869
"Don't use node.attributes, use node.hasAttributes() or axe.utils.getNodeAttributes(node) instead."
70+
},
71+
{
72+
// vNode.actualNode.attributes
73+
selector:
74+
'MemberExpression[object.property.name=actualNode][property.name=attributes]',
75+
message:
76+
"Don't use node.attributes, use node.hasAttributes() or axe.utils.getNodeAttributes(node) instead."
77+
},
78+
// node.contains doesn't work with shadow dom
79+
// @see https://github.com/dequelabs/axe-core/issues/4194
80+
{
81+
// node.contains()
82+
selector:
83+
'CallExpression[callee.object.name=node][callee.property.name=contains]',
84+
message:
85+
"Don't use node.contains(node2) as it doesn't work across shadow DOM. Use axe.utils.contains(node, node2) instead."
86+
},
87+
{
88+
// vNode.actualNode.contains()
89+
selector:
90+
'CallExpression[callee.object.property.name=actualNode][callee.property.name=contains]',
91+
message:
92+
"Don't use node.contains(node2) as it doesn't work across shadow DOM. Use axe.utils.contains(node, node2) instead."
6993
}
7094
]
7195
},

lib/checks/mobile/target-size-evaluate.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
rectHasMinimumSize,
66
hasVisualOverlap
77
} from '../../commons/math';
8+
import { contains } from '../../core/utils';
89

910
/**
1011
* Determine if an element has a minimum size, taking into account
@@ -187,9 +188,7 @@ function toDecimalSize(rect) {
187188
}
188189

189190
function isDescendantNotInTabOrder(vAncestor, vNode) {
190-
return (
191-
vAncestor.actualNode.contains(vNode.actualNode) && !isInTabOrder(vNode)
192-
);
191+
return contains(vAncestor, vNode) && !isInTabOrder(vNode);
193192
}
194193

195194
function mapActualNodes(vNodes) {

lib/commons/dom/get-target-rects.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import findNearbyElms from './find-nearby-elms';
22
import isInTabOrder from './is-in-tab-order';
33
import { splitRects, hasVisualOverlap } from '../math';
44
import memoize from '../../core/utils/memoize';
5+
import { contains } from '../../core/utils';
56

67
export default memoize(getTargetRects);
78

@@ -32,7 +33,5 @@ function getTargetRects(vNode) {
3233
}
3334

3435
function isDescendantNotInTabOrder(vAncestor, vNode) {
35-
return (
36-
vAncestor.actualNode.contains(vNode.actualNode) && !isInTabOrder(vNode)
37-
);
36+
return contains(vAncestor, vNode) && !isInTabOrder(vNode);
3837
}

lib/core/utils/contains.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ export default function contains(vNode, otherVNode) {
1212
!vNode.shadowId &&
1313
!otherVNode.shadowId &&
1414
vNode.actualNode &&
15+
// eslint-disable-next-line no-restricted-syntax
1516
typeof vNode.actualNode.contains === 'function'
1617
) {
18+
// eslint-disable-next-line no-restricted-syntax
1719
return vNode.actualNode.contains(otherVNode.actualNode);
1820
}
1921

test/checks/mobile/target-size.js

Lines changed: 68 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,22 @@
1-
describe('target-size tests', function () {
2-
'use strict';
3-
4-
var checkContext = axe.testUtils.MockCheckContext();
5-
var checkSetup = axe.testUtils.checkSetup;
6-
var shadowCheckSetup = axe.testUtils.shadowCheckSetup;
7-
var check = checks['target-size'];
1+
describe('target-size tests', () => {
2+
const checkContext = axe.testUtils.MockCheckContext();
3+
const checkSetup = axe.testUtils.checkSetup;
4+
const shadowCheckSetup = axe.testUtils.shadowCheckSetup;
5+
const check = checks['target-size'];
6+
const fixture = document.querySelector('#fixture');
87

98
function elmIds(elms) {
10-
return Array.from(elms).map(function (elm) {
9+
return Array.from(elms).map(elm => {
1110
return '#' + elm.id;
1211
});
1312
}
1413

15-
afterEach(function () {
14+
afterEach(() => {
1615
checkContext.reset();
1716
});
1817

19-
it('returns false for targets smaller than minSize', function () {
20-
var checkArgs = checkSetup(
18+
it('returns false for targets smaller than minSize', () => {
19+
const checkArgs = checkSetup(
2120
'<button id="target" style="' +
2221
'display: inline-block; width:20px; height:30px;' +
2322
'">x</button>'
@@ -30,8 +29,8 @@ describe('target-size tests', function () {
3029
});
3130
});
3231

33-
it('returns undefined for non-tabbable targets smaller than minSize', function () {
34-
var checkArgs = checkSetup(
32+
it('returns undefined for non-tabbable targets smaller than minSize', () => {
33+
const checkArgs = checkSetup(
3534
'<button id="target" tabindex="-1" style="' +
3635
'display: inline-block; width:20px; height:30px;' +
3736
'">x</button>'
@@ -44,8 +43,8 @@ describe('target-size tests', function () {
4443
});
4544
});
4645

47-
it('returns true for unobscured targets larger than minSize', function () {
48-
var checkArgs = checkSetup(
46+
it('returns true for unobscured targets larger than minSize', () => {
47+
const checkArgs = checkSetup(
4948
'<button id="target" style="' +
5049
'display: inline-block; width:40px; height:30px;' +
5150
'">x</button>'
@@ -58,8 +57,8 @@ describe('target-size tests', function () {
5857
});
5958
});
6059

61-
it('returns true for very large targets', function () {
62-
var checkArgs = checkSetup(
60+
it('returns true for very large targets', () => {
61+
const checkArgs = checkSetup(
6362
'<button id="target" style="' +
6463
'display: inline-block; width:240px; height:300px;' +
6564
'">x</button>'
@@ -68,9 +67,9 @@ describe('target-size tests', function () {
6867
assert.deepEqual(checkContext._data, { messageKey: 'large', minSize: 24 });
6968
});
7069

71-
describe('when fully obscured', function () {
72-
it('returns true, regardless of size', function () {
73-
var checkArgs = checkSetup(
70+
describe('when fully obscured', () => {
71+
it('returns true, regardless of size', () => {
72+
const checkArgs = checkSetup(
7473
'<a href="#" id="target" style="' +
7574
'display: inline-block; width:20px; height:20px;' +
7675
'">x</a>' +
@@ -83,8 +82,8 @@ describe('target-size tests', function () {
8382
assert.deepEqual(elmIds(checkContext._relatedNodes), ['#obscurer']);
8483
});
8584

86-
it('returns true when obscured by another focusable widget', function () {
87-
var checkArgs = checkSetup(
85+
it('returns true when obscured by another focusable widget', () => {
86+
const checkArgs = checkSetup(
8887
'<a href="#" id="target" style="' +
8988
'display: inline-block; width:20px; height:20px;' +
9089
'">x</a>' +
@@ -97,8 +96,8 @@ describe('target-size tests', function () {
9796
assert.deepEqual(elmIds(checkContext._relatedNodes), ['#obscurer']);
9897
});
9998

100-
it('ignores obscuring element has pointer-events:none', function () {
101-
var checkArgs = checkSetup(
99+
it('ignores obscuring element has pointer-events:none', () => {
100+
const checkArgs = checkSetup(
102101
'<a href="#" id="target" style="' +
103102
'display: inline-block; width:20px; height:20px;' +
104103
'">x</a>' +
@@ -115,9 +114,9 @@ describe('target-size tests', function () {
115114
});
116115
});
117116

118-
describe('when partially obscured', function () {
119-
it('returns true for focusable non-widgets', function () {
120-
var checkArgs = checkSetup(
117+
describe('when partially obscured', () => {
118+
it('returns true for focusable non-widgets', () => {
119+
const checkArgs = checkSetup(
121120
'<button id="target" style="' +
122121
'display: inline-block; width:40px; height:30px; margin-left:30px;' +
123122
'">x</button>' +
@@ -137,8 +136,8 @@ describe('target-size tests', function () {
137136
assert.deepEqual(elmIds(checkContext._relatedNodes), ['#obscurer']);
138137
});
139138

140-
it('returns true for non-focusable widgets', function () {
141-
var checkArgs = checkSetup(
139+
it('returns true for non-focusable widgets', () => {
140+
const checkArgs = checkSetup(
142141
'<button id="target" style="' +
143142
'display: inline-block; width:40px; height:30px; margin-left:30px;' +
144143
'">x</button>' +
@@ -158,9 +157,9 @@ describe('target-size tests', function () {
158157
assert.deepEqual(elmIds(checkContext._relatedNodes), ['#obscurer']);
159158
});
160159

161-
describe('by a focusable widget', function () {
162-
it('returns true for obscured targets with sufficient space', function () {
163-
var checkArgs = checkSetup(
160+
describe('by a focusable widget', () => {
161+
it('returns true for obscured targets with sufficient space', () => {
162+
const checkArgs = checkSetup(
164163
'<button id="target" style="' +
165164
'display: inline-block; width:40px; height:30px;' +
166165
'">x</button>' +
@@ -202,8 +201,8 @@ describe('target-size tests', function () {
202201
});
203202

204203
describe('for obscured targets with insufficient space', () => {
205-
it('returns false if all elements are tabbable', function () {
206-
var checkArgs = checkSetup(
204+
it('returns false if all elements are tabbable', () => {
205+
const checkArgs = checkSetup(
207206
'<button id="target" style="' +
208207
'display: inline-block; width:40px; height:30px; margin-left:30px;' +
209208
'">x</button>' +
@@ -227,8 +226,8 @@ describe('target-size tests', function () {
227226
]);
228227
});
229228

230-
it('returns undefined if the target is not tabbable', function () {
231-
var checkArgs = checkSetup(
229+
it('returns undefined if the target is not tabbable', () => {
230+
const checkArgs = checkSetup(
232231
'<button id="target" tabindex="-1" style="' +
233232
'display: inline-block; width:40px; height:30px; margin-left:30px;' +
234233
'">x</button>' +
@@ -252,8 +251,8 @@ describe('target-size tests', function () {
252251
]);
253252
});
254253

255-
it('returns undefined if the obscuring node is not tabbable', function () {
256-
var checkArgs = checkSetup(
254+
it('returns undefined if the obscuring node is not tabbable', () => {
255+
const checkArgs = checkSetup(
257256
'<button id="target" style="' +
258257
'display: inline-block; width:40px; height:30px; margin-left:30px;' +
259258
'">x</button>' +
@@ -279,8 +278,8 @@ describe('target-size tests', function () {
279278
});
280279

281280
describe('that is a descendant', () => {
282-
it('returns false if the widget is tabbable', function () {
283-
var checkArgs = checkSetup(
281+
it('returns false if the widget is tabbable', () => {
282+
const checkArgs = checkSetup(
284283
`<a role="link" aria-label="play" tabindex="0" style="display:inline-block" id="target">
285284
<button style="margin:1px; line-height:20px">Play</button>
286285
</a>`
@@ -289,8 +288,8 @@ describe('target-size tests', function () {
289288
assert.isFalse(out);
290289
});
291290

292-
it('returns true if the widget is not tabbable', function () {
293-
var checkArgs = checkSetup(
291+
it('returns true if the widget is not tabbable', () => {
292+
const checkArgs = checkSetup(
294293
`<a role="link" aria-label="play" tabindex="0" style="display:inline-block" id="target">
295294
<button tabindex="-1" style="margin:1px; line-height:20px">Play</button>
296295
</a>`
@@ -301,8 +300,8 @@ describe('target-size tests', function () {
301300
});
302301

303302
describe('that is a descendant', () => {
304-
it('returns false if the widget is tabbable', function () {
305-
var checkArgs = checkSetup(
303+
it('returns false if the widget is tabbable', () => {
304+
const checkArgs = checkSetup(
306305
`<a role="link" aria-label="play" tabindex="0" style="display:inline-block" id="target">
307306
<button style="margin:1px; line-height:20px">Play</button>
308307
</a>`
@@ -311,8 +310,8 @@ describe('target-size tests', function () {
311310
assert.isFalse(out);
312311
});
313312

314-
it('returns true if the widget is not tabbable', function () {
315-
var checkArgs = checkSetup(
313+
it('returns true if the widget is not tabbable', () => {
314+
const checkArgs = checkSetup(
316315
`<a role="link" aria-label="play" tabindex="0" style="display:inline-block" id="target">
317316
<button tabindex="-1" style="margin:1px; line-height:20px">Play</button>
318317
</a>`
@@ -324,9 +323,9 @@ describe('target-size tests', function () {
324323
});
325324
});
326325

327-
describe('with overflowing content', function () {
326+
describe('with overflowing content', () => {
328327
it('returns undefined target is too small', () => {
329-
var checkArgs = checkSetup(
328+
const checkArgs = checkSetup(
330329
'<a href="#" id="target"><img width="24" height="24"></a>'
331330
);
332331
assert.isUndefined(check.evaluate.apply(checkContext, checkArgs));
@@ -337,15 +336,15 @@ describe('target-size tests', function () {
337336
});
338337

339338
it('returns true if target has sufficient size', () => {
340-
var checkArgs = checkSetup(
339+
const checkArgs = checkSetup(
341340
'<a href="#" id="target" style="font-size:24px;"><img width="24" height="24"></a>'
342341
);
343342
assert.isTrue(check.evaluate.apply(checkContext, checkArgs));
344343
});
345344

346345
describe('and partially obscured', () => {
347346
it('is undefined when unobscured area is too small', () => {
348-
var checkArgs = checkSetup(
347+
const checkArgs = checkSetup(
349348
'<a href="#" id="target" style="font-size:24px;">' +
350349
' <img width="24" height="36" style="vertical-align: bottom;">' +
351350
'</a><br>' +
@@ -359,7 +358,7 @@ describe('target-size tests', function () {
359358
});
360359

361360
it('is true when unobscured area is sufficient', () => {
362-
var checkArgs = checkSetup(
361+
const checkArgs = checkSetup(
363362
'<a href="#" id="target" style="font-size:24px;">' +
364363
' <img width="24" height="36" style="vertical-align: bottom;">' +
365364
'</a><br>' +
@@ -371,7 +370,7 @@ describe('target-size tests', function () {
371370

372371
describe('and fully obscured', () => {
373372
it('is undefined', () => {
374-
var checkArgs = checkSetup(
373+
const checkArgs = checkSetup(
375374
'<a href="#" id="target" style="font-size:24px;">' +
376375
' <img width="24" height="36" style="vertical-align: bottom;">' +
377376
'</a><br>' +
@@ -386,8 +385,8 @@ describe('target-size tests', function () {
386385
});
387386
});
388387

389-
it('works across shadow boundaries', function () {
390-
var checkArgs = shadowCheckSetup(
388+
it('works across shadow boundaries', () => {
389+
const checkArgs = shadowCheckSetup(
391390
'<span id="shadow"></span>' +
392391
'<button id="obscurer1" style="' +
393392
'display: inline-block; width:40px; height:30px; margin-left: -10px;' +
@@ -411,4 +410,19 @@ describe('target-size tests', function () {
411410
'#obscurer2'
412411
]);
413412
});
413+
414+
it('ignores descendants of the target that are in shadow dom', () => {
415+
fixture.innerHTML =
416+
'<button id="target" style="width: 30px; height: 40px; position: absolute; left: 10px; top: 5px"><span id="shadow"></span></button>';
417+
const target = fixture.querySelector('#target');
418+
const shadow = fixture
419+
.querySelector('#shadow')
420+
.attachShadow({ mode: 'open' });
421+
shadow.innerHTML =
422+
'<div style="position: absolute; left: 5px; top: 5px; width: 50px; height: 50px;"></div>';
423+
424+
axe.setup(fixture);
425+
const vNode = axe.utils.getNodeFromTree(target);
426+
assert.isTrue(check.evaluate.apply(checkContext, [target, {}, vNode]));
427+
});
414428
});

0 commit comments

Comments
 (0)