用于表格行组的标题的正确HTML标记是什么?

4

我有一个只有一张表格,其中的行被分组了,每个组上方都应该有一个标题。在语义和可访问性方面,这种情况下正确的标记是什么?

我知道<th>元素的scope属性,我认为rowgroupcolgroup可能与此相关,但我不知道如何正确地应用它,或者<td><tbody>元素是否也需要应用一些属性。

这是一个没有任何可访问性属性的示例表格:

table { border-collapse: collapse; text-align: center }
thead { background: #ccc }
th, td { padding: 0.25em 0.5em; border: 1px solid #ddd }
tbody th { background: #eee }
<table>
  <thead>
    <tr>
      <th>A</th>
      <th>B</th>
      <th>C</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th colspan="3">Group 1</th>
    </tr>
    <tr>
      <td>1</td>
      <td>2</td>
      <td>3</td>
    </tr>
    <tr>
      <td>1</td>
      <td>2</td>
      <td>3</td>
    </tr>
  </tbody>
  <tbody>
    <tr>
      <th colspan="3">Gruppe 2</th>
    </tr>
    <tr>
      <td>1</td>
      <td>2</td>
      <td>3</td>
    </tr>
    <tr>
      <td>1</td>
      <td>2</td>
      <td>3</td>
    </tr>
    <tr>
      <td>1</td>
      <td>2</td>
      <td>3</td>
    </tr>
  </tbody>
  <tbody>
    <tr>
      <th colspan="3">Gruppe 3</th>
    </tr>
    <tr>
      <td>1</td>
      <td>2</td>
      <td>3</td>
    </tr>
  </tbody>
</table>


rowgroup和colgroup在UI上不会显示。但是您可以将它们用作具有ARIA角色的元素。https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/rowgroup_role - Developer
我知道aria角色,但是我说的是scope属性的允许值:https://developer.mozilla.org/en-US/docs/Web/HTML/Element/th#scope - Svish
@Svish 别忘了 table 也需要一个可访问的名称。屏幕阅读器会在页面上建立所有表格的索引,用户可以浏览。因此,它们需要一个名称。可以通过 aria-label<caption> 来提供名称。 - Andy
@Andy 别提醒我。开玩笑的,是的,我知道这些问题。不幸的是,不能仅仅添加那些内容,因为需要设计和编辑方面的一些输入,但感谢你提醒我们在跟踪器中创建一个问题。目前所有的表格都缺少标题,但它们通常在之前有一个标题,这使得添加一个基本上与标题中相同的标题有点尴尬... 你有什么建议吗?例如,是否可能将表格链接到标题元素而不是添加标题呢? - Svish
1
当然,在这种情况下,您将使用 aria-labelledby 将可见标题作为表格名称进行重用。 <h2 id="myheading">… <table aria-labelledby="myheading" … - Andy
2个回答

3

scope="rowgroup" 是正确的标记。

您已经做得很好,使用了多个 <tbody> 元素来分组行。

不要忘记 表格 也需要一个可访问的名称,您可以通过 aria-label<caption> 来提供。

HTML 规范提到了关于 rowgroup 的内容

行组 状态意味着标题单元格适用于行组中所有剩余的单元格。如果 th 元素未固定在行组中,则其 scope 属性不能处于行组状态。

那么如何标记行组呢?

HTML中的ARIA提到rowgroup角色的默认元素是<tbody>,因为它已经在问题中使用过了。

  <tbody>
    <tr>
      <th colspan="3" scope="rowgroup">Group 2</th>
    </tr>

HTML 规范中的行组概念 进一步提到,<thead><tfoot> 也建立了这样的行组。

那么,浏览器和屏幕阅读器如何公开此行组呢?对于像 section 角色这样的概念,边界是宣布的,因此当您在新组内导航时,它的名称被宣布。 rowgroup 源自 section,因此此行为似乎也是适当的。

我怀疑很多屏幕阅读器实际上并不会宣布这一点。如果有人已经分享了结果,我会进一步调查。

  • Firefox 112 在可访问性树中不公开该行组,但公开了 <th>rowheader。NVDA 2023.1 然后没有宣布它。

这里有一个测试用的沙箱:

<table>
  <caption>Table for testing row group headings</caption>
  <thead>
    <tr>
      <th>A</th>
      <th>B</th>
      <th>C</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th colspan="3" scope="rowgroup">Group 1</th>
    </tr>
    <tr>
      <td>1</td>
      <td>2</td>
      <td>3</td>
    </tr>
  </tbody>
  <tbody>
    <tr>
      <th colspan="3" scope="rowgroup">Group 2</th>
    </tr>
    <tr>
      <td>1</td>
      <td>2</td>
    </tr>
  </tbody>
</table>


附注:在表格的4.9.12处理模型规范中没有提到dir属性,因此不会影响HTML源代码中数据和标题单元格之间的关系。 dir属性只影响单元格的视觉布局。 - Oskar Grosser
我猜“这些单元格可以放置在标题的右侧或左侧”是让我感到困惑的地方,因为它听起来好像只适用于与行组相同行中的单元格?而在这种情况下,标题适用的单元格位于标题的下方,而不是其右侧或左侧。 - Svish
1
@Svish tbody 元素是行组; 它_分组行_。th 元素是标题单元格,其 范围 仅限于当前 行组 通过 scope="rowgroup"; 标题本身不是行组。我希望这能解决你的困惑。 - Oskar Grosser
我猜MDN需要一些改进?我用HTML规范中更清晰的参考替换了它。你同意吗? - Andy
2
HTML规范中的“行组”特指表格模型的概念。ARIA-role是指浏览器公开的元素角色;ARIA-role不能影响scope="rowgroup"的处理方式。您可能需要链接到表格模型的概念_行组_,它还提到了与行组对应的theadtbodytfoot元素。 - Oskar Grosser
@OskarGrosser 是的,这样更清晰,而且那里也有一个使用单独的 tbodyth scope="rowgroup" 的例子,所以看起来这确实是正确的解决方案,谢谢。 - Svish

1

你已经使用 tbody 元素将数据分成了 行组

th 元素上使用 scope="rowgroup",仅将该标题应用于同一行组中剩余的数据。

或者(作为非常冗长的替代方案),通过在数据单元格的 headers 属性 中列出每个 ID 来明确关联标题和数据单元格


行组标题适用于同一行组中所有其它数据单元格,其中“其它”意味着:其插槽的x和y坐标大于或等于标题所在插槽的坐标。

或者换句话说:所有属于标题单元格和行组“最高”插槽(其中“最高”表示:最高的x和y坐标)所包围的矩形中的单元格;左到右、从上到下书写方向中,行组位于右下角的插槽。

HTML规范的第4.9.10个th元素小节包含一个笔记,显示哪些标题适用于哪些插槽,包括行组标题。 作为视觉参考,请参见明亮的绿色分支箭头:

A table with row groups and row group headers, with arrows indicating which headers apply to which slots.

或者查看此交互式表格,它会突出显示所有受点击标题影响的单元格:

const table = document.querySelector("table");
table.addEventListener("click", evt => {
  const header = evt.target.closest("th");
  if (!header) return;
  
  const headerCoordinates = getCoordinatesOf(header);
  
  // Specific to this table
  const isColumnHeader = headerCoordinates.y === 0;  
  const isRowHeader = !isColumnHeader && headerCoordinates.x === 1;
  const isRowGroupHeader = header.getAttribute("scope") === "rowgroup";
  
  for (let row of table.rows) {
    for (let cell of row.cells) {
      const cellCoordinates = getCoordinatesOf(cell);
      
      const followsColumnHeader = isColumnHeader && cellCoordinates.x === headerCoordinates.x
        && cellCoordinates.y >= headerCoordinates.y;
      const followsRowHeader = isRowHeader && cellCoordinates.y === headerCoordinates.y
        && cellCoordinates.x >= headerCoordinates.x;
      
      // Specific to this table
      const hasSameRowGroup = cell.closest("tbody") === header.closest("tbody");
      const followsHeader = cellCoordinates.x >= headerCoordinates.x
        && cellCoordinates.y >= headerCoordinates.y;
      const followsRowGroupHeader = hasSameRowGroup && isRowGroupHeader && followsHeader;
      
      const shouldHighlight = followsColumnHeader || followsRowHeader || followsRowGroupHeader;
      
      cell.classList.toggle("highlight", shouldHighlight);
      cell.classList.toggle("rowgroup", followsRowGroupHeader);
      cell.classList.toggle("column", !followsRowGroupHeader && followsColumnHeader);
      cell.classList.toggle("row", !followsRowGroupHeader && followsRowHeader);
    }
  }
});

function getCoordinatesOf(cell) {
  return {
    x: cell.cellIndex,
    y: Array.from(cell.closest("table").rows).indexOf(cell.parentElement)
  };
}

// Allow keyboard interaction
for (let row of table.rows) {
  for (let cell of row.cells) {
    if (cell.tagName !== "TH") continue;
    cell.tabIndex = 0;
  }
}
table.addEventListener("keydown", evt => {
  if (evt.code === "Enter") evt.target.click();
});
table.addEventListener("keyup", evt => {
  if (evt.code === "Space") evt.target.click();
});
th, td {
  border: 1px solid black;
  font-size: large;
  font-family: sans-serif;
}
th {cursor: default}

.highlight.column {background-color: orange}
.highlight.rowgroup {background-color: lightgreen}
.highlight.row {background-color: aquamarine}
<table>
 <thead>
  <tr> <th> ID <th> Measurement <th> Average <th> Maximum
 <tbody>
  <tr> <td> <th scope=rowgroup> Cats <td> <td>
  <tr> <td> 93 <th> Legs <td> 3.5 <td> 4
  <tr> <td> 10 <th> Tails <td> 1 <td> 1
 <tbody>
  <tr> <td> <th scope=rowgroup> English speakers <td> <td>
  <tr> <td> 32 <th> Legs <td> 2.67 <td> 4
  <tr> <td> 35 <th> Tails <td> 0.33 <td> 1
</table>


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接