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

huangapple go评论67阅读模式
英文:

What's the correct HTML markup to use for headers for groups of table rows?

问题

我有一个只有行的单一表格,并且应该在每个组的上方有一个标题。从语义和可访问性的角度来看,正确的标记是什么?

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

以下是一个示例表格,没有任何可访问性属性:

<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">Group 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">Group 3</th>
    </tr>
    <tr>
      <td>1</td>
      <td>2</td>
      <td>3</td>
    </tr>
  </tbody>
</table>

希望这有助于你更好地理解如何标记这样的表格以获得更好的可访问性。

英文:

I have a single table with rows that are grouped, and there should be a header above each group. What is the correct markup for this when it comes to semantics and accessibility?

I'm aware of the scope attribute for &lt;th&gt; elements, and I'm thinking the rowgroup and colgroup might be related to this, but I don't understand how to actually apply it properly, or whether the &lt;td&gt; or &lt;tbody&gt; elements need some attributes applied to them as well.

Here is an example table, without any accessibility attributes:

<!-- begin snippet: js hide: false console: false babel: false -->

<!-- language: lang-css -->

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 }

<!-- language: lang-html -->

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;A&lt;/th&gt;
      &lt;th&gt;B&lt;/th&gt;
      &lt;th&gt;C&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;th colspan=&quot;3&quot;&gt;Group 1&lt;/th&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;2&lt;/td&gt;
      &lt;td&gt;3&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;2&lt;/td&gt;
      &lt;td&gt;3&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;th colspan=&quot;3&quot;&gt;Gruppe 2&lt;/th&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;2&lt;/td&gt;
      &lt;td&gt;3&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;2&lt;/td&gt;
      &lt;td&gt;3&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;2&lt;/td&gt;
      &lt;td&gt;3&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;th colspan=&quot;3&quot;&gt;Gruppe 3&lt;/th&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;2&lt;/td&gt;
      &lt;td&gt;3&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

<!-- end snippet -->

答案1

得分: 3

scope="rowgroup"应该是正确的标记。您已经做得很好,并使用了多个<tbody>元素来分组行。不要忘记,表格还需要一个可访问的名称,您可以通过aria-label<caption>来提供。

HTML规范中提到的rowgroup

row group”状态意味着标题单元格适用于行组中所有其余单元格。如果th元素的scope属性不是锚定在行组中的,则不得处于行组状态。

那么,如何标记一个行组呢?

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

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

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>
英文:

scope=&quot;rowgroup&quot; would be the correct markup.

You already did a great job and used several &lt;tbody&gt; elements to group rows.

Don’t forget that a table also needs an accessible name, which you can provide by means of aria-label or &lt;caption&gt;.

The HTML Spec mentions for rowgroup:

> The row group state means the header cell applies to all the remaining cells in the row group. A th element's scope attribute must not be in the row group state if the element is not anchored in a row group.

So how exactly does one mark up a row group?

ARIA in HTML mentions that the the default element for the rowgroup role is &lt;tbody&gt;, as it’s already used in the question.

  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;th colspan=&quot;3&quot; scope=&quot;rowgroup&quot;&gt;Group 2&lt;/th&gt;
    &lt;/tr&gt;

The row group concept in the HTML specification further mentions that also &lt;thead&gt; and &lt;tfoot&gt; establish such row groups.

The next question, then, is how browsers and screen readers actually expose this rowgroup. For concepts like section roles, the boundaries are announced, so when you navigate inside a new group, its name is announced. rowgroup is derived from section, so this behaviour would seem appropriate as well.

I doubt that a lot of screen readers actually announce this. I will look into this, if anybody has results to share already, that would be great.

  • Firefox 112 does not expose the row group in the accessibility tree, but the rowheader for the &lt;th&gt;. NVDA 2023.1 then does not announce it.

Here’s a sandbox for testing:

<!-- begin snippet: js hide: false console: true babel: false -->

<!-- language: lang-html -->

&lt;table&gt;
  &lt;caption&gt;Table for testing row group headings&lt;/caption&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;A&lt;/th&gt;
      &lt;th&gt;B&lt;/th&gt;
      &lt;th&gt;C&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;th colspan=&quot;3&quot; scope=&quot;rowgroup&quot;&gt;Group 1&lt;/th&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;2&lt;/td&gt;
      &lt;td&gt;3&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;th colspan=&quot;3&quot; scope=&quot;rowgroup&quot;&gt;Group 2&lt;/th&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;2&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

<!-- end snippet -->

答案2

得分: 1

你已经将数据分成了带有 tbody 元素的行组

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

或者(作为非常详细的替代方法),通过在数据单元格的 headers 属性 中列出每个标题的 ID,明确将标题与数据单元格关联


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

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

HTML 规范的第 4.9.10 节 - th 元素包含一个说明哪些标题适用于哪些插槽的注释,包括行组标题。供视觉参考,请查看亮绿色的分支箭头

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

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

<!-- begin snippet: js hide: true console: true babel: false -->
<!-- language: lang-js -->
const table = document.querySelector("table");
table.addEventListener("click", evt => {
  const header = evt.target.closest("th");
  if (!header) return;
  
  const headerCoordinates = getCoordinatesOf(header);
  
  // 特定于此表格
  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;
      
      // 特定于此表格
      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)
  };
}

// 允许键盘交互
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();
});
<!-- language: lang-css -->
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}
<!-- language: lang-html -->
<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>
<!-- end snippet -->
英文:

You have divided the data into row groups with tbody elements.

Use scope=&quot;rowgroup&quot; on the th element to apply that header only to the remaining data of the same row group.

Or (as very verbose alternative), explicitly associate headers to data cells by listing each header by its ID in the data cell's headers attribute.


Row group headers apply to all remaining data cells in the same row group, where "remaining" means: The cells whose slots' x- and y-coordinates are greater than or equal to the header's slots'.

Or in other words: All cells that are part of the rectangle bounding the header cell and the row group's 'highest' slot (where 'highest' means: hightest x- and y-coordinate); the row group's slot in the bottom-right corner (in left-to-right top-to-bottom writing direction).

Section 4.9.10 The th element of the HTML specification contains a note showing which headers apply to which slots, including row group headers. For visual reference, see the bright-green branching arrows:

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

Or see this interactive table which highlights all affected cells for a clicked header:

<!-- begin snippet: js hide: true console: true babel: false -->

<!-- language: lang-js -->

const table = document.querySelector(&quot;table&quot;);
table.addEventListener(&quot;click&quot;, evt =&gt; {
const header = evt.target.closest(&quot;th&quot;);
if (!header) return;
const headerCoordinates = getCoordinatesOf(header);
// Specific to this table
const isColumnHeader = headerCoordinates.y === 0;  
const isRowHeader = !isColumnHeader &amp;&amp; headerCoordinates.x === 1;
const isRowGroupHeader = header.getAttribute(&quot;scope&quot;) === &quot;rowgroup&quot;;
for (let row of table.rows) {
for (let cell of row.cells) {
const cellCoordinates = getCoordinatesOf(cell);
const followsColumnHeader = isColumnHeader &amp;&amp; cellCoordinates.x === headerCoordinates.x
&amp;&amp; cellCoordinates.y &gt;= headerCoordinates.y;
const followsRowHeader = isRowHeader &amp;&amp; cellCoordinates.y === headerCoordinates.y
&amp;&amp; cellCoordinates.x &gt;= headerCoordinates.x;
// Specific to this table
const hasSameRowGroup = cell.closest(&quot;tbody&quot;) === header.closest(&quot;tbody&quot;);
const followsHeader = cellCoordinates.x &gt;= headerCoordinates.x
&amp;&amp; cellCoordinates.y &gt;= headerCoordinates.y;
const followsRowGroupHeader = hasSameRowGroup &amp;&amp; isRowGroupHeader &amp;&amp; followsHeader;
const shouldHighlight = followsColumnHeader || followsRowHeader || followsRowGroupHeader;
cell.classList.toggle(&quot;highlight&quot;, shouldHighlight);
cell.classList.toggle(&quot;rowgroup&quot;, followsRowGroupHeader);
cell.classList.toggle(&quot;column&quot;, !followsRowGroupHeader &amp;&amp; followsColumnHeader);
cell.classList.toggle(&quot;row&quot;, !followsRowGroupHeader &amp;&amp; followsRowHeader);
}
}
});
function getCoordinatesOf(cell) {
return {
x: cell.cellIndex,
y: Array.from(cell.closest(&quot;table&quot;).rows).indexOf(cell.parentElement)
};
}
// Allow keyboard interaction
for (let row of table.rows) {
for (let cell of row.cells) {
if (cell.tagName !== &quot;TH&quot;) continue;
cell.tabIndex = 0;
}
}
table.addEventListener(&quot;keydown&quot;, evt =&gt; {
if (evt.code === &quot;Enter&quot;) evt.target.click();
});
table.addEventListener(&quot;keyup&quot;, evt =&gt; {
if (evt.code === &quot;Space&quot;) evt.target.click();
});

<!-- language: lang-css -->

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}

<!-- language: lang-html -->

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt; &lt;th&gt; ID &lt;th&gt; Measurement &lt;th&gt; Average &lt;th&gt; Maximum
&lt;tbody&gt;
&lt;tr&gt; &lt;td&gt; &lt;th scope=rowgroup&gt; Cats &lt;td&gt; &lt;td&gt;
&lt;tr&gt; &lt;td&gt; 93 &lt;th&gt; Legs &lt;td&gt; 3.5 &lt;td&gt; 4
&lt;tr&gt; &lt;td&gt; 10 &lt;th&gt; Tails &lt;td&gt; 1 &lt;td&gt; 1
&lt;tbody&gt;
&lt;tr&gt; &lt;td&gt; &lt;th scope=rowgroup&gt; English speakers &lt;td&gt; &lt;td&gt;
&lt;tr&gt; &lt;td&gt; 32 &lt;th&gt; Legs &lt;td&gt; 2.67 &lt;td&gt; 4
&lt;tr&gt; &lt;td&gt; 35 &lt;th&gt; Tails &lt;td&gt; 0.33 &lt;td&gt; 1
&lt;/table&gt;

<!-- end snippet -->

huangapple
  • 本文由 发表于 2023年5月10日 14:57:22
  • 转载请务必保留本文链接:https://go.coder-hub.com/76215673.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定