387 lines
13 KiB
JavaScript
387 lines
13 KiB
JavaScript
// Copyright 2007 The Closure Library Authors. All Rights Reserved.
|
|
//
|
|
// 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.
|
|
|
|
|
|
/**
|
|
* @fileoverview Utilities for adding, removing and setting ARIA roles and
|
|
* states as defined by W3C ARIA standard: http://www.w3.org/TR/wai-aria/
|
|
* All modern browsers have some form of ARIA support, so no browser checks are
|
|
* performed when adding ARIA to components.
|
|
*
|
|
*/
|
|
|
|
goog.provide('goog.a11y.aria');
|
|
|
|
goog.require('goog.a11y.aria.Role');
|
|
goog.require('goog.a11y.aria.State');
|
|
goog.require('goog.a11y.aria.datatables');
|
|
goog.require('goog.array');
|
|
goog.require('goog.asserts');
|
|
goog.require('goog.dom');
|
|
goog.require('goog.dom.TagName');
|
|
goog.require('goog.object');
|
|
goog.require('goog.string');
|
|
|
|
|
|
/**
|
|
* ARIA states/properties prefix.
|
|
* @private
|
|
*/
|
|
goog.a11y.aria.ARIA_PREFIX_ = 'aria-';
|
|
|
|
|
|
/**
|
|
* ARIA role attribute.
|
|
* @private
|
|
*/
|
|
goog.a11y.aria.ROLE_ATTRIBUTE_ = 'role';
|
|
|
|
|
|
/**
|
|
* A list of tag names for which we don't need to set ARIA role and states
|
|
* because they have well supported semantics for screen readers or because
|
|
* they don't contain content to be made accessible.
|
|
* @private
|
|
*/
|
|
goog.a11y.aria.TAGS_WITH_ASSUMED_ROLES_ = [
|
|
goog.dom.TagName.A,
|
|
goog.dom.TagName.AREA,
|
|
goog.dom.TagName.BUTTON,
|
|
goog.dom.TagName.HEAD,
|
|
goog.dom.TagName.INPUT,
|
|
goog.dom.TagName.LINK,
|
|
goog.dom.TagName.MENU,
|
|
goog.dom.TagName.META,
|
|
goog.dom.TagName.OPTGROUP,
|
|
goog.dom.TagName.OPTION,
|
|
goog.dom.TagName.PROGRESS,
|
|
goog.dom.TagName.STYLE,
|
|
goog.dom.TagName.SELECT,
|
|
goog.dom.TagName.SOURCE,
|
|
goog.dom.TagName.TEXTAREA,
|
|
goog.dom.TagName.TITLE,
|
|
goog.dom.TagName.TRACK
|
|
];
|
|
|
|
|
|
/**
|
|
* Sets the role of an element. If the roleName is
|
|
* empty string or null, the role for the element is removed.
|
|
* We encourage clients to call the goog.a11y.aria.removeRole
|
|
* method instead of setting null and empty string values.
|
|
* Special handling for this case is added to ensure
|
|
* backword compatibility with existing code.
|
|
*
|
|
* @param {!Element} element DOM node to set role of.
|
|
* @param {!goog.a11y.aria.Role|string} roleName role name(s).
|
|
*/
|
|
goog.a11y.aria.setRole = function(element, roleName) {
|
|
if (!roleName) {
|
|
// Setting the ARIA role to empty string is not allowed
|
|
// by the ARIA standard.
|
|
goog.a11y.aria.removeRole(element);
|
|
} else {
|
|
if (goog.asserts.ENABLE_ASSERTS) {
|
|
goog.asserts.assert(goog.object.containsValue(
|
|
goog.a11y.aria.Role, roleName), 'No such ARIA role ' + roleName);
|
|
}
|
|
element.setAttribute(goog.a11y.aria.ROLE_ATTRIBUTE_, roleName);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Gets role of an element.
|
|
* @param {!Element} element DOM element to get role of.
|
|
* @return {goog.a11y.aria.Role} ARIA Role name.
|
|
*/
|
|
goog.a11y.aria.getRole = function(element) {
|
|
var role = element.getAttribute(goog.a11y.aria.ROLE_ATTRIBUTE_);
|
|
return /** @type {goog.a11y.aria.Role} */ (role) || null;
|
|
};
|
|
|
|
|
|
/**
|
|
* Removes role of an element.
|
|
* @param {!Element} element DOM element to remove the role from.
|
|
*/
|
|
goog.a11y.aria.removeRole = function(element) {
|
|
element.removeAttribute(goog.a11y.aria.ROLE_ATTRIBUTE_);
|
|
};
|
|
|
|
|
|
/**
|
|
* Sets the state or property of an element.
|
|
* @param {!Element} element DOM node where we set state.
|
|
* @param {!(goog.a11y.aria.State|string)} stateName State attribute being set.
|
|
* Automatically adds prefix 'aria-' to the state name if the attribute is
|
|
* not an extra attribute.
|
|
* @param {string|boolean|number|!Array<string>} value Value
|
|
* for the state attribute.
|
|
*/
|
|
goog.a11y.aria.setState = function(element, stateName, value) {
|
|
if (goog.isArray(value)) {
|
|
value = value.join(' ');
|
|
}
|
|
var attrStateName = goog.a11y.aria.getAriaAttributeName_(stateName);
|
|
if (value === '' || value == undefined) {
|
|
var defaultValueMap = goog.a11y.aria.datatables.getDefaultValuesMap();
|
|
// Work around for browsers that don't properly support ARIA.
|
|
// According to the ARIA W3C standard, user agents should allow
|
|
// setting empty value which results in setting the default value
|
|
// for the ARIA state if such exists. The exact text from the ARIA W3C
|
|
// standard (http://www.w3.org/TR/wai-aria/states_and_properties):
|
|
// "When a value is indicated as the default, the user agent
|
|
// MUST follow the behavior prescribed by this value when the state or
|
|
// property is empty or undefined."
|
|
// The defaultValueMap contains the default values for the ARIA states
|
|
// and has as a key the goog.a11y.aria.State constant for the state.
|
|
if (stateName in defaultValueMap) {
|
|
element.setAttribute(attrStateName, defaultValueMap[stateName]);
|
|
} else {
|
|
element.removeAttribute(attrStateName);
|
|
}
|
|
} else {
|
|
element.setAttribute(attrStateName, value);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Toggles the ARIA attribute of an element.
|
|
* Meant for attributes with a true/false value, but works with any attribute.
|
|
* If the attribute does not have a true/false value, the following rules apply:
|
|
* A not empty attribute will be removed.
|
|
* An empty attribute will be set to true.
|
|
* @param {!Element} el DOM node for which to set attribute.
|
|
* @param {!(goog.a11y.aria.State|string)} attr ARIA attribute being set.
|
|
* Automatically adds prefix 'aria-' to the attribute name if the attribute
|
|
* is not an extra attribute.
|
|
*/
|
|
goog.a11y.aria.toggleState = function(el, attr) {
|
|
var val = goog.a11y.aria.getState(el, attr);
|
|
if (!goog.string.isEmptyOrWhitespace(goog.string.makeSafe(val)) &&
|
|
!(val == 'true' || val == 'false')) {
|
|
goog.a11y.aria.removeState(el, /** @type {!goog.a11y.aria.State} */ (attr));
|
|
return;
|
|
}
|
|
goog.a11y.aria.setState(el, attr, val == 'true' ? 'false' : 'true');
|
|
};
|
|
|
|
|
|
/**
|
|
* Remove the state or property for the element.
|
|
* @param {!Element} element DOM node where we set state.
|
|
* @param {!goog.a11y.aria.State} stateName State name.
|
|
*/
|
|
goog.a11y.aria.removeState = function(element, stateName) {
|
|
element.removeAttribute(goog.a11y.aria.getAriaAttributeName_(stateName));
|
|
};
|
|
|
|
|
|
/**
|
|
* Gets value of specified state or property.
|
|
* @param {!Element} element DOM node to get state from.
|
|
* @param {!goog.a11y.aria.State|string} stateName State name.
|
|
* @return {string} Value of the state attribute.
|
|
*/
|
|
goog.a11y.aria.getState = function(element, stateName) {
|
|
// TODO(user): return properly typed value result --
|
|
// boolean, number, string, null. We should be able to chain
|
|
// getState(...) and setState(...) methods.
|
|
|
|
var attr =
|
|
/** @type {string|number|boolean} */ (element.getAttribute(
|
|
goog.a11y.aria.getAriaAttributeName_(stateName)));
|
|
var isNullOrUndefined = attr == null || attr == undefined;
|
|
return isNullOrUndefined ? '' : String(attr);
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns the activedescendant element for the input element by
|
|
* using the activedescendant ARIA property of the given element.
|
|
* @param {!Element} element DOM node to get activedescendant
|
|
* element for.
|
|
* @return {?Element} DOM node of the activedescendant, if found.
|
|
*/
|
|
goog.a11y.aria.getActiveDescendant = function(element) {
|
|
var id = goog.a11y.aria.getState(
|
|
element, goog.a11y.aria.State.ACTIVEDESCENDANT);
|
|
return goog.dom.getOwnerDocument(element).getElementById(id);
|
|
};
|
|
|
|
|
|
/**
|
|
* Sets the activedescendant ARIA property value for an element.
|
|
* If the activeElement is not null, it should have an id set.
|
|
* @param {!Element} element DOM node to set activedescendant ARIA property to.
|
|
* @param {?Element} activeElement DOM node being set as activedescendant.
|
|
*/
|
|
goog.a11y.aria.setActiveDescendant = function(element, activeElement) {
|
|
var id = '';
|
|
if (activeElement) {
|
|
id = activeElement.id;
|
|
goog.asserts.assert(id, 'The active element should have an id.');
|
|
}
|
|
|
|
goog.a11y.aria.setState(element, goog.a11y.aria.State.ACTIVEDESCENDANT, id);
|
|
};
|
|
|
|
|
|
/**
|
|
* Gets the label of the given element.
|
|
* @param {!Element} element DOM node to get label from.
|
|
* @return {string} label The label.
|
|
*/
|
|
goog.a11y.aria.getLabel = function(element) {
|
|
return goog.a11y.aria.getState(element, goog.a11y.aria.State.LABEL);
|
|
};
|
|
|
|
|
|
/**
|
|
* Sets the label of the given element.
|
|
* @param {!Element} element DOM node to set label to.
|
|
* @param {string} label The label to set.
|
|
*/
|
|
goog.a11y.aria.setLabel = function(element, label) {
|
|
goog.a11y.aria.setState(element, goog.a11y.aria.State.LABEL, label);
|
|
};
|
|
|
|
|
|
/**
|
|
* Asserts that the element has a role set if it's not an HTML element whose
|
|
* semantics is well supported by most screen readers.
|
|
* Only to be used internally by the ARIA library in goog.a11y.aria.*.
|
|
* @param {!Element} element The element to assert an ARIA role set.
|
|
* @param {!goog.array.ArrayLike<string>} allowedRoles The child roles of
|
|
* the roles.
|
|
*/
|
|
goog.a11y.aria.assertRoleIsSetInternalUtil = function(element, allowedRoles) {
|
|
if (goog.array.contains(goog.a11y.aria.TAGS_WITH_ASSUMED_ROLES_,
|
|
element.tagName)) {
|
|
return;
|
|
}
|
|
var elementRole = /** @type {string}*/ (goog.a11y.aria.getRole(element));
|
|
goog.asserts.assert(elementRole != null,
|
|
'The element ARIA role cannot be null.');
|
|
|
|
goog.asserts.assert(goog.array.contains(allowedRoles, elementRole),
|
|
'Non existing or incorrect role set for element.' +
|
|
'The role set is "' + elementRole +
|
|
'". The role should be any of "' + allowedRoles +
|
|
'". Check the ARIA specification for more details ' +
|
|
'http://www.w3.org/TR/wai-aria/roles.');
|
|
};
|
|
|
|
|
|
/**
|
|
* Gets the boolean value of an ARIA state/property.
|
|
* @param {!Element} element The element to get the ARIA state for.
|
|
* @param {!goog.a11y.aria.State|string} stateName the ARIA state name.
|
|
* @return {?boolean} Boolean value for the ARIA state value or null if
|
|
* the state value is not 'true', not 'false', or not set.
|
|
*/
|
|
goog.a11y.aria.getStateBoolean = function(element, stateName) {
|
|
var attr =
|
|
/** @type {string|boolean} */ (element.getAttribute(
|
|
goog.a11y.aria.getAriaAttributeName_(stateName)));
|
|
goog.asserts.assert(
|
|
goog.isBoolean(attr) || attr == null || attr == 'true' ||
|
|
attr == 'false');
|
|
if (attr == null) {
|
|
return attr;
|
|
}
|
|
return goog.isBoolean(attr) ? attr : attr == 'true';
|
|
};
|
|
|
|
|
|
/**
|
|
* Gets the number value of an ARIA state/property.
|
|
* @param {!Element} element The element to get the ARIA state for.
|
|
* @param {!goog.a11y.aria.State|string} stateName the ARIA state name.
|
|
* @return {?number} Number value for the ARIA state value or null if
|
|
* the state value is not a number or not set.
|
|
*/
|
|
goog.a11y.aria.getStateNumber = function(element, stateName) {
|
|
var attr =
|
|
/** @type {string|number} */ (element.getAttribute(
|
|
goog.a11y.aria.getAriaAttributeName_(stateName)));
|
|
goog.asserts.assert((attr == null || !isNaN(Number(attr))) &&
|
|
!goog.isBoolean(attr));
|
|
return attr == null ? null : Number(attr);
|
|
};
|
|
|
|
|
|
/**
|
|
* Gets the string value of an ARIA state/property.
|
|
* @param {!Element} element The element to get the ARIA state for.
|
|
* @param {!goog.a11y.aria.State|string} stateName the ARIA state name.
|
|
* @return {?string} String value for the ARIA state value or null if
|
|
* the state value is empty string or not set.
|
|
*/
|
|
goog.a11y.aria.getStateString = function(element, stateName) {
|
|
var attr = element.getAttribute(
|
|
goog.a11y.aria.getAriaAttributeName_(stateName));
|
|
goog.asserts.assert((attr == null || goog.isString(attr)) &&
|
|
isNaN(Number(attr)) && attr != 'true' && attr != 'false');
|
|
return attr == null ? null : attr;
|
|
};
|
|
|
|
|
|
/**
|
|
* Gets array of strings value of the specified state or
|
|
* property for the element.
|
|
* Only to be used internally by the ARIA library in goog.a11y.aria.*.
|
|
* @param {!Element} element DOM node to get state from.
|
|
* @param {!goog.a11y.aria.State} stateName State name.
|
|
* @return {!goog.array.ArrayLike<string>} string Array
|
|
* value of the state attribute.
|
|
*/
|
|
goog.a11y.aria.getStringArrayStateInternalUtil = function(element, stateName) {
|
|
var attrValue = element.getAttribute(
|
|
goog.a11y.aria.getAriaAttributeName_(stateName));
|
|
return goog.a11y.aria.splitStringOnWhitespace_(attrValue);
|
|
};
|
|
|
|
|
|
/**
|
|
* Splits the input stringValue on whitespace.
|
|
* @param {string} stringValue The value of the string to split.
|
|
* @return {!goog.array.ArrayLike<string>} string Array
|
|
* value as result of the split.
|
|
* @private
|
|
*/
|
|
goog.a11y.aria.splitStringOnWhitespace_ = function(stringValue) {
|
|
return stringValue ? stringValue.split(/\s+/) : [];
|
|
};
|
|
|
|
|
|
/**
|
|
* Adds the 'aria-' prefix to ariaName.
|
|
* @param {string} ariaName ARIA state/property name.
|
|
* @private
|
|
* @return {string} The ARIA attribute name with added 'aria-' prefix.
|
|
* @throws {Error} If no such attribute exists.
|
|
*/
|
|
goog.a11y.aria.getAriaAttributeName_ = function(ariaName) {
|
|
if (goog.asserts.ENABLE_ASSERTS) {
|
|
goog.asserts.assert(ariaName, 'ARIA attribute cannot be empty.');
|
|
goog.asserts.assert(goog.object.containsValue(
|
|
goog.a11y.aria.State, ariaName),
|
|
'No such ARIA attribute ' + ariaName);
|
|
}
|
|
return goog.a11y.aria.ARIA_PREFIX_ + ariaName;
|
|
};
|