ARIA:grid 角色

grid 角色用于包含一个或多个单元格行的控件。每个单元格的位置很重要,可以通过键盘输入进行聚焦。

描述

grid 角色是一个复合控件,包含一个或多个包含一个或多个单元格的行集合,其中 grid 中的所有或部分单元格都可以通过二维导航(如方向箭头键)进行聚焦。

html
<table role="grid" aria-labelledby="id-select-your-seat">
  <caption id="id-select-your-seat">
    Select your seat
  </caption>
  <tbody role="presentation">
    <tr role="presentation">
      <td></td>
      <th>Row A</th>
      <th>Row B</th>
    </tr>
    <tr>
      <th scope="row">Aisle 1</th>
      <td tabindex="0">
        <button id="1a" tabindex="-1">1A</button>
      </td>
      <td tabindex="-1">
        <button id="1b" tabindex="-1">1B</button>
      </td>
      <!-- More Columns -->
    </tr>
    <tr>
      <th scope="row">Aisle 2</th>
      <td tabindex="-1">
        <button id="2a" tabindex="-1">2A</button>
      </td>
      <td tabindex="-1">
        <button id="2b" tabindex="-1">2B</button>
      </td>
      <!-- More Columns -->
    </tr>
  </tbody>
</table>

grid 控件包含一个或多个行,这些行包含一个或多个主题相关的交互式内容单元格。虽然它不暗示特定的视觉呈现,但它暗示了元素之间的关系。用途可分为两类:呈现表格信息(数据网格)和分组其他控件(布局网格)。尽管数据网格和布局网格都使用相同的 ARIA 角色、状态和属性,但它们的内容和目的的差异会凸显在键盘交互设计中需要考虑的重要因素。有关更多详细信息,请参阅 ARIA Authoring Practices Guide

单元格元素具有 gridcell 角色,除非它们是行或列标题,在这种情况下,元素分别是 rowheadercolumnheader 角色。单元格元素需要由具有 row 角色的元素拥有。可以使用 rowgroup 角色对行进行分组。

如果 grid 用作交互式控件,则需要实现 键盘交互

关联的 ARIA 角色、状态和属性

角色

treegrid(子类)

如果 grid 具有可展开或折叠的列,则可以使用 treegrid。

row

grid 中的一行。

rowgroup

包含一个或多个 row 的组。

状态和属性

aria-level

指示 grid 在其他结构中的层次级别。

aria-multiselectable

如果将 aria-multiselectable 设置为 true,则可以选中 grid 中的多个项目。默认值为 false

aria-readonly

如果用户可以导航 grid 但不能更改 grid 的值或值,则应将 aria-readonly 设置为 true。默认值为 false

注意:对于许多用例,HTML <table> 元素就足够了,因为该元素及其各种表格元素已经包含了许多 ARIA 角色。

键盘交互

当键盘用户遇到 grid 时,他们使用 leftrighttopdown 键在行和列之间导航。要激活交互式组件,他们将使用 returnspace 键。

动作
将焦点向右移动一个单元格。可选(布局网格),如果焦点位于行的最右侧单元格,焦点可能会移动到下一行的第一个单元格。如果焦点位于 grid 的最后一个单元格,焦点不会移动。
将焦点向左移动一个单元格。可选(布局网格),如果焦点位于行的最左侧单元格,焦点可能会移动到上一行的最后一个单元格。如果焦点位于 grid 的第一个单元格,焦点不会移动。
将焦点向下移动一个单元格。可选(布局网格),如果焦点位于列的最底部单元格,焦点可能会移动到下一列的第一个单元格。如果焦点位于 grid 的最后一个单元格,焦点不会移动。
将焦点向上移动一个单元格。可选(布局网格),如果焦点位于列的最顶部单元格,焦点可能会移动到上一列的最后一个单元格。如果焦点位于 grid 的第一个单元格,焦点不会移动。
Page Down 将焦点向下移动由作者确定的行数,通常会滚动,使当前可见行集中的最后一行成为可见行之一。如果焦点位于 grid 的最后一行,焦点不会移动。
Page Up 将焦点向上移动由作者确定的行数,通常会滚动,使当前可见行集中的第一行成为可见行的最后一行之一。如果焦点位于 grid 的第一行,焦点不会移动。
Home 将焦点移至包含焦点所在的行的第一个单元格。
End 将焦点移至包含焦点所在的行的最后一个单元格。
ctrl + Home 将焦点移至第一行的第一个单元格。
ctrl + End 将焦点移至最后一行的最后一个单元格。

如果可以选中单元格、行或列,则通常使用以下组合键

组合键 动作
ctrl + Space 选中包含焦点所在的列。
shift + Space 选中包含焦点所在的行。如果 grid 包含一个带有复选框用于选择行的列,则可以使用此组合键来选中该复选框,即使焦点不在复选框上。
ctrl + A 选中所有单元格。
shift + 将选择向右扩展一个单元格。
shift + 将选择向左扩展一个单元格。
shift + 将选择向下扩展一个单元格。
shift + 将选择向上扩展一个单元格。

示例

日历示例

HTML

html
<table role="grid" aria-labelledby="calendarheader">
  <caption id="calendarheader">
    September 2018
  </caption>
  <thead role="rowgroup">
    <tr role="row">
      <td></td>
      <th role="columnheader" aria-label="Sunday">S</th>
      <th role="columnheader" aria-label="Monday">M</th>
      <th role="columnheader" aria-label="Tuesday">T</th>
      <th role="columnheader" aria-label="Wednesday">W</th>
      <th role="columnheader" aria-label="Thursday">T</th>
      <th role="columnheader" aria-label="Friday">F</th>
      <th role="columnheader" aria-label="Saturday">S</th>
    </tr>
  </thead>
  <tbody role="rowgroup">
    <tr role="row">
      <th scope="row" role="rowheader">Week 1</th>
      <td>26</td>
      <td>27</td>
      <td>28</td>
      <td>29</td>
      <td>30</td>
      <td>31</td>
      <td role="gridcell" tabindex="-1">1</td>
    </tr>
    <tr role="row">
      <th scope="row" role="rowheader">Week 2</th>
      <td role="gridcell" tabindex="-1">2</td>
      <td role="gridcell" tabindex="-1">3</td>
      <td role="gridcell" tabindex="-1">4</td>
      <td role="gridcell" tabindex="-1">5</td>
      <td role="gridcell" tabindex="-1">6</td>
      <td role="gridcell" tabindex="-1">7</td>
      <td role="gridcell" tabindex="-1">8</td>
    </tr>
    <tr role="row">
      <th scope="row" role="rowheader">Week 3</th>
      <td role="gridcell" tabindex="-1">9</td>
      <td role="gridcell" tabindex="-1">10</td>
      <td role="gridcell" tabindex="-1">11</td>
      <td role="gridcell" tabindex="-1">12</td>
      <td role="gridcell" tabindex="-1">13</td>
      <td role="gridcell" tabindex="-1">14</td>
      <td role="gridcell" tabindex="-1">15</td>
    </tr>
    <tr role="row">
      <th scope="row" role="rowheader">Week 4</th>
      <td role="gridcell" tabindex="-1">16</td>
      <td role="gridcell" tabindex="-1">17</td>
      <td role="gridcell" tabindex="-1">18</td>
      <td role="gridcell" tabindex="-1">19</td>
      <td role="gridcell" tabindex="-1">20</td>
      <td role="gridcell" tabindex="-1">21</td>
      <td role="gridcell" tabindex="-1">22</td>
    </tr>
    <tr role="row">
      <th scope="row" role="rowheader">Week 5</th>
      <td role="gridcell" tabindex="-1">23</td>
      <td role="gridcell" tabindex="-1">24</td>
      <td role="gridcell" tabindex="-1">25</td>
      <td role="gridcell" tabindex="-1">26</td>
      <td role="gridcell" tabindex="-1">27</td>
      <td role="gridcell" tabindex="-1">28</td>
      <td role="gridcell" tabindex="-1">29</td>
    </tr>
    <tr role="row">
      <th scope="row" role="rowheader">Week 6</th>
      <td role="gridcell" tabindex="-1">30</td>
      <td>1</td>
      <td>2</td>
      <td>3</td>
      <td>4</td>
      <td>5</td>
      <td>6</td>
    </tr>
  </tbody>
</table>

CSS

css
table {
  margin: 0;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
}

tbody th,
tbody td {
  padding: 5px;
}

tbody td {
  border: 1px solid black;
  text-align: right;
  color: #767676;
}

tbody td[role="gridcell"] {
  color: black;
}

tbody td[role="gridcell"]:hover,
tbody td[role="gridcell"]:focus {
  background-color: #f6f6f6;
  outline: 3px solid blue;
}

JavaScript

js
const selectables = document.querySelectorAll('table td[role="gridcell"]');

selectables[0].setAttribute("tabindex", 0);

const trs = document.querySelectorAll("table tbody tr");
let rowIndex = 0;
let colIndex = 0;
let maxRow = trs.length - 1;
let maxCol = 0;

trs.forEach((row) => {
  row.querySelectorAll("td").forEach((el) => {
    el.dataset.row = rowIndex;
    el.dataset.col = colIndex;
    colIndex++;
  });
  if (colIndex > maxCol) {
    maxCol = colIndex - 1;
  }
  colIndex = 0;
  rowIndex++;
});

function moveTo(newRow, newCol) {
  const tgt = document.querySelector(
    `[data-row="${newRow}"][data-col="${newCol}"]`,
  );
  if (tgt?.getAttribute("role") !== "gridcell") {
    return false;
  }
  document.querySelectorAll("[role=gridcell]").forEach((el) => {
    el.setAttribute("tabindex", "-1");
  });
  tgt.setAttribute("tabindex", "0");
  tgt.focus();
  return true;
}

document.querySelector("table").addEventListener("keydown", (event) => {
  const col = parseInt(event.target.dataset.col, 10);
  const row = parseInt(event.target.dataset.row, 10);
  switch (event.key) {
    case "ArrowRight": {
      const newRow = col === 6 ? row + 1 : row;
      const newCol = col === 6 ? 0 : col + 1;
      moveTo(newRow, newCol);
      break;
    }
    case "ArrowLeft": {
      const newRow = col === 0 ? row - 1 : row;
      const newCol = col === 0 ? 6 : col - 1;
      moveTo(newRow, newCol);
      break;
    }
    case "ArrowDown":
      moveTo(row + 1, col);
      break;
    case "ArrowUp":
      moveTo(row - 1, col);
      break;
    case "Home": {
      if (event.ctrlKey) {
        let i = 0;
        let result;
        do {
          let j = 0;
          do {
            result = moveTo(i, j);
            j++;
          } while (!result);
          i++;
        } while (!result);
      } else {
        moveTo(row, 0);
      }
      break;
    }
    case "End": {
      if (event.ctrlKey) {
        let i = maxRow;
        let result;
        do {
          let j = maxCol;
          do {
            result = moveTo(i, j);
            j--;
          } while (!result);
          i--;
        } while (!result);
      } else {
        moveTo(
          row,
          document.querySelector(
            `[data-row="${event.target.dataset.row}"]:last-of-type`,
          ).dataset.col,
        );
      }
      break;
    }
    case "PageUp": {
      let i = 0;
      let result;
      do {
        result = moveTo(i, col);
        i++;
      } while (!result);
      break;
    }
    case "PageDown": {
      let i = maxRow;
      let result;
      do {
        result = moveTo(i, col);
        i--;
      } while (!result);
      break;
    }
    case "Enter": {
      console.log(event.target.textContent);
      break;
    }
  }
  event.preventDefault();
});

更多示例

可访问性考虑

即使键盘使用已正确实现,某些用户可能不知道他们需要使用箭头键。请确保 grid 角色能够最好地实现所需的功能和交互。

规范

规范
无障碍富互联网应用程序 (WAI-ARIA)
# grid

另见