// 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.object.createSet([ 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 ]); /** * A list of roles which are considered container roles. * Container roles are ARIA roles which use the aria-activedescendant property * to manage their active descendants or children. See * {@link http://www.w3.org/TR/wai-aria/states_and_properties * #aria-activedescendant} for more information. * @private @const {!Array} */ goog.a11y.aria.CONTAINER_ROLES_ = [ goog.a11y.aria.Role.COMBOBOX, goog.a11y.aria.Role.GRID, goog.a11y.aria.Role.GROUP, goog.a11y.aria.Role.LISTBOX, goog.a11y.aria.Role.MENU, goog.a11y.aria.Role.MENUBAR, goog.a11y.aria.Role.RADIOGROUP, goog.a11y.aria.Role.ROW, goog.a11y.aria.Role.ROWGROUP, goog.a11y.aria.Role.TAB_LIST, goog.a11y.aria.Role.TEXTBOX, goog.a11y.aria.Role.TOOLBAR, goog.a11y.aria.Role.TREE, goog.a11y.aria.Role.TREEGRID ]; /** * 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} 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 {!IArrayLike} allowedRoles The child roles of * the roles. */ goog.a11y.aria.assertRoleIsSetInternalUtil = function(element, allowedRoles) { if (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)) && (attr == '' || isNaN(Number(attr))) && attr != 'true' && attr != 'false'); return (attr == null || attr == '') ? 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 {!IArrayLike} 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); }; /** * Returns true if element has an ARIA state/property, false otherwise. * @param {!Element} element The element to get the ARIA state for. * @param {!goog.a11y.aria.State|string} stateName the ARIA state name. * @return {boolean} */ goog.a11y.aria.hasState = function(element, stateName) { return element.hasAttribute(goog.a11y.aria.getAriaAttributeName_(stateName)); }; /** * Returns whether the element has a container ARIA role. * Container roles are ARIA roles that use the aria-activedescendant property * to manage their active descendants or children. See * {@link http://www.w3.org/TR/wai-aria/states_and_properties * #aria-activedescendant} for more information. * @param {!Element} element * @return {boolean} */ goog.a11y.aria.isContainerRole = function(element) { var role = goog.a11y.aria.getRole(element); return goog.array.contains(goog.a11y.aria.CONTAINER_ROLES_, role); }; /** * Splits the input stringValue on whitespace. * @param {string} stringValue The value of the string to split. * @return {!IArrayLike} 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; };