2017-06-22 16:36:39 +03:00
|
|
|
(function(root, factory) {
|
|
|
|
if (typeof define === 'function' && define.amd) {
|
2017-06-27 17:39:23 +03:00
|
|
|
define([], factory)
|
2017-06-22 16:36:39 +03:00
|
|
|
} else {
|
2017-06-27 17:39:23 +03:00
|
|
|
root.scanForProblems = factory()
|
2017-06-22 16:36:39 +03:00
|
|
|
}
|
|
|
|
}(this, function() {
|
2017-06-27 16:15:30 +03:00
|
|
|
function scanForProblems(context, logError) {
|
|
|
|
|
|
|
|
var imgElements = context.querySelectorAll('img')
|
2017-08-25 10:18:06 +03:00
|
|
|
for (var i = 0; i < imgElements.length; i++) {
|
2017-06-27 16:15:30 +03:00
|
|
|
var img = imgElements[i]
|
2017-06-22 16:36:39 +03:00
|
|
|
if (!img.hasAttribute('alt')) {
|
|
|
|
logError(new ImageWithoutAltAttributeError(img))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-27 16:15:30 +03:00
|
|
|
var aElements = context.querySelectorAll('a')
|
2017-08-25 10:18:06 +03:00
|
|
|
for (var i = 0; i < aElements.length; i++) {
|
2017-06-27 16:15:30 +03:00
|
|
|
var a = aElements[i]
|
2017-06-22 16:36:39 +03:00
|
|
|
if (a.hasAttribute('name') || elementIsHidden(a)) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if (a.getAttribute('href') == null && a.getAttribute('role') !== 'button') {
|
|
|
|
logError(new LinkWithoutLabelOrRoleError(a))
|
|
|
|
} else if (!accessibleText(a)) {
|
|
|
|
logError(new ElementWithoutLabelError(a))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-27 16:15:30 +03:00
|
|
|
var buttonElements = context.querySelectorAll('button')
|
2017-08-25 10:18:06 +03:00
|
|
|
for (var i = 0; i < buttonElements.length; i++) {
|
2017-06-27 16:15:30 +03:00
|
|
|
var button = buttonElements[i]
|
2017-06-22 16:36:39 +03:00
|
|
|
if (!elementIsHidden(button) && !accessibleText(button)) {
|
|
|
|
logError(new ButtonWithoutLabelError(button))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-27 16:15:30 +03:00
|
|
|
var labelElements = context.querySelectorAll('label')
|
2017-08-25 10:18:06 +03:00
|
|
|
for (var i = 0; i < labelElements.length; i++) {
|
2017-06-27 16:15:30 +03:00
|
|
|
var label = labelElements[i]
|
2017-06-22 16:36:39 +03:00
|
|
|
// In case label.control isn't supported by browser, find the control manually (IE)
|
2017-06-27 16:15:30 +03:00
|
|
|
var control = label.control || document.getElementById(label.getAttribute('for')) || label.querySelector('input')
|
2017-06-22 16:36:39 +03:00
|
|
|
|
|
|
|
if (!control) {
|
|
|
|
logError(new LabelMissingControl(label), false)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-27 16:15:30 +03:00
|
|
|
var inputElements = context.querySelectorAll('input[type=text], textarea')
|
2017-08-25 10:18:06 +03:00
|
|
|
for (var i = 0; i < inputElements.length; i++) {
|
2017-06-27 16:15:30 +03:00
|
|
|
var input = inputElements[i]
|
2017-06-22 16:36:39 +03:00
|
|
|
if (input.labels && !input.labels.length && !elementIsHidden(input) && !input.hasAttribute('aria-label')) {
|
|
|
|
logError(new ElementWithoutLabelError(input))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-27 17:13:22 +03:00
|
|
|
for (var selector in SelectorARIAPairs) {
|
2017-06-27 16:15:30 +03:00
|
|
|
var ARIAAttrsRequired = SelectorARIAPairs[selector]
|
|
|
|
var targetElements = context.querySelectorAll(selector)
|
|
|
|
|
2017-06-27 16:59:20 +03:00
|
|
|
for (var j = 0; j < targetElements.length; j++) {
|
2017-06-27 16:15:30 +03:00
|
|
|
var target = targetElements[j]
|
|
|
|
var missingAttrs = []
|
|
|
|
|
2017-06-27 16:59:20 +03:00
|
|
|
for (var k = 0; k < ARIAAttrsRequired.length; k++) {
|
2017-06-27 16:15:30 +03:00
|
|
|
var attr = ARIAAttrsRequired[k]
|
2017-06-22 16:36:39 +03:00
|
|
|
if (!target.hasAttribute(attr)) missingAttrs.push(attr)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (missingAttrs.length > 0) {
|
|
|
|
logError(new ARIAAttributeMissingError(target, missingAttrs.join(', ')))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-26 17:58:43 +03:00
|
|
|
function errorSubclass(fn) {
|
|
|
|
fn.prototype = new Error()
|
2017-08-25 10:18:06 +03:00
|
|
|
fn.prototype.constructor = fn
|
2017-06-26 17:58:43 +03:00
|
|
|
}
|
2017-06-22 16:36:39 +03:00
|
|
|
|
2017-06-26 17:58:43 +03:00
|
|
|
function ImageWithoutAltAttributeError(element) {
|
|
|
|
this.name = 'ImageWithoutAltAttributeError'
|
|
|
|
this.stack = new Error().stack
|
|
|
|
this.element = element
|
2017-07-31 17:25:41 +03:00
|
|
|
this.message = 'Missing alt attribute on ' + inspect(element)
|
2017-06-26 17:58:43 +03:00
|
|
|
}
|
|
|
|
errorSubclass(ImageWithoutAltAttributeError)
|
|
|
|
|
|
|
|
function ElementWithoutLabelError(element) {
|
|
|
|
this.name = 'ElementWithoutLabelError'
|
|
|
|
this.stack = new Error().stack
|
|
|
|
this.element = element
|
2017-07-31 17:25:41 +03:00
|
|
|
this.message = 'Missing text, title, or aria-label attribute on ' + inspect(element)
|
2017-06-26 17:58:43 +03:00
|
|
|
}
|
|
|
|
errorSubclass(ElementWithoutLabelError)
|
|
|
|
|
|
|
|
function LinkWithoutLabelOrRoleError(element) {
|
|
|
|
this.name = 'LinkWithoutLabelOrRoleError'
|
|
|
|
this.stack = new Error().stack
|
|
|
|
this.element = element
|
2017-07-31 17:25:41 +03:00
|
|
|
this.message = 'Missing href or role=button on ' + inspect(element)
|
2017-06-26 17:58:43 +03:00
|
|
|
}
|
|
|
|
errorSubclass(LinkWithoutLabelOrRoleError)
|
|
|
|
|
|
|
|
function LabelMissingControl(element) {
|
|
|
|
this.name = 'LabelMissingControl'
|
|
|
|
this.stack = new Error().stack
|
|
|
|
this.element = element
|
2017-07-31 17:25:41 +03:00
|
|
|
this.message = 'Label missing control on ' + inspect(element)
|
2017-06-26 17:58:43 +03:00
|
|
|
}
|
|
|
|
errorSubclass(LabelMissingControl)
|
|
|
|
|
|
|
|
function ButtonWithoutLabelError(element) {
|
|
|
|
this.name = 'ButtonWithoutLabelError'
|
|
|
|
this.stack = new Error().stack
|
|
|
|
this.element = element
|
2017-07-31 17:25:41 +03:00
|
|
|
this.message = 'Missing text or aria-label attribute on ' + inspect(element)
|
2017-06-26 17:58:43 +03:00
|
|
|
}
|
|
|
|
errorSubclass(ButtonWithoutLabelError)
|
|
|
|
|
|
|
|
function ARIAAttributeMissingError(element, attr) {
|
|
|
|
this.name = 'ARIAAttributeMissingError'
|
|
|
|
this.stack = new Error().stack
|
|
|
|
this.element = element
|
2017-07-31 17:25:41 +03:00
|
|
|
this.message = 'Missing '+attr+' attribute on ' + inspect(element)
|
2017-06-22 16:36:39 +03:00
|
|
|
}
|
2017-06-26 17:58:43 +03:00
|
|
|
errorSubclass(ARIAAttributeMissingError)
|
|
|
|
|
2017-06-27 16:15:30 +03:00
|
|
|
var SelectorARIAPairs = {
|
2017-06-26 17:58:43 +03:00
|
|
|
".js-menu-target": ["aria-expanded", "aria-haspopup"],
|
|
|
|
".js-details-target": ["aria-expanded"]
|
|
|
|
}
|
|
|
|
|
|
|
|
function elementIsHidden(element) {
|
|
|
|
return element.getAttribute('aria-hidden') === 'true' || element.closest('[aria-hidden="true"]')
|
|
|
|
}
|
|
|
|
|
|
|
|
function isText(value) {
|
|
|
|
return typeof value === 'string' && !!value.trim()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Public: Check if an element has text visible by sight or screen reader.
|
|
|
|
//
|
|
|
|
// Examples
|
|
|
|
//
|
|
|
|
// <img alt="github" src="github.png">
|
|
|
|
// # => true
|
|
|
|
//
|
|
|
|
// <span aria-label="Open"></span>
|
|
|
|
// # => true
|
|
|
|
//
|
|
|
|
// <button></button>
|
|
|
|
// # => false
|
|
|
|
//
|
|
|
|
// Returns String text.
|
|
|
|
function accessibleText(node) {
|
|
|
|
switch (node.nodeType) {
|
|
|
|
case Node.ELEMENT_NODE:
|
|
|
|
if (isText(node.getAttribute('alt')) || isText(node.getAttribute('aria-label')) || isText(node.getAttribute('title'))) return true
|
2017-06-27 17:05:04 +03:00
|
|
|
for (var i = 0; i < node.childNodes.length; i++) {
|
2017-06-27 16:15:30 +03:00
|
|
|
if (accessibleText(node.childNodes[i])) return true
|
2017-06-26 17:58:43 +03:00
|
|
|
}
|
|
|
|
break
|
|
|
|
case Node.TEXT_NODE:
|
|
|
|
return isText(node.data)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-31 17:25:41 +03:00
|
|
|
function inspect(element) {
|
|
|
|
var tagHTML = element.outerHTML
|
|
|
|
if (element.innerHTML) tagHTML = tagHTML.replace(element.innerHTML, '...')
|
|
|
|
|
|
|
|
var parents = []
|
|
|
|
while (element) {
|
2017-08-21 14:09:28 +03:00
|
|
|
if (element.nodeName === 'BODY') break
|
2017-07-31 17:25:41 +03:00
|
|
|
parents.push(selectors(element))
|
2017-08-21 14:09:28 +03:00
|
|
|
// Stop traversing when we hit an ID
|
|
|
|
if (element.id) break
|
2017-07-31 17:25:41 +03:00
|
|
|
element = element.parentNode
|
|
|
|
}
|
|
|
|
return '"' + parents.reverse().join(' > ') + '". \n\n' + tagHTML
|
|
|
|
}
|
|
|
|
|
|
|
|
function selectors(element) {
|
|
|
|
var componenets = [element.nodeName.toLowerCase()]
|
|
|
|
if (element.id) componenets.push('#' + element.id)
|
|
|
|
if (element.classList) {
|
|
|
|
element.classList.forEach(function(name) {
|
|
|
|
if (name.match(/^js-/)) componenets.push('.' + name)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return componenets.join('')
|
|
|
|
}
|
|
|
|
|
2017-06-26 17:58:43 +03:00
|
|
|
return scanForProblems
|
2017-06-27 17:39:23 +03:00
|
|
|
}))
|