Skip to content

Commit 99d1e77

Browse files
camihaWilcoFiers
authored andcommitted
fix(aria): prevent getOwnedVirtual from returning duplicate nodes (#4987)
When a child element is also referenced via aria-owns, getOwnedVirtual returned the same node twice. Filter aria-owns references that already exist in children using Set. Closes: #4840 --------- Co-authored-by: Wilco Fiers <WilcoFiers@users.noreply.github.com>
1 parent 88bc57f commit 99d1e77

2 files changed

Lines changed: 65 additions & 1 deletion

File tree

lib/commons/aria/get-owned-virtual.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,15 @@ function getOwnedVirtual(virtualNode) {
1818
const owns = idrefs(actualNode, 'aria-owns')
1919
.filter(element => !!element)
2020
.map(element => axe.utils.getNodeFromTree(element));
21-
return [...children, ...owns];
21+
22+
// Deduplicates by first occurrence to match browser accessibility tree behavior
23+
// See: https://github.com/dequelabs/axe-core/pull/4987
24+
const uniqueOwns = owns.filter((own, index) => owns.indexOf(own) === index);
25+
const nativeChildren = children.filter(
26+
child => !uniqueOwns.includes(child)
27+
);
28+
29+
return [...nativeChildren, ...uniqueOwns];
2230
}
2331

2432
return [...children];

test/commons/aria/get-owned-virtual.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,62 @@ describe('aria.getOwnedVirtual', function () {
3737
assert.equal(owned[3].actualNode.nodeName.toUpperCase(), 'H4');
3838
});
3939

40+
it('does not return duplicate when child is also aria-owned', function () {
41+
fixtureSetup(
42+
'<div role="tablist" id="target" aria-owns="foo">' +
43+
'<div id="foo" role="menuitem">foo</div>' +
44+
'</div>'
45+
);
46+
var target = axe.utils.querySelectorAll(axe._tree[0], '#target')[0];
47+
var owned = aria.getOwnedVirtual(target);
48+
assert.lengthOf(owned, 1);
49+
assert.equal(owned[0].actualNode.id, 'foo');
50+
});
51+
52+
it('does not return duplicate when same ID appears multiple times in aria-owns', function () {
53+
fixtureSetup(
54+
'<div role="list" id="target" aria-owns="a b a">' +
55+
'</div>' +
56+
'<div role="listitem" id="a">A</div>' +
57+
'<div role="listitem" id="b">B</div>'
58+
);
59+
var target = axe.utils.querySelectorAll(axe._tree[0], '#target')[0];
60+
var owned = aria.getOwnedVirtual(target);
61+
assert.lengthOf(owned, 2);
62+
assert.equal(owned[0].actualNode.id, 'a');
63+
assert.equal(owned[1].actualNode.id, 'b');
64+
});
65+
66+
it('moves aria-owned child to the end', function () {
67+
fixtureSetup(
68+
'<div role="list" id="target" aria-owns="a">' +
69+
'<div role="listitem" id="a">A</div>' +
70+
'<div role="listitem" id="b">B</div>' +
71+
'</div>'
72+
);
73+
var target = axe.utils.querySelectorAll(axe._tree[0], '#target')[0];
74+
var owned = aria.getOwnedVirtual(target);
75+
assert.lengthOf(owned, 2);
76+
assert.equal(owned[0].actualNode.id, 'b');
77+
assert.equal(owned[1].actualNode.id, 'a');
78+
});
79+
80+
it('moves multiple aria-owned children to the end in aria-owns order', function () {
81+
fixtureSetup(
82+
'<div role="list" id="target" aria-owns="c a">' +
83+
'<div role="listitem" id="a">A</div>' +
84+
'<div role="listitem" id="b">B</div>' +
85+
'<div role="listitem" id="c">C</div>' +
86+
'</div>'
87+
);
88+
var target = axe.utils.querySelectorAll(axe._tree[0], '#target')[0];
89+
var owned = aria.getOwnedVirtual(target);
90+
assert.lengthOf(owned, 3);
91+
assert.equal(owned[0].actualNode.id, 'b');
92+
assert.equal(owned[1].actualNode.id, 'c');
93+
assert.equal(owned[2].actualNode.id, 'a');
94+
});
95+
4096
it('ignores whitespace-only aria-owned', function () {
4197
fixtureSetup(
4298
'<div id="target" aria-owns=" ">' +

0 commit comments

Comments
 (0)