diff --git a/README.md b/README.md
index ea47f07..fadb0d2 100644
--- a/README.md
+++ b/README.md
@@ -34,5 +34,32 @@ yet, alias the module to nothing with webpack in production.
If you want it to throw errors instead of just warnings:
```
-a11y(React, {throw: true});
+a11y(React, { throw: true });
+```
+
+You can filter failures by passing a function to the `filterFn` option. The
+filter function will receive three arguments: the name of the Component
+instance or ReactElement, the id of the element, and the failure message.
+Note: If a ReactElement, the name will be the node type followed by the id
+(e.g. div#foo).
+
+```
+var commentListFailures = (name, id, msg) => {
+ return name === "CommentList";
+};
+
+a11y(React, { filterFn: commentListFailures });
+```
+
+If you want to log DOM element references for easy lookups in the DOM inspector,
+use the `includeSrcNode` option.
+
+```
+a11y(React, { throw: true, includeSrcNode: true });
+```
+
+All failures are also accessible via the `getFailures()` method.
+
+```
+a11y.getFailures();
```
diff --git a/lib/__tests__/index-test.js b/lib/__tests__/index-test.js
index 49880f3..0d82aeb 100644
--- a/lib/__tests__/index-test.js
+++ b/lib/__tests__/index-test.js
@@ -1,6 +1,6 @@
var React = require('react');
var assert = require('assert');
-require('../index')(React);
+var a11y = require('../index');
var assertions = require('../assertions');
var k = () => {};
@@ -25,6 +25,15 @@ var doNotExpectWarning = (notExpected, fn) => {
};
describe('props', () => {
+ var createElement = React.createElement;
+
+ before(() => {
+ a11y(React);
+ });
+
+ after(() => {
+ React.createElement = createElement;
+ });
describe('onClick', () => {
@@ -162,6 +171,16 @@ describe('props', () => {
});
describe('tags', () => {
+ var createElement = React.createElement;
+
+ before(() => {
+ a11y(React);
+ });
+
+ after(() => {
+ React.createElement = createElement;
+ });
+
describe('img', () => {
it('requires alt attributes', () => {
expectWarning(assertions.tags.img.MISSING_ALT.msg, () => {
@@ -200,3 +219,62 @@ describe('tags', () => {
});
});
});
+
+describe('filterFn', () => {
+ var createElement = React.createElement;
+
+ before(() => {
+ var barOnly = (name, id, msg) => {
+ return id === "bar";
+ };
+
+ a11y(React, { filterFn: barOnly });
+ });
+
+ after(() => {
+ React.createElement = createElement;
+ });
+
+ describe('when the source element has been filtered out', () => {
+ it('does not warn', () => {
+ doNotExpectWarning(assertions.tags.img.MISSING_ALT.msg, () => {
+ ;
+ });
+ });
+ });
+
+ describe('when there are filtered results', () => {
+ it('warns', () => {
+ expectWarning(assertions.tags.img.MISSING_ALT.msg, () => {
+
+
+
+
;
+ });
+ });
+ });
+});
+
+describe('getFailures()', () => {
+ var createElement = React.createElement;
+
+ before(() => {
+ a11y(React);
+ });
+
+ after(() => {
+ React.createElement = createElement;
+ });
+
+ describe('when there are failures', () => {
+ it('returns the failures', () => {
+
+
+
+
;
+
+ assert(a11y.getFailures().length == 2);
+ });
+ });
+
+});
\ No newline at end of file
diff --git a/lib/index.js b/lib/index.js
index 37b0898..ea69f3a 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -20,36 +20,140 @@ var assertAccessibility = (tagName, props, children) => {
return failures;
};
-var error = (id, msg) => {
- throw new Error('#' + id + ": " + msg);
+var filterFailures = (failureInfo, options) => {
+ var failures = failureInfo.failures;
+ var filterFn = options.filterFn &&
+ options.filterFn.bind(undefined, failureInfo.name, failureInfo.id);
+
+ if (filterFn) {
+ failures = failures.filter(filterFn);
+ }
+
+ return failures;
};
-var warn = (id, msg) => {
- console.warn('#' + id, msg);
+var throwError = (failureInfo, options) => {
+ var failures = filterFailures(failureInfo, options);
+ var msg = failures.pop();
+ var error = [failureInfo.name, msg];
+
+ if (options.includeSrcNode) {
+ error.push(failureInfo.id);
+ }
+
+ throw new Error(error.join(' '));
+};
+
+var after = (host, name, cb) => {
+ var originalFn = host[name];
+
+ if (originalFn) {
+ host[name] = () => {
+ originalFn.call(host);
+ cb.call(host);
+ };
+ } else {
+ host[name] = cb;
+ }
+};
+
+var logAfterRender = (component, log) => {
+ after(component, 'componentDidMount', log);
+ after(component, 'componentDidUpdate', log);
+};
+
+var logWarning = (component, failureInfo, options) => {
+ var includeSrcNode = options.includeSrcNode;
+
+ var warn = () => {
+ var failures = filterFailures(failureInfo, options);
+
+ failures.forEach((failure) => {
+ var msg = failure;
+ var warning = [failureInfo.name, msg];
+
+ if (includeSrcNode) {
+ warning.push(document.getElementById(failureInfo.id));
+ }
+
+ console.warn.apply(console, warning);
+ });
+
+ totalFailures.push(failureInfo);
+ };
+
+ if (component && includeSrcNode) {
+ // Cannot log a node reference until the component is in the DOM,
+ // so defer the document.getElementById call until componentDidMount
+ // or componentDidUpdate.
+ logAfterRender(component._instance, warn);
+ } else {
+ warn();
+ }
};
var nextId = 0;
-module.exports = (React, options) => {
+var totalFailures;
+
+var reactA11y = (React, options) => {
if (!React && !React.createElement) {
throw new Error('Missing parameter: React');
}
assertions.setReact(React);
+ totalFailures = [];
var _createElement = React.createElement;
- var log = options && options.throw ? error : warn;
- React.createElement = function (type, _props, ...children) {
+ var includeSrcNode = options && !!options.includeSrcNode;
+
+ React.createElement = (type, _props, ...children) => {
var props = _props || {};
+ var reactEl;
+
if (typeof type === 'string') {
- var failures = assertAccessibility(type, props, children);
+ let failures = assertAccessibility(type, props, children);
if (failures.length) {
// Generate an id if one doesn't exist
props.id = (props.id || 'a11y-' + nextId++);
+ reactEl = _createElement.apply(this, [type, props].concat(children));
+
+ let reactComponent = reactEl._owner;
- for (var i = 0; i < failures.length; i++)
- log(props.id, failures[i]);
+ // If a Component instance, use the component's name,
+ // if a ReactElement instance, use the node DOM + id (e.g. div#foo)
+ let name = reactComponent && reactComponent.getName() ||
+ reactEl.type + '#' + props.id;
+
+ let failureInfo = {
+ 'name': name ,
+ 'id': props.id,
+ 'failures': failures
+ };
+
+ let notifyOpts = {
+ 'includeSrcNode': includeSrcNode,
+ 'filterFn': options && options.filterFn
+ };
+
+ if (options && options.throw) {
+ throwError(failureInfo, notifyOpts);
+ } else {
+ logWarning(reactComponent, failureInfo, notifyOpts);
+ }
+
+ } else {
+ reactEl = _createElement.apply(this, [type, props].concat(children));
}
+ } else {
+ reactEl = _createElement.apply(this, [type, props].concat(children));
}
- // make sure props with the id is passed down, even if no props were passed in.
- return _createElement.apply(this, [type, props].concat(children));
+
+ return reactEl;
};
+
+ reactA11y.getFailures = () => {
+ return totalFailures;
+ };
+
};
+
+module.exports = reactA11y;