accessible-text-virtual.js 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. import arialabelledbyText from '../aria/arialabelledby-text';
  2. import arialabelText from '../aria/arialabel-text';
  3. import nativeTextAlternative from './native-text-alternative';
  4. import formControlValue from './form-control-value';
  5. import subtreeText from './subtree-text';
  6. import titleText from './title-text';
  7. import sanitize from './sanitize';
  8. import isVisible from '../dom/is-visible';
  9. /**
  10. * Finds virtual node and calls accessibleTextVirtual()
  11. * IMPORTANT: This method requires the composed tree at axe._tree
  12. *
  13. * @param {HTMLElement} element The HTMLElement
  14. * @param {Object} context
  15. * @property {Bool} inControlContext
  16. * @property {Bool} inLabelledByContext
  17. * @return {string}
  18. */
  19. function accessibleTextVirtual(virtualNode, context = {}) {
  20. const { actualNode } = virtualNode;
  21. context = prepareContext(virtualNode, context);
  22. // Step 2A, check visibility
  23. if (shouldIgnoreHidden(virtualNode, context)) {
  24. return '';
  25. }
  26. const computationSteps = [
  27. arialabelledbyText, // Step 2B.1
  28. arialabelText, // Step 2C
  29. nativeTextAlternative, // Step 2D
  30. formControlValue, // Step 2E
  31. subtreeText, // Step 2F + Step 2H
  32. textNodeValue, // Step 2G (order with 2H does not matter)
  33. titleText // Step 2I
  34. ];
  35. // Find the first step that returns a non-empty string
  36. const accName = computationSteps.reduce((accName, step) => {
  37. if (context.startNode === virtualNode) {
  38. accName = sanitize(accName);
  39. }
  40. if (accName !== '') {
  41. // yes, whitespace only a11y names halt the algorithm
  42. return accName;
  43. }
  44. return step(virtualNode, context);
  45. }, '');
  46. if (context.debug) {
  47. axe.log(accName || '{empty-value}', actualNode, context);
  48. }
  49. return accName;
  50. }
  51. /**
  52. * Return the nodeValue of a node
  53. * @param {VirtualNode} element
  54. * @return {String} nodeValue value
  55. */
  56. function textNodeValue(virtualNode) {
  57. if (virtualNode.props.nodeType !== 3) {
  58. return '';
  59. }
  60. return virtualNode.props.nodeValue;
  61. }
  62. /**
  63. * Check if the
  64. * @param {VirtualNode} element
  65. * @param {Object} context
  66. * @property {VirtualNode[]} processed
  67. * @return {Boolean}
  68. */
  69. function shouldIgnoreHidden({ actualNode }, context) {
  70. if (!actualNode) {
  71. return false;
  72. }
  73. if (
  74. // If the parent isn't ignored, the text node should not be either
  75. actualNode.nodeType !== 1 ||
  76. // If the target of aria-labelledby is hidden, ignore all descendents
  77. context.includeHidden
  78. ) {
  79. return false;
  80. }
  81. return !isVisible(actualNode, true);
  82. }
  83. /**
  84. * Apply defaults to the context
  85. * @param {VirtualNode} element
  86. * @param {Object} context
  87. * @return {Object} context object with defaults applied
  88. */
  89. function prepareContext(virtualNode, context) {
  90. const { actualNode } = virtualNode;
  91. if (!context.startNode) {
  92. context = { startNode: virtualNode, ...context };
  93. }
  94. if (!actualNode) {
  95. return context;
  96. }
  97. /**
  98. * When `aria-labelledby` directly references a `hidden` element
  99. * the element needs to be included in the accessible name.
  100. *
  101. * When a descendent of the `aria-labelledby` reference is `hidden`
  102. * the element should not be included in the accessible name.
  103. *
  104. * This is done by setting `includeHidden` for the `aria-labelledby` reference.
  105. */
  106. if (
  107. actualNode.nodeType === 1 &&
  108. context.inLabelledByContext &&
  109. context.includeHidden === undefined
  110. ) {
  111. context = {
  112. includeHidden: !isVisible(actualNode, true),
  113. ...context
  114. };
  115. }
  116. return context;
  117. }
  118. /**
  119. * Check if the node is processed with this context before
  120. * @param {VirtualNode} element
  121. * @param {Object} context
  122. * @property {VirtualNode[]} processed
  123. * @return {Boolean}
  124. */
  125. accessibleTextVirtual.alreadyProcessed = function alreadyProcessed(
  126. virtualnode,
  127. context
  128. ) {
  129. context.processed = context.processed || [];
  130. if (context.processed.includes(virtualnode)) {
  131. return true;
  132. }
  133. context.processed.push(virtualnode);
  134. return false;
  135. };
  136. export default accessibleTextVirtual;