Path: blob/trunk/third_party/closure/goog/a11y/aria/aria.js
4386 views
/**1* @license2* Copyright The Closure Library Authors.3* SPDX-License-Identifier: Apache-2.04*/567/**8* @fileoverview Utilities for adding, removing and setting ARIA roles and9* states as defined by W3C ARIA standard: http://www.w3.org/TR/wai-aria/10* All modern browsers have some form of ARIA support, so no browser checks are11* performed when adding ARIA to components.12*/1314goog.provide('goog.a11y.aria');1516goog.require('goog.a11y.aria.Role');17goog.require('goog.a11y.aria.State');18goog.require('goog.a11y.aria.datatables');19goog.require('goog.array');20goog.require('goog.asserts');21goog.require('goog.dom');22goog.require('goog.dom.TagName');23goog.require('goog.object');24goog.require('goog.string');252627/**28* ARIA states/properties prefix.29* @private30*/31goog.a11y.aria.ARIA_PREFIX_ = 'aria-';323334/**35* ARIA role attribute.36* @private37*/38goog.a11y.aria.ROLE_ATTRIBUTE_ = 'role';394041/**42* A list of tag names for which we don't need to set ARIA role and states43* because they have well supported semantics for screen readers or because44* they don't contain content to be made accessible.45* @private46*/47goog.a11y.aria.TAGS_WITH_ASSUMED_ROLES_ = goog.object.createSet([48goog.dom.TagName.A, goog.dom.TagName.AREA, goog.dom.TagName.BUTTON,49goog.dom.TagName.HEAD, goog.dom.TagName.INPUT, goog.dom.TagName.LINK,50goog.dom.TagName.MENU, goog.dom.TagName.META, goog.dom.TagName.OPTGROUP,51goog.dom.TagName.OPTION, goog.dom.TagName.PROGRESS, goog.dom.TagName.STYLE,52goog.dom.TagName.SELECT, goog.dom.TagName.SOURCE, goog.dom.TagName.TEXTAREA,53goog.dom.TagName.TITLE, goog.dom.TagName.TRACK54]);555657/**58* A list of roles which are considered container roles.59* Container roles are ARIA roles which use the aria-activedescendant property60* to manage their active descendants or children. See61* {@link http://www.w3.org/TR/wai-aria/states_and_properties62* #aria-activedescendant} for more information.63* @private @const {!Array<goog.a11y.aria.Role>}64*/65goog.a11y.aria.CONTAINER_ROLES_ = [66goog.a11y.aria.Role.COMBOBOX, goog.a11y.aria.Role.GRID,67goog.a11y.aria.Role.GROUP, goog.a11y.aria.Role.LISTBOX,68goog.a11y.aria.Role.MENU, goog.a11y.aria.Role.MENUBAR,69goog.a11y.aria.Role.RADIOGROUP, goog.a11y.aria.Role.ROW,70goog.a11y.aria.Role.ROWGROUP, goog.a11y.aria.Role.TAB_LIST,71goog.a11y.aria.Role.TEXTBOX, goog.a11y.aria.Role.TOOLBAR,72goog.a11y.aria.Role.TREE, goog.a11y.aria.Role.TREEGRID73];747576/**77* Sets the role of an element. If the roleName is78* empty string or null, the role for the element is removed.79* We encourage clients to call the goog.a11y.aria.removeRole80* method instead of setting null and empty string values.81* Special handling for this case is added to ensure82* backword compatibility with existing code.83*84* @param {!Element} element DOM node to set role of.85* @param {!goog.a11y.aria.Role|string} roleName role name(s).86*/87goog.a11y.aria.setRole = function(element, roleName) {88'use strict';89if (!roleName) {90// Setting the ARIA role to empty string is not allowed91// by the ARIA standard.92goog.a11y.aria.removeRole(element);93} else {94if (goog.asserts.ENABLE_ASSERTS) {95goog.asserts.assert(96goog.object.containsValue(goog.a11y.aria.Role, roleName),97'No such ARIA role ' + roleName);98}99element.setAttribute(goog.a11y.aria.ROLE_ATTRIBUTE_, roleName);100}101};102103104/**105* Gets role of an element.106* @param {!Element} element DOM element to get role of.107* @return {?goog.a11y.aria.Role} ARIA Role name.108*/109goog.a11y.aria.getRole = function(element) {110'use strict';111var role = element.getAttribute(goog.a11y.aria.ROLE_ATTRIBUTE_);112return /** @type {goog.a11y.aria.Role} */ (role) || null;113};114115116/**117* Removes role of an element.118* @param {!Element} element DOM element to remove the role from.119*/120goog.a11y.aria.removeRole = function(element) {121'use strict';122element.removeAttribute(goog.a11y.aria.ROLE_ATTRIBUTE_);123};124125126/**127* Sets the state or property of an element.128* @param {!Element} element DOM node where we set state.129* @param {!(goog.a11y.aria.State|string)} stateName State attribute being set.130* Automatically adds prefix 'aria-' to the state name if the attribute is131* not an extra attribute.132* @param {string|boolean|number|!Array<string>} value Value133* for the state attribute.134*/135goog.a11y.aria.setState = function(element, stateName, value) {136'use strict';137if (Array.isArray(value)) {138value = value.join(' ');139}140var attrStateName = goog.a11y.aria.getAriaAttributeName_(stateName);141if (value === '' || value == undefined) {142var defaultValueMap = goog.a11y.aria.datatables.getDefaultValuesMap();143// Work around for browsers that don't properly support ARIA.144// According to the ARIA W3C standard, user agents should allow145// setting empty value which results in setting the default value146// for the ARIA state if such exists. The exact text from the ARIA W3C147// standard (http://www.w3.org/TR/wai-aria/states_and_properties):148// "When a value is indicated as the default, the user agent149// MUST follow the behavior prescribed by this value when the state or150// property is empty or undefined."151// The defaultValueMap contains the default values for the ARIA states152// and has as a key the goog.a11y.aria.State constant for the state.153if (stateName in defaultValueMap) {154element.setAttribute(attrStateName, defaultValueMap[stateName]);155} else {156element.removeAttribute(attrStateName);157}158} else {159element.setAttribute(attrStateName, value);160}161};162163164/**165* Toggles the ARIA attribute of an element.166* Meant for attributes with a true/false value, but works with any attribute.167* If the attribute does not have a true/false value, the following rules apply:168* A not empty attribute will be removed.169* An empty attribute will be set to true.170* @param {!Element} el DOM node for which to set attribute.171* @param {!(goog.a11y.aria.State|string)} attr ARIA attribute being set.172* Automatically adds prefix 'aria-' to the attribute name if the attribute173* is not an extra attribute.174*/175goog.a11y.aria.toggleState = function(el, attr) {176'use strict';177var val = goog.a11y.aria.getState(el, attr);178if (!goog.string.isEmptyOrWhitespace(goog.string.makeSafe(val)) &&179!(val == 'true' || val == 'false')) {180goog.a11y.aria.removeState(el, /** @type {!goog.a11y.aria.State} */ (attr));181return;182}183goog.a11y.aria.setState(el, attr, val == 'true' ? 'false' : 'true');184};185186187/**188* Remove the state or property for the element.189* @param {!Element} element DOM node where we set state.190* @param {!goog.a11y.aria.State} stateName State name.191*/192goog.a11y.aria.removeState = function(element, stateName) {193'use strict';194element.removeAttribute(goog.a11y.aria.getAriaAttributeName_(stateName));195};196197198/**199* Gets value of specified state or property.200* @param {!Element} element DOM node to get state from.201* @param {!goog.a11y.aria.State|string} stateName State name.202* @return {string} Value of the state attribute.203*/204goog.a11y.aria.getState = function(element, stateName) {205'use strict';206// TODO(user): return properly typed value result --207// boolean, number, string, null. We should be able to chain208// getState(...) and setState(...) methods.209210var attr =211/** @type {string|number|boolean} */ (212element.getAttribute(213goog.a11y.aria.getAriaAttributeName_(stateName)));214var isNullOrUndefined = attr == null || attr == undefined;215return isNullOrUndefined ? '' : String(attr);216};217218219/**220* Returns the activedescendant element for the input element by221* using the activedescendant ARIA property of the given element.222* @param {!Element} element DOM node to get activedescendant223* element for.224* @return {?Element} DOM node of the activedescendant, if found.225*/226goog.a11y.aria.getActiveDescendant = function(element) {227'use strict';228var id =229goog.a11y.aria.getState(element, goog.a11y.aria.State.ACTIVEDESCENDANT);230return goog.dom.getOwnerDocument(element).getElementById(id);231};232233234/**235* Sets the activedescendant ARIA property value for an element.236* If the activeElement is not null, it should have an id set.237* @param {!Element} element DOM node to set activedescendant ARIA property to.238* @param {?Element} activeElement DOM node being set as activedescendant.239*/240goog.a11y.aria.setActiveDescendant = function(element, activeElement) {241'use strict';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) {258'use strict';259return goog.a11y.aria.getState(element, goog.a11y.aria.State.LABEL);260};261262263/**264* Sets the label of the given element.265* @param {!Element} element DOM node to set label to.266* @param {string} label The label to set.267*/268goog.a11y.aria.setLabel = function(element, label) {269'use strict';270goog.a11y.aria.setState(element, goog.a11y.aria.State.LABEL, label);271};272273274/**275* Asserts that the element has a role set if it's not an HTML element whose276* semantics is well supported by most screen readers.277* Only to be used internally by the ARIA library in goog.a11y.aria.*.278* @param {!Element} element The element to assert an ARIA role set.279* @param {!IArrayLike<string>} allowedRoles The child roles of280* the roles.281*/282goog.a11y.aria.assertRoleIsSetInternalUtil = function(element, allowedRoles) {283'use strict';284if (goog.a11y.aria.TAGS_WITH_ASSUMED_ROLES_[element.tagName]) {285return;286}287var elementRole = /** @type {string}*/ (goog.a11y.aria.getRole(element));288goog.asserts.assert(289elementRole != null, 'The element ARIA role cannot be null.');290291goog.asserts.assert(292goog.array.contains(allowedRoles, elementRole),293'Non existing or incorrect role set for element.' +294'The role set is "' + elementRole + '". The role should be any of "' +295allowedRoles + '". Check the ARIA specification for more details ' +296'http://www.w3.org/TR/wai-aria/roles.');297};298299300/**301* Gets the boolean value of an ARIA state/property.302* @param {!Element} element The element to get the ARIA state for.303* @param {!goog.a11y.aria.State|string} stateName the ARIA state name.304* @return {?boolean} Boolean value for the ARIA state value or null if305* the state value is not 'true', not 'false', or not set.306*/307goog.a11y.aria.getStateBoolean = function(element, stateName) {308'use strict';309var attr =310/** @type {string|boolean|null} */ (element.getAttribute(311goog.a11y.aria.getAriaAttributeName_(stateName)));312goog.asserts.assert(313typeof attr === 'boolean' || attr == null || attr == 'true' ||314attr == 'false');315if (attr == null) {316return attr;317}318return typeof attr === 'boolean' ? attr : attr == 'true';319};320321322/**323* Gets the number value of an ARIA state/property.324* @param {!Element} element The element to get the ARIA state for.325* @param {!goog.a11y.aria.State|string} stateName the ARIA state name.326* @return {?number} Number value for the ARIA state value or null if327* the state value is not a number or not set.328*/329goog.a11y.aria.getStateNumber = function(element, stateName) {330'use strict';331var attr =332/** @type {string|number} */ (element.getAttribute(333goog.a11y.aria.getAriaAttributeName_(stateName)));334goog.asserts.assert(335(attr == null || !isNaN(Number(attr))) && typeof attr !== 'boolean');336return attr == null ? null : Number(attr);337};338339340/**341* Gets the string value of an ARIA state/property.342* @param {!Element} element The element to get the ARIA state for.343* @param {!goog.a11y.aria.State|string} stateName the ARIA state name.344* @return {?string} String value for the ARIA state value or null if345* the state value is empty string or not set.346*/347goog.a11y.aria.getStateString = function(element, stateName) {348'use strict';349var attr =350element.getAttribute(goog.a11y.aria.getAriaAttributeName_(stateName));351goog.asserts.assert(352(attr == null || typeof attr === 'string') &&353(attr == '' || isNaN(Number(attr))) && attr != 'true' && attr != 'false');354return (attr == null || attr == '') ? null : attr;355};356357358/**359* Gets array of strings value of the specified state or360* property for the element.361* Only to be used internally by the ARIA library in goog.a11y.aria.*.362* @param {!Element} element DOM node to get state from.363* @param {!goog.a11y.aria.State} stateName State name.364* @return {!IArrayLike<string>} string Array365* value of the state attribute.366*/367goog.a11y.aria.getStringArrayStateInternalUtil = function(element, stateName) {368'use strict';369var attrValue =370element.getAttribute(goog.a11y.aria.getAriaAttributeName_(stateName));371return goog.a11y.aria.splitStringOnWhitespace_(attrValue);372};373374375/**376* Returns true if element has an ARIA state/property, false otherwise.377* @param {!Element} element The element to get the ARIA state for.378* @param {!goog.a11y.aria.State|string} stateName the ARIA state name.379* @return {boolean}380*/381goog.a11y.aria.hasState = function(element, stateName) {382'use strict';383return element.hasAttribute(goog.a11y.aria.getAriaAttributeName_(stateName));384};385386387/**388* Returns whether the element has a container ARIA role.389* Container roles are ARIA roles that use the aria-activedescendant property390* to manage their active descendants or children. See391* {@link http://www.w3.org/TR/wai-aria/states_and_properties392* #aria-activedescendant} for more information.393* @param {!Element} element394* @return {boolean}395*/396goog.a11y.aria.isContainerRole = function(element) {397'use strict';398var role = goog.a11y.aria.getRole(element);399return goog.array.contains(goog.a11y.aria.CONTAINER_ROLES_, role);400};401402403/**404* Splits the input stringValue on whitespace.405* @param {string} stringValue The value of the string to split.406* @return {!IArrayLike<string>} string Array407* value as result of the split.408* @private409*/410goog.a11y.aria.splitStringOnWhitespace_ = function(stringValue) {411'use strict';412return stringValue ? stringValue.split(/\s+/) : [];413};414415416/**417* Adds the 'aria-' prefix to ariaName.418* @param {string} ariaName ARIA state/property name.419* @private420* @return {string} The ARIA attribute name with added 'aria-' prefix.421* @throws {Error} If no such attribute exists.422*/423goog.a11y.aria.getAriaAttributeName_ = function(ariaName) {424'use strict';425if (goog.asserts.ENABLE_ASSERTS) {426goog.asserts.assert(ariaName, 'ARIA attribute cannot be empty.');427goog.asserts.assert(428goog.object.containsValue(goog.a11y.aria.State, ariaName),429'No such ARIA attribute ' + ariaName);430}431return goog.a11y.aria.ARIA_PREFIX_ + ariaName;432};433434435