diff --git a/lib/__tests__/index-test.js b/lib/__tests__/index-test.js
index ad760a6..3b62e4f 100644
--- a/lib/__tests__/index-test.js
+++ b/lib/__tests__/index-test.js
@@ -61,6 +61,12 @@ describe('props', () => {
;
});
});
+
+ it('does not warn with no role and `aria-hidden="true"`', () => {
+ doNotExpectWarning(assertions.props.onClick.NO_ROLE.msg, () => {
+ ;
+ });
+ });
});
describe('tabIndex', () => {
@@ -98,6 +104,48 @@ describe('props', () => {
});
});
});
+
+ describe('aria-hidden', () => {
+ describe('when set to `true`', () => {
+ it('warns when applied to an interactive element without `tabIndex="-1"`', () => {
+ expectWarning(assertions.props['aria-hidden'].TABINDEX_REQUIRED_WHEN_ARIA_HIDDEN.msg, () => {
+ ;
+ });
+ });
+
+ it('warns when applied to an interactive element with `tabIndex="0"`', () => {
+ expectWarning(assertions.props['aria-hidden'].TABINDEX_REQUIRED_WHEN_ARIA_HIDDEN.msg, () => {
+ ;
+ });
+ });
+
+ it('does not warn when applied to a placeholder link', () => {
+ expectWarning(assertions.props['aria-hidden'].TABINDEX_REQUIRED_WHEN_ARIA_HIDDEN.msg, () => {
+ ;
+ });
+ });
+
+ it('does not warn when applied to an interactive element with `tabIndex="-1"`', () => {
+ doNotExpectWarning(assertions.props['aria-hidden'].TABINDEX_REQUIRED_WHEN_ARIA_HIDDEN.msg, () => {
+ ;
+ });
+ });
+
+ it('does not warn when applied to a non-interactive element', () => {
+ doNotExpectWarning(assertions.props['aria-hidden'].TABINDEX_REQUIRED_WHEN_ARIA_HIDDEN.msg, () => {
+ ;
+ });
+ });
+ });
+
+ describe('when set to `false`', () =>{
+ it('does not warn when applied to an interactive element with `tabIndex="-1"`', () => {
+ doNotExpectWarning(assertions.props['aria-hidden'].TABINDEX_REQUIRED_WHEN_ARIA_HIDDEN.msg, () => {
+ ;
+ });
+ });
+ });
+ });
});
describe('tags', () => {
@@ -132,6 +180,22 @@ describe('tags', () => {
});
describe('a', () => {
+ describe('placeholder links without href', () => {
+ it('does not warn', () => {
+ doNotExpectWarning(assertions.tags.a.HASH_HREF_NEEDS_BUTTON.msg, () => {
+ ;
+ });
+ });
+ });
+
+ describe('placeholder links without tabindex', () => {
+ it('does not warn', () => {
+ doNotExpectWarning(assertions.tags.a.TABINDEX_NEEDS_BUTTON.msg, () => {
+ ;
+ });
+ });
+ });
+
describe('with [href="#"]', () => {
it('warns', () => {
expectWarning(assertions.tags.a.HASH_HREF_NEEDS_BUTTON.msg, () => {
@@ -194,9 +258,27 @@ describe('labels', () => {
});
});
- it('does not warn when the ARIA role is presentation', () => {
+ it('does not warn when `role="presentation"`', () => {
doNotExpectWarning(assertions.render.NO_LABEL.msg, () => {
- ;
+
;
+ });
+ });
+
+ it('does not warn when `role="none"`', () => {
+ doNotExpectWarning(assertions.render.NO_LABEL.msg, () => {
+
;
+ });
+ });
+
+ it('does not warn when `aria-hidden="true"`', () => {
+ doNotExpectWarning(assertions.render.NO_LABEL.msg, () => {
+ ;
+ });
+ });
+
+ it('warns when `aria-hidden="false"`', () => {
+ expectWarning(assertions.render.NO_LABEL.msg, () => {
+ ;
});
});
diff --git a/lib/assertions.js b/lib/assertions.js
index a6534a5..aa76ef4 100644
--- a/lib/assertions.js
+++ b/lib/assertions.js
@@ -21,6 +21,12 @@ var INTERACTIVE = {
}
};
+const presentationRoles = new Set(['presentation', 'none']);
+
+var isHiddenFromAT = (props) => {
+ return props['aria-hidden'] == 'true';
+};
+
var hasAlt = (props) => {
return typeof props.alt === 'string';
};
@@ -105,11 +111,21 @@ var hasChildTextNode = (props, children, failureCB) => {
return hasText;
};
+exports.mobileExclusions = [
+ 'NO_TABINDEX',
+ 'BUTTON_ROLE_SPACE',
+ 'BUTTON_ROLE_ENTER',
+ 'TABINDEX_REQUIRED_WHEN_ARIA_HIDDEN'
+];
+
exports.props = {
onClick: {
NO_ROLE: {
msg: 'You have a click handler on a non-interactive element but no `role` DOM property. It will be unclear what this element is supposed to do to a screen-reader user. http://www.w3.org/TR/wai-aria/roles#role_definitions',
test (tagName, props, children) {
+ if (isHiddenFromAT(props))
+ return true;
+
return !(!isInteractive(tagName, props) && !props.role);
}
},
@@ -117,6 +133,9 @@ exports.props = {
NO_TABINDEX: {
msg: 'You have a click handler on a non-interactive element but no `tabIndex` DOM property. The element will not be navigable or interactive by keyboard users. http://www.w3.org/TR/wai-aria-practices/#focus_tabindex',
test (tagName, props, children) {
+ if (isHiddenFromAT(props))
+ return true;
+
return !(
!isInteractive(tagName, props) &&
props.tabIndex == null // tabIndex={0} is valid
@@ -127,6 +146,9 @@ exports.props = {
BUTTON_ROLE_SPACE: {
msg: 'You have `role="button"` but did not define an `onKeyDown` handler. Add it, and have the "Space" key do the same thing as an `onClick` handler.',
test (tagName, props, children) {
+ if (isHiddenFromAT(props))
+ return true;
+
return !(props.role === 'button' && !props.onKeyDown);
}
},
@@ -134,10 +156,25 @@ exports.props = {
BUTTON_ROLE_ENTER: {
msg: 'You have `role="button"` but did not define an `onKeyDown` handler. Add it, and have the "Enter" key do the same thing as an `onClick` handler.',
test (tagName, props, children) {
+ if (isHiddenFromAT(props))
+ return true;
+
return !(props.role === 'button' && !props.onKeyDown);
}
}
+ },
+ 'aria-hidden': {
+ 'TABINDEX_REQUIRED_WHEN_ARIA_HIDDEN': {
+ msg: 'You have `aria-hidden="true"` applied to an interactive element but have not removed it from the tab flow. This could result in a hidden tab stop for users of screen readers.',
+ test (tagName, props, children) {
+ return !(
+ (isInteractive(tagName, props) || (tagName == 'a' && !props.href)) &&
+ props['aria-hidden'] == 'true' &&
+ props.tabIndex != '-1'
+ );
+ }
+ }
}
};
@@ -146,13 +183,19 @@ exports.tags = {
HASH_HREF_NEEDS_BUTTON: {
msg: 'You have an anchor with `href="#"` and no `role` DOM property. Add `role="button"` or better yet, use a ``.',
test (tagName, props, children) {
+ if (isHiddenFromAT(props))
+ return true;
+
return !(!props.role && props.href === '#');
}
},
TABINDEX_NEEDS_BUTTON: {
msg: 'You have an anchor with a tabIndex, no `href` and no `role` DOM property. Add `role="button"` or better yet, use a ``.',
test (tagName, props, children) {
- return !(!props.role && props.tabIndex !== null && !props.href);
+ if (isHiddenFromAT(props))
+ return true;
+
+ return !(!props.role && props.tabIndex != null && !props.href);
}
}
},
@@ -161,6 +204,9 @@ exports.tags = {
MISSING_ALT: {
msg: 'You forgot an `alt` DOM property on an image. Screen-reader users will not know what it is.',
test (tagName, props, children) {
+ if (isHiddenFromAT(props))
+ return true;
+
return hasAlt(props);
}
},
@@ -169,6 +215,9 @@ exports.tags = {
// TODO: have some way to set localization strings to match against
msg: 'Screen-readers already announce `img` tags as an image, you don\'t need to use the word "image" in the description',
test (tagName, props, children) {
+ if (isHiddenFromAT(props))
+ return true;
+
return !(hasAlt(props) && props.alt.match('image'));
}
}
@@ -179,22 +228,17 @@ exports.render = {
NO_LABEL: {
msg: 'You have an unlabled element or control. Add `aria-label` or `aria-labelled-by` attribute, or put some text in the element.',
test (tagName, props, children, failureCB) {
- var labelRequired = (
- isInteractive(tagName, props) ||
- props.role && props.role != 'presentation'
- );
+ if (isHiddenFromAT(props) || presentationRoles.has(props.role))
+ return;
- if (!labelRequired)
+ if (!(isInteractive(tagName, props) || props.role))
return;
var failed = !(
- labelRequired &&
- (
- props['aria-label'] ||
- props['aria-labelled-by'] ||
- (tagName === 'img' && props.alt) ||
- hasChildTextNode(props, children, failureCB)
- )
+ props['aria-label'] ||
+ props['aria-labelled-by'] ||
+ (tagName === 'img' && props.alt) ||
+ hasChildTextNode(props, children, failureCB)
);
if (failed)
diff --git a/lib/index.js b/lib/index.js
index 95539d7..015ca93 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -1,17 +1,11 @@
var assertions = require('./assertions');
var after = require('./after');
-const mobileExclusions = [
- 'NO_TABINDEX',
- 'BUTTON_ROLE_SPACE',
- 'BUTTON_ROLE_ENTER'
-];
-
var shouldRunTest = (testName, options) => {
var exclude = options.exclude || [];
if (options.device == 'mobile') {
- exclude = new Set(exclude.concat(mobileExclusions));
+ exclude = new Set(exclude.concat(assertions.mobileExclusions));
exclude = [...exclude];
}