Skip to content

Commit 1012dfe

Browse files
authored
fix(scrollable-region-focusable): pass for elements with contenteditable (#2133)
* fix(scrollable-region-focusable): pass for elements with contenteditable * change order * full test cases * fix ie11
1 parent 493dd22 commit 1012dfe

4 files changed

Lines changed: 111 additions & 4 deletions

File tree

lib/checks/keyboard/focusable-element.js

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,35 @@
44
* - if element is focusable
55
* - if element is in focus order via `tabindex`
66
*/
7-
const isFocusable = virtualNode.isFocusable;
7+
if (virtualNode.hasAttr('contenteditable') && isContenteditable(virtualNode)) {
8+
return true;
9+
}
810

9-
let tabIndex = parseInt(virtualNode.actualNode.getAttribute('tabindex'), 10);
11+
const isFocusable = virtualNode.isFocusable;
12+
let tabIndex = parseInt(virtualNode.attr('tabindex'), 10);
1013
tabIndex = !isNaN(tabIndex) ? tabIndex : null;
1114

1215
return tabIndex ? isFocusable && tabIndex >= 0 : isFocusable;
16+
17+
// contenteditable is focusable when it is an empty string (whitespace
18+
// is not considered empty) or "true". if the value is "false"
19+
// you can't edit it, but if it's anything else it inherits the value
20+
// from the first valid ancestor
21+
// @see https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/contenteditable
22+
function isContenteditable(vNode) {
23+
const contenteditable = vNode.attr('contenteditable');
24+
if (contenteditable === 'true' || contenteditable === '') {
25+
return true;
26+
}
27+
28+
if (contenteditable === 'false') {
29+
return false;
30+
}
31+
32+
const ancestor = axe.utils.closest(virtualNode.parent, '[contenteditable]');
33+
if (!ancestor) {
34+
return false;
35+
}
36+
37+
return isContenteditable(ancestor);
38+
}

test/checks/keyboard/focusable-element.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,52 @@ describe('focusable-element tests', function() {
4343
var actual = check.evaluate.apply(checkContext, params);
4444
assert.isTrue(actual);
4545
});
46+
47+
it('returns true when element made focusable by contenteditable', function() {
48+
var params = checkSetup(
49+
'<p id="target" contenteditable>I hold some text </p>'
50+
);
51+
var actual = check.evaluate.apply(checkContext, params);
52+
assert.isTrue(actual);
53+
});
54+
55+
it('returns true when element made focusable by contenteditable="true"', function() {
56+
var params = checkSetup(
57+
'<p id="target" contenteditable="true">I hold some text </p>'
58+
);
59+
var actual = check.evaluate.apply(checkContext, params);
60+
assert.isTrue(actual);
61+
});
62+
63+
it('returns false when element made focusable by contenteditable="false"', function() {
64+
var params = checkSetup(
65+
'<p id="target" contenteditable="false">I hold some text </p>'
66+
);
67+
var actual = check.evaluate.apply(checkContext, params);
68+
assert.isFalse(actual);
69+
});
70+
71+
it('returns true when element made focusable by contenteditable="invalid" and parent is contenteditable', function() {
72+
var params = checkSetup(
73+
'<div contenteditable><p id="target" contenteditable="invalid">I hold some text </p></div>'
74+
);
75+
var actual = check.evaluate.apply(checkContext, params);
76+
assert.isTrue(actual);
77+
});
78+
79+
it('returns false when element made focusable by contenteditable="invalid" and parent is not contenteditable', function() {
80+
var params = checkSetup(
81+
'<div><p id="target" contenteditable="invalid">I hold some text </p></div>'
82+
);
83+
var actual = check.evaluate.apply(checkContext, params);
84+
assert.isFalse(actual);
85+
});
86+
87+
it('returns false when element made focusable by contenteditable="invalid" and parent is contenteditable="false"', function() {
88+
var params = checkSetup(
89+
'<div contenteditable="false"><p id="target" contenteditable="invalid">I hold some text </p></div>'
90+
);
91+
var actual = check.evaluate.apply(checkContext, params);
92+
assert.isFalse(actual);
93+
});
4694
});

test/integration/rules/scrollable-region-focusable/scrollable-region-focusable.html

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,26 @@
1616
<p style="height: 200px;" tabindex="0"></p>
1717
</div>
1818

19+
<div id="pass4" style="height: 200px; overflow-y: auto" contenteditable="true">
20+
<div style="height: 2000px">
21+
<p>Content</p>
22+
</div>
23+
</div>
24+
25+
<div id="pass5" style="height: 200px; overflow-y: auto" contenteditable="true">
26+
<div style="height: 2000px">
27+
<div
28+
id="pass6"
29+
style="height: 200px; overflow-y: auto"
30+
contenteditable="invalid"
31+
>
32+
<div style="height: 2000px">
33+
<p>Content</p>
34+
</div>
35+
</div>
36+
</div>
37+
</div>
38+
1939
<!-- fail -->
2040
<div id="fail1" style="height: 5px; overflow: auto;">
2141
<input type="text" tabindex="-1" />
@@ -27,6 +47,12 @@
2747
<textarea tabindex="-1"></textarea>
2848
</div>
2949

50+
<div id="fail3" style="height: 200px; overflow-y: auto" contenteditable="false">
51+
<div style="height: 2000px">
52+
<p>Content</p>
53+
</div>
54+
</div>
55+
3056
<!-- inapplicable -->
3157
<section id="inapplicable1">This element is not scrollable</section>
3258

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
{
22
"description": "scrollable-region-focusable tests",
33
"rule": "scrollable-region-focusable",
4-
"violations": [["#fail1"], ["#fail2"]],
5-
"passes": [["#pass1"], ["#pass2"], ["#pass3"]]
4+
"violations": [["#fail1"], ["#fail2"], ["#fail3"]],
5+
"passes": [
6+
["#pass1"],
7+
["#pass2"],
8+
["#pass3"],
9+
["#pass4"],
10+
["#pass5"],
11+
["#pass6"]
12+
]
613
}

0 commit comments

Comments
 (0)