diff --git a/lib/checks/keyboard/focusable-element.js b/lib/checks/keyboard/focusable-element.js index eacb898168..dc5d750697 100644 --- a/lib/checks/keyboard/focusable-element.js +++ b/lib/checks/keyboard/focusable-element.js @@ -4,9 +4,35 @@ * - if element is focusable * - if element is in focus order via `tabindex` */ -const isFocusable = virtualNode.isFocusable; +if (virtualNode.hasAttr('contenteditable') && isContenteditable(virtualNode)) { + return true; +} -let tabIndex = parseInt(virtualNode.actualNode.getAttribute('tabindex'), 10); +const isFocusable = virtualNode.isFocusable; +let tabIndex = parseInt(virtualNode.attr('tabindex'), 10); tabIndex = !isNaN(tabIndex) ? tabIndex : null; return tabIndex ? isFocusable && tabIndex >= 0 : isFocusable; + +// contenteditable is focusable when it is an empty string (whitespace +// is not considered empty) or "true". if the value is "false" +// you can't edit it, but if it's anything else it inherits the value +// from the first valid ancestor +// @see https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/contenteditable +function isContenteditable(vNode) { + const contenteditable = vNode.attr('contenteditable'); + if (contenteditable === 'true' || contenteditable === '') { + return true; + } + + if (contenteditable === 'false') { + return false; + } + + const ancestor = axe.utils.closest(virtualNode.parent, '[contenteditable]'); + if (!ancestor) { + return false; + } + + return isContenteditable(ancestor); +} diff --git a/test/checks/keyboard/focusable-element.js b/test/checks/keyboard/focusable-element.js index 179936c6dc..a989f2e8b7 100644 --- a/test/checks/keyboard/focusable-element.js +++ b/test/checks/keyboard/focusable-element.js @@ -43,4 +43,52 @@ describe('focusable-element tests', function() { var actual = check.evaluate.apply(checkContext, params); assert.isTrue(actual); }); + + it('returns true when element made focusable by contenteditable', function() { + var params = checkSetup( + '
I hold some text
' + ); + var actual = check.evaluate.apply(checkContext, params); + assert.isTrue(actual); + }); + + it('returns true when element made focusable by contenteditable="true"', function() { + var params = checkSetup( + 'I hold some text
' + ); + var actual = check.evaluate.apply(checkContext, params); + assert.isTrue(actual); + }); + + it('returns false when element made focusable by contenteditable="false"', function() { + var params = checkSetup( + 'I hold some text
' + ); + var actual = check.evaluate.apply(checkContext, params); + assert.isFalse(actual); + }); + + it('returns true when element made focusable by contenteditable="invalid" and parent is contenteditable', function() { + var params = checkSetup( + 'I hold some text
I hold some text
I hold some text
Content
+Content
+Content
+