axe-core
Version:
Accessibility engine for automated Web UI testing
193 lines (176 loc) • 5.54 kB
JavaScript
/* global text, aria, dom */
const controlValueRoles = [
'textbox',
'progressbar',
'scrollbar',
'slider',
'spinbutton',
'combobox',
'listbox'
];
text.formControlValueMethods = {
nativeTextboxValue,
nativeSelectValue,
ariaTextboxValue,
ariaListboxValue,
ariaComboboxValue,
ariaRangeValue
};
/**
* Calculate value of a form control
*
* @param {VirtualNode} element The VirtualNode instance whose value we want
* @param {Object} context
* @property {Element} startNode First node in accessible name computation
* @property {String[]} unsupported List of roles where value computation is unsupported
* @property {Bool} debug Enable logging for formControlValue
* @return {string} The calculated value
*/
text.formControlValue = function formControlValue(virtualNode, context = {}) {
const { actualNode } = virtualNode;
const unsupported = text.unsupported.accessibleNameFromFieldValue || [];
const role = aria.getRole(actualNode);
if (
// For the targeted node, the accessible name is never the value:
context.startNode === virtualNode ||
// Only elements with a certain role can return their value
!controlValueRoles.includes(role) ||
// Skip unsupported roles
unsupported.includes(role)
) {
return '';
}
// Object.values:
const valueMethods = Object.keys(text.formControlValueMethods).map(
name => text.formControlValueMethods[name]
);
// Return the value of the first step that returns with text
let valueString = valueMethods.reduce((accName, step) => {
return accName || step(virtualNode, context);
}, '');
if (context.debug) {
axe.log(valueString || '{empty-value}', actualNode, context);
}
return valueString;
};
/**
* Calculate value of a native textbox element (input and textarea)
*
* @param {VirtualNode|Element} element The element whose value we want
* @return {string} The calculated value
*/
function nativeTextboxValue(node) {
node = node.actualNode || node;
if (axe.commons.forms.isNativeTextbox(node)) {
return node.value || '';
}
return '';
}
/**
* Calculate value of a select element
*
* @param {VirtualNode} element The VirtualNode instance whose value we want
* @return {string} The calculated value
*/
function nativeSelectValue(node) {
node = node.actualNode || node;
if (!axe.commons.forms.isNativeSelect(node)) {
return '';
}
return (
Array.from(node.options)
.filter(option => option.selected)
.map(option => option.text)
.join(' ') || ''
);
}
/**
* Calculate value of a an element with role=textbox
*
* @param {VirtualNode} element The VirtualNode instance whose value we want
* @return {string} The calculated value
*/
function ariaTextboxValue(virtualNode) {
const { actualNode } = virtualNode;
if (!axe.commons.forms.isAriaTextbox(actualNode)) {
return '';
}
if (!dom.isHiddenWithCSS(actualNode)) {
return text.visibleVirtual(virtualNode, true);
} else {
return actualNode.textContent;
}
}
/**
* Calculate value of an element with role=combobox or role=listbox
*
* @param {VirtualNode} element The VirtualNode instance whose value we want
* @param {Object} context The VirtualNode instance whose value we want
* @property {Element} startNode First node in accessible name computation
* @property {String[]} unsupported List of roles where value computation is unsupported
* @property {Bool} debug Enable logging for formControlValue
* @return {string} The calculated value
*/
function ariaListboxValue(virtualNode, context) {
const { actualNode } = virtualNode;
if (!axe.commons.forms.isAriaListbox(actualNode)) {
return '';
}
const selected = aria
.getOwnedVirtual(virtualNode)
.filter(
owned =>
aria.getRole(owned) === 'option' &&
owned.actualNode.getAttribute('aria-selected') === 'true'
);
if (selected.length === 0) {
return '';
}
// Note: Even with aria-multiselectable, only the first value will be used
// in the accessible name. This isn't spec'ed out, but seems to be how all
// browser behave.
return axe.commons.text.accessibleTextVirtual(selected[0], context);
}
/**
* Calculate value of an element with role=combobox or role=listbox
*
* @param {VirtualNode} element The VirtualNode instance whose value we want
* @param {Object} context The VirtualNode instance whose value we want
* @property {Element} startNode First node in accessible name computation
* @property {String[]} unsupported List of roles where value computation is unsupported
* @property {Bool} debug Enable logging for formControlValue
* @return {string} The calculated value
*/
function ariaComboboxValue(virtualNode, context) {
const { actualNode } = virtualNode;
let listbox;
// For combobox, find the first owned listbox:
if (!axe.commons.forms.isAriaCombobox(actualNode)) {
return '';
}
listbox = aria
.getOwnedVirtual(virtualNode)
.filter(elm => aria.getRole(elm) === 'listbox')[0];
return listbox
? text.formControlValueMethods.ariaListboxValue(listbox, context)
: '';
}
/**
* Calculate value of an element with range-type role
*
* @param {VirtualNode|Node} element The VirtualNode instance whose value we want
* @return {string} The calculated value
*/
function ariaRangeValue(node) {
node = node.actualNode || node;
if (
!axe.commons.forms.isAriaRange(node) ||
!node.hasAttribute('aria-valuenow')
) {
return '';
}
// Validate the number, if not, return 0.
// Chrome 70 typecasts this, Firefox 62 does not
const valueNow = +node.getAttribute('aria-valuenow');
return !isNaN(valueNow) ? String(valueNow) : '0';
}