get-role.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import getExplicitRole from './get-explicit-role';
  2. import getImplicitRole from './implicit-role';
  3. import getGlobalAriaAttrs from '../standards/get-global-aria-attrs';
  4. import isFocusable from '../dom/is-focusable';
  5. import { getNodeFromTree } from '../../core/utils';
  6. import AbstractVirtuaNode from '../../core/base/virtual-node/abstract-virtual-node';
  7. // when an element inherits the presentational role from a parent
  8. // is not defined in the spec, but through testing it seems to be
  9. // when a specific HTML parent relationship is required and that
  10. // parent has `role=presentation`, then the child inherits the
  11. // role (i.e. table, ul, dl). Further testing has shown that
  12. // intermediate elements (such as divs) break this chain only in
  13. // Chrome.
  14. //
  15. // Also, any nested structure chains reset the role (so two nested
  16. // lists with the topmost list role=none will not cause the nested
  17. // list to inherit the role=none).
  18. //
  19. // from Scott O'Hara:
  20. //
  21. // "the expectation for me, in standard html is that element
  22. // structures that require specific parent/child relationships,
  23. // if the parent is set to presentational that should set the
  24. // children to presentational. ala, tables and lists."
  25. // "but outside of those specific constructs, i would not expect
  26. // role=presentation to do anything to child element roles"
  27. const inheritsPresentationChain = {
  28. // valid parent elements, any other element will prevent any
  29. // children from inheriting a presentational role from a valid
  30. // ancestor
  31. td: ['tr'],
  32. th: ['tr'],
  33. tr: ['thead', 'tbody', 'tfoot', 'table'],
  34. thead: ['table'],
  35. tbody: ['table'],
  36. tfoot: ['table'],
  37. li: ['ol', 'ul'],
  38. // dts and dds can be wrapped in divs and the div will pass through
  39. // the presentation role
  40. dt: ['dl', 'div'],
  41. dd: ['dl', 'div'],
  42. div: ['dl']
  43. };
  44. // role presentation inheritance.
  45. // Source: https://www.w3.org/TR/wai-aria-1.1/#conflict_resolution_presentation_none
  46. function getInheritedRole(vNode, explicitRoleOptions) {
  47. const parentNodeNames = inheritsPresentationChain[vNode.props.nodeName];
  48. if (!parentNodeNames) {
  49. return null;
  50. }
  51. // if we can't look at the parent then we can't know if the node
  52. // inherits the presentational role or not
  53. if (!vNode.parent) {
  54. throw new ReferenceError(
  55. 'Cannot determine role presentational inheritance of a required parent outside the current scope.'
  56. );
  57. }
  58. // parent is not a valid ancestor that can inherit presentation
  59. if (!parentNodeNames.includes(vNode.parent.props.nodeName)) {
  60. return null;
  61. }
  62. const parentRole = getExplicitRole(vNode.parent, explicitRoleOptions);
  63. if (
  64. ['none', 'presentation'].includes(parentRole) &&
  65. !hasConflictResolution(vNode.parent)
  66. ) {
  67. return parentRole;
  68. }
  69. // an explicit role of anything other than presentational will
  70. // prevent any children from inheriting a presentational role
  71. // from a valid ancestor
  72. if (parentRole) {
  73. return null;
  74. }
  75. return getInheritedRole(vNode.parent, explicitRoleOptions);
  76. }
  77. function resolveImplicitRole(vNode, { chromium, ...explicitRoleOptions }) {
  78. const implicitRole = getImplicitRole(vNode, {
  79. chromium
  80. });
  81. if (!implicitRole) {
  82. return null;
  83. }
  84. const presentationalRole = getInheritedRole(vNode, explicitRoleOptions);
  85. if (presentationalRole) {
  86. return presentationalRole;
  87. }
  88. return implicitRole;
  89. }
  90. // role conflict resolution
  91. // note: Chrome returns a list with resolved role as "generic"
  92. // instead of as a list
  93. // (e.g. <ul role="none" aria-label><li>hello</li></ul>)
  94. // we will return it as a list as that is the best option.
  95. // Source: https://www.w3.org/TR/wai-aria-1.1/#conflict_resolution_presentation_none
  96. // See also: https://github.com/w3c/aria/issues/1270
  97. function hasConflictResolution(vNode) {
  98. const hasGlobalAria = getGlobalAriaAttrs().some(attr => vNode.hasAttr(attr));
  99. return hasGlobalAria || isFocusable(vNode);
  100. }
  101. /**
  102. *
  103. * @method resolveRole
  104. * @param {Element|VirtualNode} node
  105. * @param {Object} options
  106. * @see getRole for option details
  107. * @returns {string|null} Role or null
  108. * @deprecated noImplicit option is deprecated. Use aria.getExplicitRole instead.
  109. */
  110. function resolveRole(node, { noImplicit, ...roleOptions } = {}) {
  111. const vNode =
  112. node instanceof AbstractVirtuaNode ? node : getNodeFromTree(node);
  113. if (vNode.props.nodeType !== 1) {
  114. return null;
  115. }
  116. const explicitRole = getExplicitRole(vNode, roleOptions);
  117. if (!explicitRole) {
  118. return noImplicit ? null : resolveImplicitRole(vNode, roleOptions);
  119. }
  120. if (!['presentation', 'none'].includes(explicitRole)) {
  121. return explicitRole;
  122. }
  123. if (hasConflictResolution(vNode)) {
  124. // return null if there is a conflict resolution but no implicit
  125. // has been set as the explicit role is not the true role
  126. return noImplicit ? null : resolveImplicitRole(vNode, roleOptions);
  127. }
  128. // role presentation or none and no conflict resolution
  129. return explicitRole;
  130. }
  131. /**
  132. * Return the semantic role of an element.
  133. *
  134. * @method getRole
  135. * @memberof axe.commons.aria
  136. * @instance
  137. * @param {Element|VirtualNode} node
  138. * @param {Object} options
  139. * @param {boolean} options.noImplicit Do not return the implicit role // @deprecated
  140. * @param {boolean} options.fallback Allow fallback roles
  141. * @param {boolean} options.abstracts Allow role to be abstract
  142. * @param {boolean} options.dpub Allow role to be any (valid) doc-* roles
  143. * @param {boolean} options.noPresentational return null if role is presentation or none
  144. * @param {boolean} options.chromium Include implicit roles from chromium-based browsers in role result
  145. * @returns {string|null} Role or null
  146. *
  147. * @deprecated noImplicit option is deprecated. Use aria.getExplicitRole instead.
  148. */
  149. function getRole(node, { noPresentational, ...options } = {}) {
  150. const role = resolveRole(node, options);
  151. if (noPresentational && ['presentation', 'none'].includes(role)) {
  152. return null;
  153. }
  154. return role;
  155. }
  156. export default getRole;