Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/checks/aria/aria-allowed-attr-evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { isFocusable } from '../../commons/dom';
export default function ariaAllowedAttrEvaluate(node, options, virtualNode) {
const invalid = [];
const role = getRole(virtualNode);
let allowed = allowedAttr(role);
let allowed = allowedAttr(role, { vNode: virtualNode });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit reluctant about doing it this way for two reasons.

  1. I think this is mixing node constraints with role constraints. I don't think this function should give both the allowed attributes based on roles, and the allowed attrs based on node names. I think we should probably have a separate method for something like this.

  2. I think we should have role restrictions like this available on the standards object, so that it is configurable and findable, instead of having an inline exception for this case.

The way you've implemented this will ignore the role. I don't think that's the right thing to do here. A <br> with role=heading should allow all ARIA properties allowed on headings. Axe has a separate rule that reports using role=heading on br's as an issue. That way we keep the issues more clearly separated.

I appreciate this is going to make this work a little more complicated. @straker would you mind weighing in here too? I think it might be good to maybe propose a structure for the standards object before asking for more changes.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the feedback.

Regarding my original approach: the issue I was addressing was specifically about restricting ARIA attributes on certain node types (br / wbr), rather than on roles. Because of that, I initially thought it was necessary to pass the node information into allowedAttr in order to enforce node-level constraints.

About the standards object: when you mention moving these restrictions there, are you referring to lib/standards/html-elms.js? If so, would supporting this require changing the data structure in a way that affects other elements as well, or is it expected to be handled only for br / wbr?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree we could have the aria restrictions in the htmlElms standards, but that logic becomes a bit tricky in aria-allowed-attr. The way I see it working would be in the allowed-attr function if the element doesn't have a role, we check if the htmlElms standard has any restrictions on attrs and then remove them from the array before we return it. But I don't think that's the correct way to go as it makes the final failure message of not allowing it muddy since the element does not have a role (and in fact has nothing to do with the role).

We could probably move the logic to the aria-prohibited-attr check, similar to our aria-label and aria-labelledby checks on nodes with no role, but again that is a little difficult since we need to list all attributes as prohibited except aria-hidden. @dbjorge what do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the idea of listing all attributes as prohibited, I'd rather keep the standards object closer to the actual standard (the wording of the relevant table header in ARIA in HTML is "ARIA role, state and property allowances"). I agree that it'd be good to keep the failure message clear about whether the issue is coming from a role-based or element-based restriction. I think based on the rule naming and our existing rule docs, it'd make the most sense for this new type of violation to appear under aria-allowed-attr and not aria-prohibited-attr.

I'd lean towards something like:

  1. Update the html-elms shape to add a new property for allowedAriaAttrs - array(optional). If specified, restricts which ARIA attributes may be used with this element (in addition to any role-based restrictions).`
  2. Keep using the aria-allowed-attr rule, but split the aria-allowed-attr check into aria-allowed-attr-role and aria-allowed-attr-elm (both in the rule's all checks)
  3. The role one functions like the current check. The elm one checks if allowedAriaAttrs is set for the elm type, and if so, verifies against that list. It uses a different failure message that clarifies that the source of the failure is a restriction from ARIA in HTML's allowances for that element type.

We might consider whether we should deprecate noAriaAttrs in favor of allowedAriaAttrs: []. That makes conceptual sense to me, but would make this a much broader change; it doesn't look like we actually use noAriaAttrs anywhere in axe-core right now, so that would start enforcing restrictions on quite a few cases that we don't currently enforce.


// @deprecated: allowed attr options to pass more attrs.
// configure the standards spec instead
Expand Down
9 changes: 8 additions & 1 deletion lib/commons/aria/allowed-attr.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,16 @@ import getGlobalAriaAttrs from '../standards/get-global-aria-attrs';
* @memberof axe.commons.aria
* @instance
* @param {String} role The role to check
* @param {Object} options Optional configuration
* @param {VirtualNode} options.vNode Optional virtual node for element-level restrictions
* @return {Array}
*/
function allowedAttr(role) {
function allowedAttr(role, { vNode } = {}) {
// Check for br and wbr elements - only aria-hidden is allowed
if (vNode && ['br', 'wbr'].includes(vNode.props.nodeName)) {
return ['aria-hidden'];
}

const roleDef = standards.ariaRoles[role];
const attrs = [...getGlobalAriaAttrs()];

Expand Down
42 changes: 42 additions & 0 deletions test/checks/aria/aria-allowed-attr.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,4 +282,46 @@ describe('aria-allowed-attr', () => {
);
});
});

it('should pass for br with aria-hidden', () => {
const vNode = queryFixture('<br aria-hidden="true" id="target">');

assert.isTrue(
axe.testUtils
.getCheckEvaluate('aria-allowed-attr')
.call(checkContext, null, null, vNode)
);
});

it('should fail for br with global aria attribute', () => {
const vNode = queryFixture('<br aria-busy="true" id="target">');

assert.isFalse(
axe.testUtils
.getCheckEvaluate('aria-allowed-attr')
.call(checkContext, null, null, vNode)
);
assert.deepEqual(checkContext._data, ['aria-busy="true"']);
});

it('should pass for wbr with aria-hidden', () => {
const vNode = queryFixture('<wbr aria-hidden="false" id="target">');

assert.isTrue(
axe.testUtils
.getCheckEvaluate('aria-allowed-attr')
.call(checkContext, null, null, vNode)
);
});

it('should fail for wbr with global aria attribute', () => {
const vNode = queryFixture('<wbr aria-busy="true" id="target">');

assert.isFalse(
axe.testUtils
.getCheckEvaluate('aria-allowed-attr')
.call(checkContext, null, null, vNode)
);
assert.deepEqual(checkContext._data, ['aria-busy="true"']);
});
});
16 changes: 16 additions & 0 deletions test/commons/aria/allowed-attr.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,20 @@ describe('aria.allowedAttr', function () {
it('should return an array with globally allowed attributes', function () {
assert.deepEqual(axe.commons.aria.allowedAttr('cats'), globalAttrs);
});

it('should return only aria-hidden for br element', function () {
const queryFixture = axe.testUtils.queryFixture;
const vNode = queryFixture('<br id="target">');
assert.deepEqual(axe.commons.aria.allowedAttr(null, { vNode }), [
'aria-hidden'
]);
});

it('should return only aria-hidden for wbr element', function () {
const queryFixture = axe.testUtils.queryFixture;
const vNode = queryFixture('<wbr id="target">');
assert.deepEqual(axe.commons.aria.allowedAttr(null, { vNode }), [
'aria-hidden'
]);
});
});
3 changes: 3 additions & 0 deletions test/integration/rules/aria-allowed-attr/failures.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@
<div id="fail6" role="combobox" aria-multiline="false"></div>
<div id="fail7" role="combobox" aria-multiline="true" contenteditable></div>
<div id="fail8" role="combobox" aria-multiline="true"></div>

<br aria-busy="true" id="fail9" />
<wbr aria-busy="true" id="fail10" />
4 changes: 3 additions & 1 deletion test/integration/rules/aria-allowed-attr/failures.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
["#fail5"],
["#fail6"],
["#fail7"],
["#fail8"]
["#fail8"],
["#fail9"],
["#fail10"]
]
}
4 changes: 4 additions & 0 deletions test/integration/rules/aria-allowed-attr/passes.html
Original file line number Diff line number Diff line change
Expand Up @@ -2173,3 +2173,7 @@
<!-- Ignored ARIA attributes -->
<button id="pass101" aria-required="false"></button>
<div id="pass102" role="combobox" aria-multiline="false" contenteditable></div>

<!-- br and wbr allowed attrs -->
<br aria-hidden="true" id="pass103" />
<wbr aria-hidden="true" id="pass104" />
4 changes: 3 additions & 1 deletion test/integration/rules/aria-allowed-attr/passes.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@
["#pass99"],
["#pass100"],
["#pass101"],
["#pass102"]
["#pass102"],
["#pass103"],
["#pass104"]
]
}