is-data-table.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. import getRoleType from '../aria/get-role-type';
  2. import isFocusable from '../dom/is-focusable';
  3. import findUp from '../dom/find-up';
  4. import getElementCoordinates from '../dom/get-element-coordinates';
  5. import getViewportSize from '../dom/get-viewport-size';
  6. /**
  7. * Determines whether a table is a data table
  8. * @method isDataTable
  9. * @memberof axe.commons.table
  10. * @instance
  11. * @param {HTMLTableElement} node The table to test
  12. * @return {Boolean}
  13. * @see http://asurkov.blogspot.co.uk/2011/10/data-vs-layout-table.html
  14. */
  15. function isDataTable(node) {
  16. var role = (node.getAttribute('role') || '').toLowerCase();
  17. // The element is not focusable and has role=presentation
  18. if ((role === 'presentation' || role === 'none') && !isFocusable(node)) {
  19. return false;
  20. }
  21. // Table inside editable area is data table always since the table structure is crucial for table editing
  22. if (
  23. node.getAttribute('contenteditable') === 'true' ||
  24. findUp(node, '[contenteditable="true"]')
  25. ) {
  26. return true;
  27. }
  28. // Table having ARIA table related role is data table
  29. if (role === 'grid' || role === 'treegrid' || role === 'table') {
  30. return true;
  31. }
  32. // Table having ARIA landmark role is data table
  33. if (getRoleType(role) === 'landmark') {
  34. return true;
  35. }
  36. // Table having datatable="0" attribute is layout table
  37. if (node.getAttribute('datatable') === '0') {
  38. return false;
  39. }
  40. // Table having summary attribute is data table
  41. if (node.getAttribute('summary')) {
  42. return true;
  43. }
  44. // Table having legitimate data table structures is data table
  45. if (node.tHead || node.tFoot || node.caption) {
  46. return true;
  47. }
  48. // colgroup / col - colgroup is magically generated
  49. for (
  50. var childIndex = 0, childLength = node.children.length;
  51. childIndex < childLength;
  52. childIndex++
  53. ) {
  54. if (node.children[childIndex].nodeName.toUpperCase() === 'COLGROUP') {
  55. return true;
  56. }
  57. }
  58. var cells = 0;
  59. var rowLength = node.rows.length;
  60. var row, cell;
  61. var hasBorder = false;
  62. for (var rowIndex = 0; rowIndex < rowLength; rowIndex++) {
  63. row = node.rows[rowIndex];
  64. for (
  65. var cellIndex = 0, cellLength = row.cells.length;
  66. cellIndex < cellLength;
  67. cellIndex++
  68. ) {
  69. cell = row.cells[cellIndex];
  70. if (cell.nodeName.toUpperCase() === 'TH') {
  71. return true;
  72. }
  73. if (
  74. !hasBorder &&
  75. (cell.offsetWidth !== cell.clientWidth ||
  76. cell.offsetHeight !== cell.clientHeight)
  77. ) {
  78. hasBorder = true;
  79. }
  80. if (
  81. cell.getAttribute('scope') ||
  82. cell.getAttribute('headers') ||
  83. cell.getAttribute('abbr')
  84. ) {
  85. return true;
  86. }
  87. if (
  88. ['columnheader', 'rowheader'].includes(
  89. (cell.getAttribute('role') || '').toLowerCase()
  90. )
  91. ) {
  92. return true;
  93. }
  94. // abbr element as a single child element of table cell
  95. if (
  96. cell.children.length === 1 &&
  97. cell.children[0].nodeName.toUpperCase() === 'ABBR'
  98. ) {
  99. return true;
  100. }
  101. cells++;
  102. }
  103. }
  104. // Table having nested table is layout table
  105. if (node.getElementsByTagName('table').length) {
  106. return false;
  107. }
  108. // Table having only one row or column is layout table (row)
  109. if (rowLength < 2) {
  110. return false;
  111. }
  112. // Table having only one row or column is layout table (column)
  113. var sampleRow = node.rows[Math.ceil(rowLength / 2)];
  114. if (sampleRow.cells.length === 1 && sampleRow.cells[0].colSpan === 1) {
  115. return false;
  116. }
  117. // Table having many columns (>= 5) is data table
  118. if (sampleRow.cells.length >= 5) {
  119. return true;
  120. }
  121. // Table having borders around cells is data table
  122. if (hasBorder) {
  123. return true;
  124. }
  125. // Table having differently colored rows is data table
  126. var bgColor, bgImage;
  127. for (rowIndex = 0; rowIndex < rowLength; rowIndex++) {
  128. row = node.rows[rowIndex];
  129. if (
  130. bgColor &&
  131. bgColor !==
  132. window.getComputedStyle(row).getPropertyValue('background-color')
  133. ) {
  134. return true;
  135. } else {
  136. bgColor = window
  137. .getComputedStyle(row)
  138. .getPropertyValue('background-color');
  139. }
  140. if (
  141. bgImage &&
  142. bgImage !==
  143. window.getComputedStyle(row).getPropertyValue('background-image')
  144. ) {
  145. return true;
  146. } else {
  147. bgImage = window
  148. .getComputedStyle(row)
  149. .getPropertyValue('background-image');
  150. }
  151. }
  152. // Table having many rows (>= 20) is data table
  153. if (rowLength >= 20) {
  154. return true;
  155. }
  156. // Wide table (more than 95% of the document width) is layout table
  157. if (
  158. getElementCoordinates(node).width >
  159. getViewportSize(window).width * 0.95
  160. ) {
  161. return false;
  162. }
  163. // Table having small amount of cells (<= 10) is layout table
  164. if (cells < 10) {
  165. return false;
  166. }
  167. // Table containing embed, object, applet of iframe elements (typical advertisements elements) is layout table
  168. if (node.querySelector('object, embed, iframe, applet')) {
  169. return false;
  170. }
  171. // Otherwise it's data table
  172. return true;
  173. }
  174. export default isDataTable;