accessibilityjs/index.js

176 строки
5.5 KiB
JavaScript
Исходник Обычный вид История

2017-06-22 16:36:39 +03:00
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory);
} else {
2017-06-27 14:35:01 +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 i
var imgElements = context.querySelectorAll('img')
for (i = 0; i < imgElements.length; i++) {
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')
for (i = 0; i < aElements.length; i++) {
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')
for (i = 0; i < buttonElements.length; i++) {
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')
for (i = 0; i < labelElements.length; i++) {
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')
for (i = 0; i < inputElements.length; i++) {
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 16:15:30 +03:00
var selectorElements = Object.keys(SelectorARIAPairs)
for (i = 0; i < selectorElements.length; i++) {
var selector = selectorElements[i]
var ARIAAttrsRequired = SelectorARIAPairs[selector]
var targetElements = context.querySelectorAll(selector)
for (var j = 0; i < targetElements.length; j++) {
var target = targetElements[j]
var missingAttrs = []
for (var k = 0; attr < ARIAAttrsRequired.length; k++) {
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-06-27 16:15:30 +03:00
fn.prototype.varructor = 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
this.message = `Missing alt attribute on ${element.outerHTML}`
}
errorSubclass(ImageWithoutAltAttributeError)
function ElementWithoutLabelError(element) {
this.name = 'ElementWithoutLabelError'
this.stack = new Error().stack
this.element = element
this.message = `Missing text, title, or aria-label attribute on ${element.outerHTML}`
}
errorSubclass(ElementWithoutLabelError)
function LinkWithoutLabelOrRoleError(element) {
this.name = 'LinkWithoutLabelOrRoleError'
this.stack = new Error().stack
this.element = element
this.message = `Missing href or role=button on ${element.outerHTML}`
}
errorSubclass(LinkWithoutLabelOrRoleError)
function LabelMissingControl(element) {
this.name = 'LabelMissingControl'
this.stack = new Error().stack
this.element = element
this.message = `Label missing control on ${element.outerHTML}`
}
errorSubclass(LabelMissingControl)
function ButtonWithoutLabelError(element) {
this.name = 'ButtonWithoutLabelError'
this.stack = new Error().stack
this.element = element
this.message = `Missing text or aria-label attribute on ${element.outerHTML}`
}
errorSubclass(ButtonWithoutLabelError)
function ARIAAttributeMissingError(element, attr) {
this.name = 'ARIAAttributeMissingError'
this.stack = new Error().stack
this.element = element
this.message = `Missing ${attr} attribute on ${element.outerHTML}`
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 16:15:30 +03:00
for (i = 0; i < node.childNodes.length; i++) {
if (accessibleText(node.childNodes[i])) return true
2017-06-26 17:58:43 +03:00
}
break
case Node.TEXT_NODE:
return isText(node.data)
}
}
return scanForProblems
}));