aria-required-children-evaluate.js 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. import {
  2. requiredOwned,
  3. getRole,
  4. getExplicitRole,
  5. getOwnedVirtual
  6. } from '../../commons/aria';
  7. import { hasContentVirtual, idrefs } from '../../commons/dom';
  8. /**
  9. * Get all owned roles of an element
  10. */
  11. function getOwnedRoles(virtualNode, required) {
  12. const ownedRoles = [];
  13. const ownedElements = getOwnedVirtual(virtualNode);
  14. for (let i = 0; i < ownedElements.length; i++) {
  15. const ownedElement = ownedElements[i];
  16. const role = getRole(ownedElement, { noPresentational: true });
  17. // if owned node has no role or is presentational, or if role
  18. // allows group or rowgroup, we keep parsing the descendant tree.
  19. // this means intermediate roles between a required parent and
  20. // child will fail the check
  21. if (
  22. !role ||
  23. (['group', 'rowgroup'].includes(role) &&
  24. required.some(requiredRole => requiredRole === role))
  25. ) {
  26. ownedElements.push(...ownedElement.children);
  27. } else if (role) {
  28. ownedRoles.push(role);
  29. }
  30. }
  31. return ownedRoles;
  32. }
  33. /**
  34. * Get missing children roles
  35. */
  36. function missingRequiredChildren(virtualNode, role, required, ownedRoles) {
  37. const isCombobox = role === 'combobox';
  38. // combobox exceptions
  39. if (isCombobox) {
  40. // remove 'textbox' from missing roles if combobox is a native
  41. // text-type input or owns a 'searchbox'
  42. const textTypeInputs = ['text', 'search', 'email', 'url', 'tel'];
  43. if (
  44. (virtualNode.props.nodeName === 'input' &&
  45. textTypeInputs.includes(virtualNode.props.type)) ||
  46. ownedRoles.includes('searchbox')
  47. ) {
  48. required = required.filter(requiredRole => requiredRole !== 'textbox');
  49. }
  50. // combobox only needs one of [listbox, tree, grid, dialog] and
  51. // only the type that matches the aria-popup value. remove
  52. // all the other popup roles from the list of required
  53. const expandedChildRoles = ['listbox', 'tree', 'grid', 'dialog'];
  54. const expandedValue = virtualNode.attr('aria-expanded');
  55. const expanded = expandedValue && expandedValue.toLowerCase() !== 'false';
  56. const popupRole = (
  57. virtualNode.attr('aria-haspopup') || 'listbox'
  58. ).toLowerCase();
  59. required = required.filter(
  60. requiredRole =>
  61. !expandedChildRoles.includes(requiredRole) ||
  62. (expanded && requiredRole === popupRole)
  63. );
  64. }
  65. for (let i = 0; i < ownedRoles.length; i++) {
  66. var ownedRole = ownedRoles[i];
  67. if (required.includes(ownedRole)) {
  68. required = required.filter(requiredRole => requiredRole !== ownedRole);
  69. // combobox requires all the roles not just any one of them
  70. if (!isCombobox) {
  71. return null;
  72. }
  73. }
  74. }
  75. if (required.length) {
  76. return required;
  77. }
  78. return null;
  79. }
  80. /**
  81. * Check that an element owns all required children for its explicit role.
  82. *
  83. * Required roles are taken from the `ariaRoles` standards object from the roles `requiredOwned` property.
  84. *
  85. * @memberof checks
  86. * @param {Boolean} options.reviewEmpty List of ARIA roles that should be flagged as "Needs Review" rather than a violation if the element has no owned children.
  87. * @data {String[]} List of all missing owned roles.
  88. * @returns {Mixed} True if the element owns all required roles. Undefined if `options.reviewEmpty=true` and the element has no owned children. False otherwise.
  89. */
  90. function ariaRequiredChildrenEvaluate(node, options, virtualNode) {
  91. const reviewEmpty =
  92. options && Array.isArray(options.reviewEmpty) ? options.reviewEmpty : [];
  93. const role = getExplicitRole(virtualNode, { dpub: true });
  94. const required = requiredOwned(role);
  95. if (required === null) {
  96. return true;
  97. }
  98. const ownedRoles = getOwnedRoles(virtualNode, required);
  99. const missing = missingRequiredChildren(
  100. virtualNode,
  101. role,
  102. required,
  103. ownedRoles
  104. );
  105. if (!missing) {
  106. return true;
  107. }
  108. this.data(missing);
  109. // Only review empty nodes when a node is both empty and does not have an aria-owns relationship
  110. if (
  111. reviewEmpty.includes(role) &&
  112. !hasContentVirtual(virtualNode, false, true) &&
  113. !ownedRoles.length &&
  114. (!virtualNode.hasAttr('aria-owns') || !idrefs(node, 'aria-owns').length)
  115. ) {
  116. return undefined;
  117. }
  118. return false;
  119. }
  120. export default ariaRequiredChildrenEvaluate;