Path: blob/trunk/third_party/closure/goog/a11y/aria/aria.js
1865 views
// Copyright 2007 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.131415/**16* @fileoverview Utilities for adding, removing and setting ARIA roles and17* states as defined by W3C ARIA standard: http://www.w3.org/TR/wai-aria/18* All modern browsers have some form of ARIA support, so no browser checks are19* performed when adding ARIA to components.20*21*/2223goog.provide('goog.a11y.aria');2425goog.require('goog.a11y.aria.Role');26goog.require('goog.a11y.aria.State');27goog.require('goog.a11y.aria.datatables');28goog.require('goog.array');29goog.require('goog.asserts');30goog.require('goog.dom');31goog.require('goog.dom.TagName');32goog.require('goog.object');33goog.require('goog.string');343536/**37* ARIA states/properties prefix.38* @private39*/40goog.a11y.aria.ARIA_PREFIX_ = 'aria-';414243/**44* ARIA role attribute.45* @private46*/47goog.a11y.aria.ROLE_ATTRIBUTE_ = 'role';484950/**51* A list of tag names for which we don't need to set ARIA role and states52* because they have well supported semantics for screen readers or because53* they don't contain content to be made accessible.54* @private55*/56goog.a11y.aria.TAGS_WITH_ASSUMED_ROLES_ = goog.object.createSet([57goog.dom.TagName.A, goog.dom.TagName.AREA, goog.dom.TagName.BUTTON,58goog.dom.TagName.HEAD, goog.dom.TagName.INPUT, goog.dom.TagName.LINK,59goog.dom.TagName.MENU, goog.dom.TagName.META, goog.dom.TagName.OPTGROUP,60goog.dom.TagName.OPTION, goog.dom.TagName.PROGRESS, goog.dom.TagName.STYLE,61goog.dom.TagName.SELECT, goog.dom.TagName.SOURCE, goog.dom.TagName.TEXTAREA,62goog.dom.TagName.TITLE, goog.dom.TagName.TRACK63]);646566/**67* A list of roles which are considered container roles.68* Container roles are ARIA roles which use the aria-activedescendant property69* to manage their active descendants or children. See70* {@link http://www.w3.org/TR/wai-aria/states_and_properties71* #aria-activedescendant} for more information.72* @private @const {!Array<goog.a11y.aria.Role>}73*/74goog.a11y.aria.CONTAINER_ROLES_ = [75goog.a11y.aria.Role.COMBOBOX, goog.a11y.aria.Role.GRID,76goog.a11y.aria.Role.GROUP, goog.a11y.aria.Role.LISTBOX,77goog.a11y.aria.Role.MENU, goog.a11y.aria.Role.MENUBAR,78goog.a11y.aria.Role.RADIOGROUP, goog.a11y.aria.Role.ROW,79goog.a11y.aria.Role.ROWGROUP, goog.a11y.aria.Role.TAB_LIST,80goog.a11y.aria.Role.TEXTBOX, goog.a11y.aria.Role.TOOLBAR,81goog.a11y.aria.Role.TREE, goog.a11y.aria.Role.TREEGRID82];838485/**86* Sets the role of an element. If the roleName is87* empty string or null, the role for the element is removed.88* We encourage clients to call the goog.a11y.aria.removeRole89* method instead of setting null and empty string values.90* Special handling for this case is added to ensure91* backword compatibility with existing code.92*93* @param {!Element} element DOM node to set role of.94* @param {!goog.a11y.aria.Role|string} roleName role name(s).95*/96goog.a11y.aria.setRole = function(element, roleName) {97if (!roleName) {98// Setting the ARIA role to empty string is not allowed99// by the ARIA standard.100goog.a11y.aria.removeRole(element);101} else {102if (goog.asserts.ENABLE_ASSERTS) {103goog.asserts.assert(104goog.object.containsValue(goog.a11y.aria.Role, roleName),105'No such ARIA role ' + roleName);106}107element.setAttribute(goog.a11y.aria.ROLE_ATTRIBUTE_, roleName);108}109};110111112/**113* Gets role of an element.114* @param {!Element} element DOM element to get role of.115* @return {?goog.a11y.aria.Role} ARIA Role name.116*/117goog.a11y.aria.getRole = function(element) {118var role = element.getAttribute(goog.a11y.aria.ROLE_ATTRIBUTE_);119return /** @type {goog.a11y.aria.Role} */ (role) || null;120};121122123/**124* Removes role of an element.125* @param {!Element} element DOM element to remove the role from.126*/127goog.a11y.aria.removeRole = function(element) {128element.removeAttribute(goog.a11y.aria.ROLE_ATTRIBUTE_);129};130131132/**133* Sets the state or property of an element.134* @param {!Element} element DOM node where we set state.135* @param {!(goog.a11y.aria.State|string)} stateName State attribute being set.136* Automatically adds prefix 'aria-' to the state name if the attribute is137* not an extra attribute.138* @param {string|boolean|number|!Array<string>} value Value139* for the state attribute.140*/141goog.a11y.aria.setState = function(element, stateName, value) {142if (goog.isArray(value)) {143value = value.join(' ');144}145var attrStateName = goog.a11y.aria.getAriaAttributeName_(stateName);146if (value === '' || value == undefined) {147var defaultValueMap = goog.a11y.aria.datatables.getDefaultValuesMap();148// Work around for browsers that don't properly support ARIA.149// According to the ARIA W3C standard, user agents should allow150// setting empty value which results in setting the default value151// for the ARIA state if such exists. The exact text from the ARIA W3C152// standard (http://www.w3.org/TR/wai-aria/states_and_properties):153// "When a value is indicated as the default, the user agent154// MUST follow the behavior prescribed by this value when the state or155// property is empty or undefined."156// The defaultValueMap contains the default values for the ARIA states157// and has as a key the goog.a11y.aria.State constant for the state.158if (stateName in defaultValueMap) {159element.setAttribute(attrStateName, defaultValueMap[stateName]);160} else {161element.removeAttribute(attrStateName);162}163} else {164element.setAttribute(attrStateName, value);165}166};167168169/**170* Toggles the ARIA attribute of an element.171* Meant for attributes with a true/false value, but works with any attribute.172* If the attribute does not have a true/false value, the following rules apply:173* A not empty attribute will be removed.174* An empty attribute will be set to true.175* @param {!Element} el DOM node for which to set attribute.176* @param {!(goog.a11y.aria.State|string)} attr ARIA attribute being set.177* Automatically adds prefix 'aria-' to the attribute name if the attribute178* is not an extra attribute.179*/180goog.a11y.aria.toggleState = function(el, attr) {181var val = goog.a11y.aria.getState(el, attr);182if (!goog.string.isEmptyOrWhitespace(goog.string.makeSafe(val)) &&183!(val == 'true' || val == 'false')) {184goog.a11y.aria.removeState(el, /** @type {!goog.a11y.aria.State} */ (attr));185return;186}187goog.a11y.aria.setState(el, attr, val == 'true' ? 'false' : 'true');188};189190191/**192* Remove the state or property for the element.193* @param {!Element} element DOM node where we set state.194* @param {!goog.a11y.aria.State} stateName State name.195*/196goog.a11y.aria.removeState = function(element, stateName) {197element.removeAttribute(goog.a11y.aria.getAriaAttributeName_(stateName));198};199200201/**202* Gets value of specified state or property.203* @param {!Element} element DOM node to get state from.204* @param {!goog.a11y.aria.State|string} stateName State name.205* @return {string} Value of the state attribute.206*/207goog.a11y.aria.getState = function(element, stateName) {208// TODO(user): return properly typed value result --209// boolean, number, string, null. We should be able to chain210// getState(...) and setState(...) methods.211212var attr =213/** @type {string|number|boolean} */ (214element.getAttribute(215goog.a11y.aria.getAriaAttributeName_(stateName)));216var isNullOrUndefined = attr == null || attr == undefined;217return isNullOrUndefined ? '' : String(attr);218};219220221/**222* Returns the activedescendant element for the input element by223* using the activedescendant ARIA property of the given element.224* @param {!Element} element DOM node to get activedescendant225* element for.226* @return {?Element} DOM node of the activedescendant, if found.227*/228goog.a11y.aria.getActiveDescendant = function(element) {229var id =230goog.a11y.aria.getState(element, goog.a11y.aria.State.ACTIVEDESCENDANT);231return goog.dom.getOwnerDocument(element).getElementById(id);232};233234235/**236* Sets the activedescendant ARIA property value for an element.237* If the activeElement is not null, it should have an id set.238* @param {!Element} element DOM node to set activedescendant ARIA property to.239* @param {?Element} activeElement DOM node being set as activedescendant.240*/241goog.a11y.aria.setActiveDescendant = function(element, activeElement) {242var id = '';243if (activeElement) {244id = activeElement.id;245goog.asserts.assert(id, 'The active element should have an id.');246}247248goog.a11y.aria.setState(element, goog.a11y.aria.State.ACTIVEDESCENDANT, id);249};250251252/**253* Gets the label of the given element.254* @param {!Element} element DOM node to get label from.255* @return {string} label The label.256*/257goog.a11y.aria.getLabel = function(element) {258return goog.a11y.aria.getState(element, goog.a11y.aria.State.LABEL);259};260261262/**263* Sets the label of the given element.264* @param {!Element} element DOM node to set label to.265* @param {string} label The label to set.266*/267goog.a11y.aria.setLabel = function(element, label) {268goog.a11y.aria.setState(element, goog.a11y.aria.State.LABEL, label);269};270271272/**273* Asserts that the element has a role set if it's not an HTML element whose274* semantics is well supported by most screen readers.275* Only to be used internally by the ARIA library in goog.a11y.aria.*.276* @param {!Element} element The element to assert an ARIA role set.277* @param {!IArrayLike<string>} allowedRoles The child roles of278* the roles.279*/280goog.a11y.aria.assertRoleIsSetInternalUtil = function(element, allowedRoles) {281if (goog.a11y.aria.TAGS_WITH_ASSUMED_ROLES_[element.tagName]) {282return;283}284var elementRole = /** @type {string}*/ (goog.a11y.aria.getRole(element));285goog.asserts.assert(286elementRole != null, 'The element ARIA role cannot be null.');287288goog.asserts.assert(289goog.array.contains(allowedRoles, elementRole),290'Non existing or incorrect role set for element.' +291'The role set is "' + elementRole + '". The role should be any of "' +292allowedRoles + '". Check the ARIA specification for more details ' +293'http://www.w3.org/TR/wai-aria/roles.');294};295296297/**298* Gets the boolean value of an ARIA state/property.299* @param {!Element} element The element to get the ARIA state for.300* @param {!goog.a11y.aria.State|string} stateName the ARIA state name.301* @return {?boolean} Boolean value for the ARIA state value or null if302* the state value is not 'true', not 'false', or not set.303*/304goog.a11y.aria.getStateBoolean = function(element, stateName) {305var attr =306/** @type {string|boolean} */ (307element.getAttribute(308goog.a11y.aria.getAriaAttributeName_(stateName)));309goog.asserts.assert(310goog.isBoolean(attr) || attr == null || attr == 'true' ||311attr == 'false');312if (attr == null) {313return attr;314}315return goog.isBoolean(attr) ? attr : attr == 'true';316};317318319/**320* Gets the number value of an ARIA state/property.321* @param {!Element} element The element to get the ARIA state for.322* @param {!goog.a11y.aria.State|string} stateName the ARIA state name.323* @return {?number} Number value for the ARIA state value or null if324* the state value is not a number or not set.325*/326goog.a11y.aria.getStateNumber = function(element, stateName) {327var attr =328/** @type {string|number} */ (329element.getAttribute(330goog.a11y.aria.getAriaAttributeName_(stateName)));331goog.asserts.assert(332(attr == null || !isNaN(Number(attr))) && !goog.isBoolean(attr));333return attr == null ? null : Number(attr);334};335336337/**338* Gets the string value of an ARIA state/property.339* @param {!Element} element The element to get the ARIA state for.340* @param {!goog.a11y.aria.State|string} stateName the ARIA state name.341* @return {?string} String value for the ARIA state value or null if342* the state value is empty string or not set.343*/344goog.a11y.aria.getStateString = function(element, stateName) {345var attr =346element.getAttribute(goog.a11y.aria.getAriaAttributeName_(stateName));347goog.asserts.assert(348(attr == null || goog.isString(attr)) &&349(attr == '' || isNaN(Number(attr))) && attr != 'true' && attr != 'false');350return (attr == null || attr == '') ? null : attr;351};352353354/**355* Gets array of strings value of the specified state or356* property for the element.357* Only to be used internally by the ARIA library in goog.a11y.aria.*.358* @param {!Element} element DOM node to get state from.359* @param {!goog.a11y.aria.State} stateName State name.360* @return {!IArrayLike<string>} string Array361* value of the state attribute.362*/363goog.a11y.aria.getStringArrayStateInternalUtil = function(element, stateName) {364var attrValue =365element.getAttribute(goog.a11y.aria.getAriaAttributeName_(stateName));366return goog.a11y.aria.splitStringOnWhitespace_(attrValue);367};368369370/**371* Returns true if element has an ARIA state/property, false otherwise.372* @param {!Element} element The element to get the ARIA state for.373* @param {!goog.a11y.aria.State|string} stateName the ARIA state name.374* @return {boolean}375*/376goog.a11y.aria.hasState = function(element, stateName) {377return element.hasAttribute(goog.a11y.aria.getAriaAttributeName_(stateName));378};379380381/**382* Returns whether the element has a container ARIA role.383* Container roles are ARIA roles that use the aria-activedescendant property384* to manage their active descendants or children. See385* {@link http://www.w3.org/TR/wai-aria/states_and_properties386* #aria-activedescendant} for more information.387* @param {!Element} element388* @return {boolean}389*/390goog.a11y.aria.isContainerRole = function(element) {391var role = goog.a11y.aria.getRole(element);392return goog.array.contains(goog.a11y.aria.CONTAINER_ROLES_, role);393};394395396/**397* Splits the input stringValue on whitespace.398* @param {string} stringValue The value of the string to split.399* @return {!IArrayLike<string>} string Array400* value as result of the split.401* @private402*/403goog.a11y.aria.splitStringOnWhitespace_ = function(stringValue) {404return stringValue ? stringValue.split(/\s+/) : [];405};406407408/**409* Adds the 'aria-' prefix to ariaName.410* @param {string} ariaName ARIA state/property name.411* @private412* @return {string} The ARIA attribute name with added 'aria-' prefix.413* @throws {Error} If no such attribute exists.414*/415goog.a11y.aria.getAriaAttributeName_ = function(ariaName) {416if (goog.asserts.ENABLE_ASSERTS) {417goog.asserts.assert(ariaName, 'ARIA attribute cannot be empty.');418goog.asserts.assert(419goog.object.containsValue(goog.a11y.aria.State, ariaName),420'No such ARIA attribute ' + ariaName);421}422return goog.a11y.aria.ARIA_PREFIX_ + ariaName;423};424425426