diff --git a/lib/checks/aria/aria-allowed-attr-evaluate.js b/lib/checks/aria/aria-allowed-attr-evaluate.js index ab1002eb37..59d09e7e6e 100644 --- a/lib/checks/aria/aria-allowed-attr-evaluate.js +++ b/lib/checks/aria/aria-allowed-attr-evaluate.js @@ -42,7 +42,7 @@ export default function ariaAllowedAttrEvaluate(node, options, virtualNode) { if ( validateAttr(attrName) && !allowed.includes(attrName) && - !ignoredAttrs(attrName, virtualNode.attr(attrName)) + !ignoredAttrs(attrName, virtualNode.attr(attrName), virtualNode) ) { invalid.push(attrName); } @@ -62,8 +62,22 @@ export default function ariaAllowedAttrEvaluate(node, options, virtualNode) { return false; } -function ignoredAttrs(attrName, attrValue) { +function ignoredAttrs(attrName, attrValue, vNode) { // allow aria-required=false as screen readers consistently ignore it // @see https://github.com/dequelabs/axe-core/issues/3756 - return attrName === 'aria-required' && attrValue === 'false'; + if (attrName === 'aria-required' && attrValue === 'false') { + return true; + } + + // allow aria-multiline=false when contenteditable is set + // @see https://github.com/dequelabs/axe-core/issues/4463 + if ( + attrName === 'aria-multiline' && + attrValue === 'false' && + vNode.hasAttr('contenteditable') + ) { + return true; + } + + return false; } diff --git a/test/checks/aria/aria-allowed-attr.js b/test/checks/aria/aria-allowed-attr.js index 578dd2771b..b25b9df4a1 100644 --- a/test/checks/aria/aria-allowed-attr.js +++ b/test/checks/aria/aria-allowed-attr.js @@ -124,6 +124,56 @@ describe('aria-allowed-attr', () => { assert.deepEqual(checkContext._data, ['aria-required="true"']); }); + it('should not report on aria-multiline=false with contenteditable', () => { + const vNode = queryFixture( + '
' + ); + + assert.isTrue( + axe.testUtils + .getCheckEvaluate('aria-allowed-attr') + .call(checkContext, null, null, vNode) + ); + assert.isNull(checkContext._data); + }); + + it('should return false for unallowed aria-multiline=true and contenteditable', () => { + const vNode = queryFixture( + '' + ); + + assert.isFalse( + axe.testUtils + .getCheckEvaluate('aria-allowed-attr') + .call(checkContext, null, null, vNode) + ); + assert.deepEqual(checkContext._data, ['aria-multiline="true"']); + }); + + it('should return false for unallowed aria-multiline=false', () => { + const vNode = queryFixture( + '' + ); + + assert.isFalse( + axe.testUtils + .getCheckEvaluate('aria-allowed-attr') + .call(checkContext, null, null, vNode) + ); + assert.deepEqual(checkContext._data, ['aria-multiline="false"']); + }); + + it('should return false for unallowed aria-multiline=true', () => { + const vNode = queryFixture(''); + + assert.isFalse( + axe.testUtils + .getCheckEvaluate('aria-allowed-attr') + .call(checkContext, null, null, vNode) + ); + assert.deepEqual(checkContext._data, ['aria-multiline="true"']); + }); + it('should return undefined for custom element that has no role and is not focusable', () => { const vNode = queryFixture( '