aria.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. // Copyright 2007 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview Utilities for adding, removing and setting ARIA roles and
  16. * states as defined by W3C ARIA standard: http://www.w3.org/TR/wai-aria/
  17. * All modern browsers have some form of ARIA support, so no browser checks are
  18. * performed when adding ARIA to components.
  19. *
  20. */
  21. goog.provide('goog.a11y.aria');
  22. goog.require('goog.a11y.aria.Role');
  23. goog.require('goog.a11y.aria.State');
  24. goog.require('goog.a11y.aria.datatables');
  25. goog.require('goog.array');
  26. goog.require('goog.asserts');
  27. goog.require('goog.dom');
  28. goog.require('goog.dom.TagName');
  29. goog.require('goog.object');
  30. goog.require('goog.string');
  31. /**
  32. * ARIA states/properties prefix.
  33. * @private
  34. */
  35. goog.a11y.aria.ARIA_PREFIX_ = 'aria-';
  36. /**
  37. * ARIA role attribute.
  38. * @private
  39. */
  40. goog.a11y.aria.ROLE_ATTRIBUTE_ = 'role';
  41. /**
  42. * A list of tag names for which we don't need to set ARIA role and states
  43. * because they have well supported semantics for screen readers or because
  44. * they don't contain content to be made accessible.
  45. * @private
  46. */
  47. goog.a11y.aria.TAGS_WITH_ASSUMED_ROLES_ = goog.object.createSet([
  48. goog.dom.TagName.A, goog.dom.TagName.AREA, goog.dom.TagName.BUTTON,
  49. goog.dom.TagName.HEAD, goog.dom.TagName.INPUT, goog.dom.TagName.LINK,
  50. goog.dom.TagName.MENU, goog.dom.TagName.META, goog.dom.TagName.OPTGROUP,
  51. goog.dom.TagName.OPTION, goog.dom.TagName.PROGRESS, goog.dom.TagName.STYLE,
  52. goog.dom.TagName.SELECT, goog.dom.TagName.SOURCE, goog.dom.TagName.TEXTAREA,
  53. goog.dom.TagName.TITLE, goog.dom.TagName.TRACK
  54. ]);
  55. /**
  56. * A list of roles which are considered container roles.
  57. * Container roles are ARIA roles which use the aria-activedescendant property
  58. * to manage their active descendants or children. See
  59. * {@link http://www.w3.org/TR/wai-aria/states_and_properties
  60. * #aria-activedescendant} for more information.
  61. * @private @const {!Array<goog.a11y.aria.Role>}
  62. */
  63. goog.a11y.aria.CONTAINER_ROLES_ = [
  64. goog.a11y.aria.Role.COMBOBOX, goog.a11y.aria.Role.GRID,
  65. goog.a11y.aria.Role.GROUP, goog.a11y.aria.Role.LISTBOX,
  66. goog.a11y.aria.Role.MENU, goog.a11y.aria.Role.MENUBAR,
  67. goog.a11y.aria.Role.RADIOGROUP, goog.a11y.aria.Role.ROW,
  68. goog.a11y.aria.Role.ROWGROUP, goog.a11y.aria.Role.TAB_LIST,
  69. goog.a11y.aria.Role.TEXTBOX, goog.a11y.aria.Role.TOOLBAR,
  70. goog.a11y.aria.Role.TREE, goog.a11y.aria.Role.TREEGRID
  71. ];
  72. /**
  73. * Sets the role of an element. If the roleName is
  74. * empty string or null, the role for the element is removed.
  75. * We encourage clients to call the goog.a11y.aria.removeRole
  76. * method instead of setting null and empty string values.
  77. * Special handling for this case is added to ensure
  78. * backword compatibility with existing code.
  79. *
  80. * @param {!Element} element DOM node to set role of.
  81. * @param {!goog.a11y.aria.Role|string} roleName role name(s).
  82. */
  83. goog.a11y.aria.setRole = function(element, roleName) {
  84. if (!roleName) {
  85. // Setting the ARIA role to empty string is not allowed
  86. // by the ARIA standard.
  87. goog.a11y.aria.removeRole(element);
  88. } else {
  89. if (goog.asserts.ENABLE_ASSERTS) {
  90. goog.asserts.assert(
  91. goog.object.containsValue(goog.a11y.aria.Role, roleName),
  92. 'No such ARIA role ' + roleName);
  93. }
  94. element.setAttribute(goog.a11y.aria.ROLE_ATTRIBUTE_, roleName);
  95. }
  96. };
  97. /**
  98. * Gets role of an element.
  99. * @param {!Element} element DOM element to get role of.
  100. * @return {?goog.a11y.aria.Role} ARIA Role name.
  101. */
  102. goog.a11y.aria.getRole = function(element) {
  103. var role = element.getAttribute(goog.a11y.aria.ROLE_ATTRIBUTE_);
  104. return /** @type {goog.a11y.aria.Role} */ (role) || null;
  105. };
  106. /**
  107. * Removes role of an element.
  108. * @param {!Element} element DOM element to remove the role from.
  109. */
  110. goog.a11y.aria.removeRole = function(element) {
  111. element.removeAttribute(goog.a11y.aria.ROLE_ATTRIBUTE_);
  112. };
  113. /**
  114. * Sets the state or property of an element.
  115. * @param {!Element} element DOM node where we set state.
  116. * @param {!(goog.a11y.aria.State|string)} stateName State attribute being set.
  117. * Automatically adds prefix 'aria-' to the state name if the attribute is
  118. * not an extra attribute.
  119. * @param {string|boolean|number|!Array<string>} value Value
  120. * for the state attribute.
  121. */
  122. goog.a11y.aria.setState = function(element, stateName, value) {
  123. if (goog.isArray(value)) {
  124. value = value.join(' ');
  125. }
  126. var attrStateName = goog.a11y.aria.getAriaAttributeName_(stateName);
  127. if (value === '' || value == undefined) {
  128. var defaultValueMap = goog.a11y.aria.datatables.getDefaultValuesMap();
  129. // Work around for browsers that don't properly support ARIA.
  130. // According to the ARIA W3C standard, user agents should allow
  131. // setting empty value which results in setting the default value
  132. // for the ARIA state if such exists. The exact text from the ARIA W3C
  133. // standard (http://www.w3.org/TR/wai-aria/states_and_properties):
  134. // "When a value is indicated as the default, the user agent
  135. // MUST follow the behavior prescribed by this value when the state or
  136. // property is empty or undefined."
  137. // The defaultValueMap contains the default values for the ARIA states
  138. // and has as a key the goog.a11y.aria.State constant for the state.
  139. if (stateName in defaultValueMap) {
  140. element.setAttribute(attrStateName, defaultValueMap[stateName]);
  141. } else {
  142. element.removeAttribute(attrStateName);
  143. }
  144. } else {
  145. element.setAttribute(attrStateName, value);
  146. }
  147. };
  148. /**
  149. * Toggles the ARIA attribute of an element.
  150. * Meant for attributes with a true/false value, but works with any attribute.
  151. * If the attribute does not have a true/false value, the following rules apply:
  152. * A not empty attribute will be removed.
  153. * An empty attribute will be set to true.
  154. * @param {!Element} el DOM node for which to set attribute.
  155. * @param {!(goog.a11y.aria.State|string)} attr ARIA attribute being set.
  156. * Automatically adds prefix 'aria-' to the attribute name if the attribute
  157. * is not an extra attribute.
  158. */
  159. goog.a11y.aria.toggleState = function(el, attr) {
  160. var val = goog.a11y.aria.getState(el, attr);
  161. if (!goog.string.isEmptyOrWhitespace(goog.string.makeSafe(val)) &&
  162. !(val == 'true' || val == 'false')) {
  163. goog.a11y.aria.removeState(el, /** @type {!goog.a11y.aria.State} */ (attr));
  164. return;
  165. }
  166. goog.a11y.aria.setState(el, attr, val == 'true' ? 'false' : 'true');
  167. };
  168. /**
  169. * Remove the state or property for the element.
  170. * @param {!Element} element DOM node where we set state.
  171. * @param {!goog.a11y.aria.State} stateName State name.
  172. */
  173. goog.a11y.aria.removeState = function(element, stateName) {
  174. element.removeAttribute(goog.a11y.aria.getAriaAttributeName_(stateName));
  175. };
  176. /**
  177. * Gets value of specified state or property.
  178. * @param {!Element} element DOM node to get state from.
  179. * @param {!goog.a11y.aria.State|string} stateName State name.
  180. * @return {string} Value of the state attribute.
  181. */
  182. goog.a11y.aria.getState = function(element, stateName) {
  183. // TODO(user): return properly typed value result --
  184. // boolean, number, string, null. We should be able to chain
  185. // getState(...) and setState(...) methods.
  186. var attr =
  187. /** @type {string|number|boolean} */ (
  188. element.getAttribute(
  189. goog.a11y.aria.getAriaAttributeName_(stateName)));
  190. var isNullOrUndefined = attr == null || attr == undefined;
  191. return isNullOrUndefined ? '' : String(attr);
  192. };
  193. /**
  194. * Returns the activedescendant element for the input element by
  195. * using the activedescendant ARIA property of the given element.
  196. * @param {!Element} element DOM node to get activedescendant
  197. * element for.
  198. * @return {?Element} DOM node of the activedescendant, if found.
  199. */
  200. goog.a11y.aria.getActiveDescendant = function(element) {
  201. var id =
  202. goog.a11y.aria.getState(element, goog.a11y.aria.State.ACTIVEDESCENDANT);
  203. return goog.dom.getOwnerDocument(element).getElementById(id);
  204. };
  205. /**
  206. * Sets the activedescendant ARIA property value for an element.
  207. * If the activeElement is not null, it should have an id set.
  208. * @param {!Element} element DOM node to set activedescendant ARIA property to.
  209. * @param {?Element} activeElement DOM node being set as activedescendant.
  210. */
  211. goog.a11y.aria.setActiveDescendant = function(element, activeElement) {
  212. var id = '';
  213. if (activeElement) {
  214. id = activeElement.id;
  215. goog.asserts.assert(id, 'The active element should have an id.');
  216. }
  217. goog.a11y.aria.setState(element, goog.a11y.aria.State.ACTIVEDESCENDANT, id);
  218. };
  219. /**
  220. * Gets the label of the given element.
  221. * @param {!Element} element DOM node to get label from.
  222. * @return {string} label The label.
  223. */
  224. goog.a11y.aria.getLabel = function(element) {
  225. return goog.a11y.aria.getState(element, goog.a11y.aria.State.LABEL);
  226. };
  227. /**
  228. * Sets the label of the given element.
  229. * @param {!Element} element DOM node to set label to.
  230. * @param {string} label The label to set.
  231. */
  232. goog.a11y.aria.setLabel = function(element, label) {
  233. goog.a11y.aria.setState(element, goog.a11y.aria.State.LABEL, label);
  234. };
  235. /**
  236. * Asserts that the element has a role set if it's not an HTML element whose
  237. * semantics is well supported by most screen readers.
  238. * Only to be used internally by the ARIA library in goog.a11y.aria.*.
  239. * @param {!Element} element The element to assert an ARIA role set.
  240. * @param {!IArrayLike<string>} allowedRoles The child roles of
  241. * the roles.
  242. */
  243. goog.a11y.aria.assertRoleIsSetInternalUtil = function(element, allowedRoles) {
  244. if (goog.a11y.aria.TAGS_WITH_ASSUMED_ROLES_[element.tagName]) {
  245. return;
  246. }
  247. var elementRole = /** @type {string}*/ (goog.a11y.aria.getRole(element));
  248. goog.asserts.assert(
  249. elementRole != null, 'The element ARIA role cannot be null.');
  250. goog.asserts.assert(
  251. goog.array.contains(allowedRoles, elementRole),
  252. 'Non existing or incorrect role set for element.' +
  253. 'The role set is "' + elementRole + '". The role should be any of "' +
  254. allowedRoles + '". Check the ARIA specification for more details ' +
  255. 'http://www.w3.org/TR/wai-aria/roles.');
  256. };
  257. /**
  258. * Gets the boolean value of an ARIA state/property.
  259. * @param {!Element} element The element to get the ARIA state for.
  260. * @param {!goog.a11y.aria.State|string} stateName the ARIA state name.
  261. * @return {?boolean} Boolean value for the ARIA state value or null if
  262. * the state value is not 'true', not 'false', or not set.
  263. */
  264. goog.a11y.aria.getStateBoolean = function(element, stateName) {
  265. var attr =
  266. /** @type {string|boolean} */ (
  267. element.getAttribute(
  268. goog.a11y.aria.getAriaAttributeName_(stateName)));
  269. goog.asserts.assert(
  270. goog.isBoolean(attr) || attr == null || attr == 'true' ||
  271. attr == 'false');
  272. if (attr == null) {
  273. return attr;
  274. }
  275. return goog.isBoolean(attr) ? attr : attr == 'true';
  276. };
  277. /**
  278. * Gets the number value of an ARIA state/property.
  279. * @param {!Element} element The element to get the ARIA state for.
  280. * @param {!goog.a11y.aria.State|string} stateName the ARIA state name.
  281. * @return {?number} Number value for the ARIA state value or null if
  282. * the state value is not a number or not set.
  283. */
  284. goog.a11y.aria.getStateNumber = function(element, stateName) {
  285. var attr =
  286. /** @type {string|number} */ (
  287. element.getAttribute(
  288. goog.a11y.aria.getAriaAttributeName_(stateName)));
  289. goog.asserts.assert(
  290. (attr == null || !isNaN(Number(attr))) && !goog.isBoolean(attr));
  291. return attr == null ? null : Number(attr);
  292. };
  293. /**
  294. * Gets the string value of an ARIA state/property.
  295. * @param {!Element} element The element to get the ARIA state for.
  296. * @param {!goog.a11y.aria.State|string} stateName the ARIA state name.
  297. * @return {?string} String value for the ARIA state value or null if
  298. * the state value is empty string or not set.
  299. */
  300. goog.a11y.aria.getStateString = function(element, stateName) {
  301. var attr =
  302. element.getAttribute(goog.a11y.aria.getAriaAttributeName_(stateName));
  303. goog.asserts.assert(
  304. (attr == null || goog.isString(attr)) &&
  305. (attr == '' || isNaN(Number(attr))) && attr != 'true' && attr != 'false');
  306. return (attr == null || attr == '') ? null : attr;
  307. };
  308. /**
  309. * Gets array of strings value of the specified state or
  310. * property for the element.
  311. * Only to be used internally by the ARIA library in goog.a11y.aria.*.
  312. * @param {!Element} element DOM node to get state from.
  313. * @param {!goog.a11y.aria.State} stateName State name.
  314. * @return {!IArrayLike<string>} string Array
  315. * value of the state attribute.
  316. */
  317. goog.a11y.aria.getStringArrayStateInternalUtil = function(element, stateName) {
  318. var attrValue =
  319. element.getAttribute(goog.a11y.aria.getAriaAttributeName_(stateName));
  320. return goog.a11y.aria.splitStringOnWhitespace_(attrValue);
  321. };
  322. /**
  323. * Returns true if element has an ARIA state/property, false otherwise.
  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 {boolean}
  327. */
  328. goog.a11y.aria.hasState = function(element, stateName) {
  329. return element.hasAttribute(goog.a11y.aria.getAriaAttributeName_(stateName));
  330. };
  331. /**
  332. * Returns whether the element has a container ARIA role.
  333. * Container roles are ARIA roles that use the aria-activedescendant property
  334. * to manage their active descendants or children. See
  335. * {@link http://www.w3.org/TR/wai-aria/states_and_properties
  336. * #aria-activedescendant} for more information.
  337. * @param {!Element} element
  338. * @return {boolean}
  339. */
  340. goog.a11y.aria.isContainerRole = function(element) {
  341. var role = goog.a11y.aria.getRole(element);
  342. return goog.array.contains(goog.a11y.aria.CONTAINER_ROLES_, role);
  343. };
  344. /**
  345. * Splits the input stringValue on whitespace.
  346. * @param {string} stringValue The value of the string to split.
  347. * @return {!IArrayLike<string>} string Array
  348. * value as result of the split.
  349. * @private
  350. */
  351. goog.a11y.aria.splitStringOnWhitespace_ = function(stringValue) {
  352. return stringValue ? stringValue.split(/\s+/) : [];
  353. };
  354. /**
  355. * Adds the 'aria-' prefix to ariaName.
  356. * @param {string} ariaName ARIA state/property name.
  357. * @private
  358. * @return {string} The ARIA attribute name with added 'aria-' prefix.
  359. * @throws {Error} If no such attribute exists.
  360. */
  361. goog.a11y.aria.getAriaAttributeName_ = function(ariaName) {
  362. if (goog.asserts.ENABLE_ASSERTS) {
  363. goog.asserts.assert(ariaName, 'ARIA attribute cannot be empty.');
  364. goog.asserts.assert(
  365. goog.object.containsValue(goog.a11y.aria.State, ariaName),
  366. 'No such ARIA attribute ' + ariaName);
  367. }
  368. return goog.a11y.aria.ARIA_PREFIX_ + ariaName;
  369. };