accessibility-developer-tools
Version:
This is a library of accessibility-related testing and utility code.
928 lines (856 loc) • 37.3 kB
JavaScript
// Copyright 2012 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.require('axs.browserUtils');
goog.require('axs.color');
goog.require('axs.dom');
goog.require('axs.utils');
goog.provide('axs.properties');
/**
* @const
* @type {string}
*/
axs.properties.TEXT_CONTENT_XPATH = './/text()[normalize-space(.)!=""]/parent::*[name()!="script"]';
/**
* @param {Element} element
* @return {Object.<string, Object>}
*/
axs.properties.getFocusProperties = function(element) {
var focusProperties = {};
var tabindex = element.getAttribute('tabindex');
if (tabindex != undefined) {
focusProperties['tabindex'] = { value: tabindex, valid: true };
} else {
if (axs.utils.isElementImplicitlyFocusable(element))
focusProperties['implicitlyFocusable'] = { value: true, valid: true };
}
if (Object.keys(focusProperties).length == 0)
return null;
var transparent = axs.utils.elementIsTransparent(element);
var zeroArea = axs.utils.elementHasZeroArea(element);
var outsideScrollArea = axs.utils.elementIsOutsideScrollArea(element);
var overlappingElements = axs.utils.overlappingElements(element);
if (transparent || zeroArea || outsideScrollArea || overlappingElements.length > 0) {
var hidden = axs.utils.isElementOrAncestorHidden(element);
var visibleProperties = { value: false,
valid: hidden };
if (transparent)
visibleProperties['transparent'] = true;
if (zeroArea)
visibleProperties['zeroArea'] = true;
if (outsideScrollArea)
visibleProperties['outsideScrollArea'] = true;
if (overlappingElements && overlappingElements.length > 0)
visibleProperties['overlappingElements'] = overlappingElements;
var hiddenProperties = { value: hidden, valid: hidden };
if (hidden)
hiddenProperties['reason'] = axs.properties.getHiddenReason(element);
visibleProperties['hidden'] = hiddenProperties;
focusProperties['visible'] = visibleProperties;
} else {
focusProperties['visible'] = { value: true, valid: true };
}
return focusProperties;
};
/**
* @typedef {{ property: string,
* on: Element }}
*
* property examples: 'display: none', 'visibility: hidden', 'aria-hidden'
*/
axs.properties.hiddenReason;
/**
* Determine the reason an element is not visible.
* Will give the CSS rule or attribute and the element/ancestor it is set on.
* @param {Element} element
* @return {?axs.properties.hiddenReason}
*/
axs.properties.getHiddenReason = function(element) {
if (!element || !(element instanceof element.ownerDocument.defaultView.HTMLElement))
return null;
if (element.hasAttribute('chromevoxignoreariahidden'))
var chromevoxignoreariahidden = true;
var style = window.getComputedStyle(element, null);
if (style.display == 'none')
return { 'property': 'display: none',
'on': element };
if (style.visibility == 'hidden')
return { 'property': 'visibility: hidden',
'on': element };
if (element.hasAttribute('aria-hidden') &&
element.getAttribute('aria-hidden').toLowerCase() == 'true') {
if (!chromevoxignoreariahidden)
return { 'property': 'aria-hidden',
'on': element };
}
return axs.properties.getHiddenReason(axs.dom.parentElement(element));
};
/**
* @param {Element} element
* @return {Object.<string, Object>}
*/
axs.properties.getColorProperties = function(element) {
var colorProperties = {};
var contrastRatioProperties =
axs.properties.getContrastRatioProperties(element);
if (contrastRatioProperties)
colorProperties['contrastRatio'] = contrastRatioProperties;
if (Object.keys(colorProperties).length == 0)
return null;
return colorProperties;
};
/**
* Determines whether the given element has a text node as a direct descendant.
* @param {Element} element
* @return {boolean}
*/
axs.properties.hasDirectTextDescendant = function(element) {
var ownerDocument;
if (element.nodeType == Node.DOCUMENT_NODE)
ownerDocument = element;
else
ownerDocument = element.ownerDocument;
if (ownerDocument.evaluate) {
return hasDirectTextDescendantXpath();
}
return hasDirectTextDescendantTreeWalker();
/**
* Determines whether element has a text node as a direct descendant.
* This method uses XPath on HTML DOM which is not universally supported.
* @return {boolean}
*/
function hasDirectTextDescendantXpath() {
var selectorResults = ownerDocument.evaluate(axs.properties.TEXT_CONTENT_XPATH,
element,
null,
XPathResult.ANY_TYPE,
null);
for (var resultElement = selectorResults.iterateNext();
resultElement != null;
resultElement = selectorResults.iterateNext()) {
if (resultElement !== element)
continue;
return true;
}
return false;
}
/**
* Determines whether element has a text node as a direct descendant.
* This method uses TreeWalker as a fallback (at time of writing no version
* of IE (including IE11) supports XPath in the HTML DOM).
* @return {boolean}
*/
function hasDirectTextDescendantTreeWalker() {
var treeWalker = ownerDocument.createTreeWalker(element,
NodeFilter.SHOW_TEXT,
null,
false);
while (treeWalker.nextNode()) {
var resultElement = treeWalker.currentNode;
var parent = resultElement.parentNode;
// Handle elements hosted in <template>.content.
parent = parent.host || parent;
var tagName = parent.tagName.toLowerCase();
var value = resultElement.nodeValue.trim();
if (value && tagName !== 'script' && element !== resultElement)
return true;
}
return false;
}
};
/**
* @param {Element} element
* @return {Object.<string, Object>}
*/
axs.properties.getContrastRatioProperties = function(element) {
if (!axs.properties.hasDirectTextDescendant(element))
return null;
var contrastRatioProperties = {};
var style = window.getComputedStyle(element, null);
var bgColor = axs.utils.getBgColor(style, element);
if (!bgColor)
return null;
contrastRatioProperties['backgroundColor'] = axs.color.colorToString(bgColor);
var fgColor = axs.utils.getFgColor(style, element, bgColor);
contrastRatioProperties['foregroundColor'] = axs.color.colorToString(fgColor);
var contrast = axs.utils.getContrastRatioForElementWithComputedStyle(style, element);
if (!contrast)
return null;
contrastRatioProperties['value'] = contrast.toFixed(2);
if (axs.utils.isLowContrast(contrast, style))
contrastRatioProperties['alert'] = true;
var levelAAContrast = axs.utils.isLargeFont(style) ? 3.0 : 4.5;
var levelAAAContrast = axs.utils.isLargeFont(style) ? 4.5 : 7.0;
var desiredContrastRatios = {};
if (levelAAContrast > contrast)
desiredContrastRatios['AA'] = levelAAContrast;
if (levelAAAContrast > contrast)
desiredContrastRatios['AAA'] = levelAAAContrast;
if (!Object.keys(desiredContrastRatios).length)
return contrastRatioProperties;
var suggestedColors = axs.color.suggestColors(bgColor, fgColor, desiredContrastRatios);
if (suggestedColors && Object.keys(suggestedColors).length)
contrastRatioProperties['suggestedColors'] = suggestedColors;
return contrastRatioProperties;
};
/**
* @param {Node} node
* @param {!Object} textAlternatives The properties object to fill in
* @param {boolean=} opt_recursive Whether this is a recursive call or not
* @param {boolean=} opt_force Whether to return text alternatives for this
* element regardless of its hidden state.
* @return {?string} The calculated text alternative for the given element
*/
axs.properties.findTextAlternatives = function(node, textAlternatives, opt_recursive, opt_force) {
var recursive = opt_recursive || false;
/** @type {Element} */ var element = axs.dom.asElement(node);
if (!element)
return null;
// 1. Skip hidden elements unless the author specifies to use them via an aria-labelledby or
// aria-describedby being used in the current computation.
if (!opt_force && axs.utils.isElementOrAncestorHidden(element))
return null;
// if this is a text node, just return text content.
if (node.nodeType == Node.TEXT_NODE) {
var textContentValue = {};
textContentValue.type = 'text';
textContentValue.text = node.textContent;
textContentValue.lastWord = axs.properties.getLastWord(textContentValue.text);
textAlternatives['content'] = textContentValue;
return node.textContent;
}
var computedName = null;
if (!recursive) {
// 2A. The aria-labelledby attribute takes precedence as the element's text alternative
// unless this computation is already occurring as the result of a recursive aria-labelledby
// declaration.
computedName = axs.properties.getTextFromAriaLabelledby(element, textAlternatives);
}
// 2A. If aria-labelledby is empty or undefined, the aria-label attribute, which defines an
// explicit text string, is used.
if (element.hasAttribute('aria-label')) {
var ariaLabelValue = {};
ariaLabelValue.type = 'text';
ariaLabelValue.text = element.getAttribute('aria-label');
ariaLabelValue.lastWord = axs.properties.getLastWord(ariaLabelValue.text);
if (computedName)
ariaLabelValue.unused = true;
else if (!(recursive && axs.utils.elementIsHtmlControl(element)))
computedName = ariaLabelValue.text;
textAlternatives['ariaLabel'] = ariaLabelValue;
}
// 2A. If aria-labelledby and aria-label are both empty or undefined, and if the element is not
// marked as presentational (role="presentation", check for the presence of an equivalent host
// language attribute or element for associating a label, and use those mechanisms to determine
// a text alternative.
if (!element.hasAttribute('role') || element.getAttribute('role') != 'presentation') {
computedName = axs.properties.getTextFromHostLanguageAttributes(element,
textAlternatives,
computedName,
recursive);
}
// 2B (HTML version).
if (recursive && axs.utils.elementIsHtmlControl(element)) {
var defaultView = element.ownerDocument.defaultView;
// include the value of the embedded control as part of the text alternative in the
// following manner:
if (element instanceof defaultView.HTMLInputElement) {
// If the embedded control is a text field, use its value.
var inputElement = /** @type {HTMLInputElement} */ (element);
if (inputElement.type == 'text') {
if (inputElement.value && inputElement.value.length > 0)
textAlternatives['controlValue'] = { 'text': inputElement.value };
}
// If the embedded control is a range (e.g. a spinbutton or slider), use the value of the
// aria-valuetext attribute if available, or otherwise the value of the aria-valuenow
// attribute.
if (inputElement.type == 'range')
textAlternatives['controlValue'] = { 'text': inputElement.value };
}
// If the embedded control is a menu, use the text alternative of the chosen menu item.
// If the embedded control is a select or combobox, use the chosen option.
if (element instanceof defaultView.HTMLSelectElement) {
var inputElement = /** @type {HTMLSelectElement} */ (element);
textAlternatives['controlValue'] = { 'text': inputElement.value };
}
if (textAlternatives['controlValue']) {
var controlValue = textAlternatives['controlValue'];
if (computedName)
controlValue.unused = true;
else
computedName = controlValue.text;
}
}
// 2B (ARIA version).
if (recursive && axs.utils.elementIsAriaWidget(element)) {
var role = element.getAttribute('role');
// If the embedded control is a text field, use its value.
if (role == 'textbox') {
if (element.textContent && element.textContent.length > 0)
textAlternatives['controlValue'] = { 'text': element.textContent };
}
// If the embedded control is a range (e.g. a spinbutton or slider), use the value of the
// aria-valuetext attribute if available, or otherwise the value of the aria-valuenow
// attribute.
if (role == 'slider' || role == 'spinbutton') {
if (element.hasAttribute('aria-valuetext'))
textAlternatives['controlValue'] = { 'text': element.getAttribute('aria-valuetext') };
else if (element.hasAttribute('aria-valuenow'))
textAlternatives['controlValue'] = { 'value': element.getAttribute('aria-valuenow'),
'text': '' + element.getAttribute('aria-valuenow') };
}
// If the embedded control is a menu, use the text alternative of the chosen menu item.
if (role == 'menu') {
var menuitems = element.querySelectorAll('[role=menuitemcheckbox], [role=menuitemradio]');
var selectedMenuitems = [];
for (var i = 0; i < menuitems.length; i++) {
if (menuitems[i].getAttribute('aria-checked') == 'true')
selectedMenuitems.push(menuitems[i]);
}
if (selectedMenuitems.length > 0) {
var selectedMenuText = '';
for (var i = 0; i < selectedMenuitems.length; i++) {
selectedMenuText += axs.properties.findTextAlternatives(selectedMenuitems[i], {}, true);
if (i < selectedMenuitems.length - 1)
selectedMenuText += ', ';
}
textAlternatives['controlValue'] = { 'text': selectedMenuText };
}
}
// If the embedded control is a select or combobox, use the chosen option.
if (role == 'combobox' || role == 'select') {
// TODO
textAlternatives['controlValue'] = { 'text': 'TODO' };
}
if (textAlternatives['controlValue']) {
var controlValue = textAlternatives['controlValue'];
if (computedName)
controlValue.unused = true;
else
computedName = controlValue.text;
}
}
// 2C. Otherwise, if the attributes checked in rules A and B didn't provide results, text is
// collected from descendant content if the current element's role allows "Name From: contents."
var hasRole = element.hasAttribute('role');
var canGetNameFromContents = true;
if (hasRole) {
var roleName = element.getAttribute('role');
// if element has a role, check that it allows "Name From: contents"
var role = axs.constants.ARIA_ROLES[roleName];
if (role && (!role.namefrom || role.namefrom.indexOf('contents') < 0))
canGetNameFromContents = false;
}
var textFromContent = axs.properties.getTextFromDescendantContent(element, opt_force);
if (textFromContent && canGetNameFromContents) {
var textFromContentValue = {};
textFromContentValue.type = 'text';
textFromContentValue.text = textFromContent;
textFromContentValue.lastWord = axs.properties.getLastWord(textFromContentValue.text);
if (computedName)
textFromContentValue.unused = true;
else
computedName = textFromContent;
textAlternatives['content'] = textFromContentValue;
}
// 2D. The last resort is to use text from a tooltip attribute (such as the title attribute in
// HTML). This is used only if nothing else, including subtree content, has provided results.
if (element.hasAttribute('title')) {
var titleValue = {};
titleValue.type = 'string';
titleValue.valid = true;
titleValue.text = element.getAttribute('title');
titleValue.lastWord = axs.properties.getLastWord(titleValue.lastWord);
if (computedName)
titleValue.unused = true;
else
computedName = titleValue.text;
textAlternatives['title'] = titleValue;
}
if (Object.keys(textAlternatives).length == 0 && computedName == null)
return null;
return computedName;
};
/**
* @param {Element} element
* @param {boolean=} opt_force Whether to return text alternatives for this
* element regardless of its hidden state.
* @return {?string}
*/
axs.properties.getTextFromDescendantContent = function(element, opt_force) {
var children = element.childNodes;
var childrenTextContent = [];
for (var i = 0; i < children.length; i++) {
var childTextContent = axs.properties.findTextAlternatives(children[i], {}, true, opt_force);
if (childTextContent)
childrenTextContent.push(childTextContent.trim());
}
if (childrenTextContent.length) {
var result = '';
// Empty children are allowed, but collapse all of them
for (var i = 0; i < childrenTextContent.length; i++)
result = [result, childrenTextContent[i]].join(' ').trim();
return result;
}
return null;
};
/**
* @param {Element} element
* @param {Object} textAlternatives
* @return {?string}
*/
axs.properties.getTextFromAriaLabelledby = function(element, textAlternatives) {
var computedName = null;
if (!element.hasAttribute('aria-labelledby'))
return computedName;
var labelledbyAttr = element.getAttribute('aria-labelledby');
var labelledbyIds = labelledbyAttr.split(/\s+/);
var labelledbyValue = {};
labelledbyValue.valid = true;
var labelledbyText = [];
var labelledbyValues = [];
for (var i = 0; i < labelledbyIds.length; i++) {
var labelledby = {};
labelledby.type = 'element';
var labelledbyId = labelledbyIds[i];
labelledby.value = labelledbyId;
var labelledbyElement = document.getElementById(labelledbyId);
if (!labelledbyElement) {
labelledby.valid = false;
labelledbyValue.valid = false;
labelledby.errorMessage = { 'messageKey': 'noElementWithId', 'args': [labelledbyId] };
} else {
labelledby.valid = true;
labelledby.text = axs.properties.findTextAlternatives(labelledbyElement, {}, true, true);
labelledby.lastWord = axs.properties.getLastWord(labelledby.text);
labelledbyText.push(labelledby.text);
labelledby.element = labelledbyElement;
}
labelledbyValues.push(labelledby);
}
if (labelledbyValues.length > 0) {
labelledbyValues[labelledbyValues.length - 1].last = true;
labelledbyValue.values = labelledbyValues;
labelledbyValue.text = labelledbyText.join(' ');
labelledbyValue.lastWord = axs.properties.getLastWord(labelledbyValue.text);
computedName = labelledbyValue.text;
textAlternatives['ariaLabelledby'] = labelledbyValue;
}
return computedName;
};
/**
* Determine the text description/label for an element.
* For example will attempt to find the alt text for an image or label text for a form control.
* @param {!Element} element
* @param {!Object} textAlternatives An object that will be updated with information.
* @param {?string} existingComputedname
* @param {boolean} recursive Whether this method is being called recursively as described in
* http://www.w3.org/TR/wai-aria/roles#textalternativecomputation section 2A.
* @return {Object}
*/
axs.properties.getTextFromHostLanguageAttributes = function(element,
textAlternatives,
existingComputedname,
recursive) {
var computedName = existingComputedname;
if (axs.browserUtils.matchSelector(element, 'img') && element.hasAttribute('alt')) {
var altValue = {};
altValue.type = 'string';
altValue.valid = true;
altValue.text = element.getAttribute('alt');
if (computedName)
altValue.unused = true;
else
computedName = altValue.text;
textAlternatives['alt'] = altValue;
}
var controlsSelector = ['input:not([type="hidden"]):not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'button:not([disabled])',
'video:not([disabled])'].join(', ');
if (axs.browserUtils.matchSelector(element, controlsSelector) && !recursive) {
if (element.hasAttribute('id')) {
var labelForQuerySelector = 'label[for="' + element.id + '"]';
var labelsFor = document.querySelectorAll(labelForQuerySelector);
var labelForValue = {};
var labelForValues = [];
var labelForText = [];
for (var i = 0; i < labelsFor.length; i++) {
var labelFor = {};
labelFor.type = 'element';
var label = labelsFor[i];
var labelText = axs.properties.findTextAlternatives(label, {}, true);
if (labelText && labelText.trim().length > 0) {
labelFor.text = labelText.trim();
labelForText.push(labelText.trim());
}
labelFor.element = label;
labelForValues.push(labelFor);
}
if (labelForValues.length > 0) {
labelForValues[labelForValues.length - 1].last = true;
labelForValue.values = labelForValues;
labelForValue.text = labelForText.join(' ');
labelForValue.lastWord = axs.properties.getLastWord(labelForValue.text);
if (computedName)
labelForValue.unused = true;
else
computedName = labelForValue.text;
textAlternatives['labelFor'] = labelForValue;
}
}
var parent = axs.dom.parentElement(element);
var labelWrappedValue = {};
while (parent) {
if (parent.tagName.toLowerCase() == 'label') {
var parentLabel = /** @type {HTMLLabelElement} */ (parent);
if (parentLabel.control == element) {
labelWrappedValue.type = 'element';
labelWrappedValue.text = axs.properties.findTextAlternatives(parentLabel, {}, true);
labelWrappedValue.lastWord = axs.properties.getLastWord(labelWrappedValue.text);
labelWrappedValue.element = parentLabel;
break;
}
}
parent = axs.dom.parentElement(parent);
}
if (labelWrappedValue.text) {
if (computedName)
labelWrappedValue.unused = true;
else
computedName = labelWrappedValue.text;
textAlternatives['labelWrapped'] = labelWrappedValue;
}
// If all else fails input of type image can fall back to its alt text
if (axs.browserUtils.matchSelector(element, 'input[type="image"]') && element.hasAttribute('alt')) {
var altValue = {};
altValue.type = 'string';
altValue.valid = true;
altValue.text = element.getAttribute('alt');
if (computedName)
altValue.unused = true;
else
computedName = altValue.text;
textAlternatives['alt'] = altValue;
}
if (!Object.keys(textAlternatives).length)
textAlternatives['noLabel'] = true;
}
return computedName;
};
/**
* @param {?string} text
* @return {?string}
*/
axs.properties.getLastWord = function(text) {
if (!text)
return null;
// TODO: this makes a lot of assumptions.
var lastSpace = text.lastIndexOf(' ') + 1;
var MAXLENGTH = 10;
var cutoff = text.length - MAXLENGTH;
var wordStart = lastSpace > cutoff ? lastSpace : cutoff;
return text.substring(wordStart);
};
/**
* @param {Node} node
* @return {Object}
*/
axs.properties.getTextProperties = function(node) {
var textProperties = {};
var computedName = axs.properties.findTextAlternatives(node, textProperties, false, true);
if (Object.keys(textProperties).length == 0) {
/** @type {Element} */ var element = axs.dom.asElement(node);
if (element && axs.browserUtils.matchSelector(element, 'img')) {
var altValue = {};
altValue.valid = false;
altValue.errorMessage = 'No alt value provided';
textProperties['alt'] = altValue;
var src = element.src;
if (typeof src == 'string') {
var parts = src.split('/');
var filename = parts.pop();
var filenameValue = { text: filename };
textProperties['filename'] = filenameValue;
computedName = filename;
}
}
if (!computedName)
return null;
}
textProperties.hasProperties = Boolean(Object.keys(textProperties).length);
textProperties.computedText = computedName;
textProperties.lastWord = axs.properties.getLastWord(computedName);
return textProperties;
};
/**
* Finds any ARIA attributes (roles, states and properties) explicitly set on this element.
* @param {Element} element
* @return {Object}
*/
axs.properties.getAriaProperties = function(element) {
var ariaProperties = {};
var statesAndProperties = axs.properties.getGlobalAriaProperties(element);
for (var property in axs.constants.ARIA_PROPERTIES) {
var attributeName = 'aria-' + property;
if (element.hasAttribute(attributeName)) {
var propertyValue = element.getAttribute(attributeName);
statesAndProperties[attributeName] =
axs.utils.getAriaPropertyValue(attributeName, propertyValue, element);
}
}
if (Object.keys(statesAndProperties).length > 0)
ariaProperties['properties'] = axs.utils.values(statesAndProperties);
var roles = axs.utils.getRoles(element);
if (!roles) {
if (Object.keys(ariaProperties).length)
return ariaProperties;
return null;
}
ariaProperties['roles'] = roles;
if (!roles.valid || !roles['roles'])
return ariaProperties;
var roleDetails = roles['roles'];
for (var i = 0; i < roleDetails.length; i++) {
var role = roleDetails[i];
if (!role.details || !role.details.propertiesSet)
continue;
for (var property in role.details.propertiesSet) {
if (property in statesAndProperties)
continue;
if (element.hasAttribute(property)) {
var propertyValue = element.getAttribute(property);
statesAndProperties[property] =
axs.utils.getAriaPropertyValue(property, propertyValue, element);
if ('values' in statesAndProperties[property]) {
var values = statesAndProperties[property].values;
values[values.length - 1].isLast = true;
}
} else if (role.details.requiredPropertiesSet[property]) {
statesAndProperties[property] =
{ 'name': property, 'valid': false, 'reason': 'Required property not set' };
}
}
}
if (Object.keys(statesAndProperties).length > 0)
ariaProperties['properties'] = axs.utils.values(statesAndProperties);
if (Object.keys(ariaProperties).length > 0)
return ariaProperties;
return null;
};
/**
* Gets the ARIA properties found on this element which apply to all elements, not just elements with ARIA roles.
* @param {Element} element
* @return {!Object}
*/
axs.properties.getGlobalAriaProperties = function(element) {
var globalProperties = {};
for (var property in axs.constants.GLOBAL_PROPERTIES) {
if (element.hasAttribute(property)) {
var propertyValue = element.getAttribute(property);
globalProperties[property] =
axs.utils.getAriaPropertyValue(property, propertyValue, element);
}
}
return globalProperties;
};
/**
* @param {Element} element
* @return {Object.<string, Object>}
*/
axs.properties.getVideoProperties = function(element) {
var videoSelector = 'video';
if (!axs.browserUtils.matchSelector(element, videoSelector))
return null;
var videoProperties = {};
videoProperties['captionTracks'] = axs.properties.getTrackElements(element, 'captions');
videoProperties['descriptionTracks'] = axs.properties.getTrackElements(element, 'descriptions');
videoProperties['chapterTracks'] = axs.properties.getTrackElements(element, 'chapters');
// error if no text alternatives?
return videoProperties;
};
/**
* @param {Element} element
* @param {string} kind
* @return {Object}
*/
axs.properties.getTrackElements = function(element, kind) {
// error if resource is not available
var trackElements = element.querySelectorAll('track[kind=' + kind + ']');
var result = {};
if (!trackElements.length) {
result.valid = false;
result.reason = { 'messageKey': 'noTracksProvided', 'args': [[kind]] };
return result;
}
result.valid = true;
var values = [];
for (var i = 0; i < trackElements.length; i++) {
var trackElement = {};
var src = trackElements[i].getAttribute('src');
var srcLang = trackElements[i].getAttribute('srcLang');
var label = trackElements[i].getAttribute('label');
if (!src) {
trackElement.valid = false;
trackElement.reason = { 'messageKey': 'noSrcProvided' };
} else {
trackElement.valid = true;
trackElement.src = src;
}
var name = '';
if (label) {
name += label;
if (srcLang)
name += ' ';
}
if (srcLang)
name += '(' + srcLang + ')';
if (name == '')
name = '[' + { 'messageKey': 'unnamed' } + ']';
trackElement.name = name;
values.push(trackElement);
}
result.values = values;
return result;
};
/**
* @param {Node} node
* @return {Object.<string, Object>}
*/
axs.properties.getAllProperties = function(node) {
/** @type {Element} */ var element = axs.dom.asElement(node);
if (!element)
return {};
var allProperties = {};
allProperties['ariaProperties'] = axs.properties.getAriaProperties(element);
allProperties['colorProperties'] = axs.properties.getColorProperties(element);
allProperties['focusProperties'] = axs.properties.getFocusProperties(element);
allProperties['textProperties'] = axs.properties.getTextProperties(node);
allProperties['videoProperties'] = axs.properties.getVideoProperties(element);
return allProperties;
};
(function() {
/**
* Helper for implicit semantic functionality.
* Can be made part of the public API if need be.
* @param {Element} element
* @return {?axs.constants.HtmlInfo}
*/
function getHtmlInfo(element) {
if (!element)
return null;
var tagName = element.tagName;
if (!tagName)
return null;
tagName = tagName.toUpperCase();
var infos = axs.constants.TAG_TO_IMPLICIT_SEMANTIC_INFO[tagName];
if (!infos || !infos.length)
return null;
var defaultInfo = null; // will contain the info with no specific selector if no others match
for (var i = 0, len = infos.length; i < len; i++) {
var htmlInfo = infos[i];
if (htmlInfo.selector) {
if (axs.browserUtils.matchSelector(element, htmlInfo.selector))
return htmlInfo;
} else {
defaultInfo = htmlInfo;
}
}
return defaultInfo;
}
/**
* @param {Element} element
* @return {string} role
*/
axs.properties.getImplicitRole = function(element) {
var htmlInfo = getHtmlInfo(element);
if (htmlInfo)
return htmlInfo.role;
return '';
};
/**
* Determine if this element can take ANY ARIA attributes including roles, state and properties.
* If false then even global attributes should not be used.
* @param {Element} element
* @return {boolean}
*/
axs.properties.canTakeAriaAttributes = function(element) {
var htmlInfo = getHtmlInfo(element);
if (htmlInfo)
return !htmlInfo.reserved;
return true;
};
})();
/**
* This lists the ARIA attributes that are supported implicitly by native properties of this element.
*
* @param {Element} element The element to check.
* @return {!Array.<string>} An array of ARIA attributes.
*
* example:
* var element = document.createElement("input");
* element.setAttribute("type", "range");
* var supported = axs.properties.getNativelySupportedAttributes(element); // an array of ARIA attributes
* console.log(supported.indexOf("aria-valuemax") >=0); // logs 'true'
*/
axs.properties.getNativelySupportedAttributes = function(element) {
var result = [];
if (!element) {
return result;
}
var testElement = element.cloneNode(false); // gets rid of expandos
var ariaAttributes = Object.keys(/** @type {!Object} */(axs.constants.ARIA_TO_HTML_ATTRIBUTE));
for (var i = 0; i < ariaAttributes.length; i++) {
var ariaAttribute = ariaAttributes[i];
var nativeAttribute = axs.constants.ARIA_TO_HTML_ATTRIBUTE[ariaAttribute];
if (nativeAttribute in testElement) {
result[result.length] = ariaAttribute;
}
}
return result;
};
(function() {
var roleToSelectorCache = {}; // performance optimization, cache results from getSelectorForRole
/**
* Build a selector that will match elements which implicity or explicitly have this role.
* Note that the selector will probably not look elegant but it will work.
* @param {string} role
* @return {string} selector
*/
axs.properties.getSelectorForRole = function(role) {
if (!role)
return '';
if (roleToSelectorCache[role] && roleToSelectorCache.hasOwnProperty(role))
return roleToSelectorCache[role];
var selectors = ['[role="' + role + '"]'];
var tagNames = Object.keys(/** @type {!Object} */(axs.constants.TAG_TO_IMPLICIT_SEMANTIC_INFO));
tagNames.forEach(function(tagName) {
var htmlInfos = axs.constants.TAG_TO_IMPLICIT_SEMANTIC_INFO[tagName];
if (htmlInfos && htmlInfos.length) {
for (var i = 0; i < htmlInfos.length; i++) {
var htmlInfo = htmlInfos[i];
if (htmlInfo.role === role) {
if (htmlInfo.selector) {
selectors[selectors.length] = htmlInfo.selector;
} else {
selectors[selectors.length] = tagName; // Selectors API is not case sensitive.
break; // No need to continue adding selectors since we will match the tag itself.
}
}
}
}
});
return (roleToSelectorCache[role] = selectors.join(','));
};
})();