ARIA:网格角色

网格角色用于包含一个或多个行单元格的小部件。每个单元格的位置都很重要,可以使用键盘输入来获得焦点。

描述

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>

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

单元格元素具有 gridcell 角色,除非它们是行或列标题。然后,这些元素分别为 rowheadercolumnheader。单元格元素需要由具有 row 角色的元素拥有。行可以使用 rowgroups 分组。

如果网格用作交互式小部件,则需要实现 键盘交互

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

角色

treegrid(子类)

如果网格的列可以展开或折叠,则可以使用树网格。

row

网格内的行。

rowgroup

包含一个或多个 row 的组。

状态和属性

aria-level

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

aria-multiselectable

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

aria-readonly

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

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

键盘交互

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

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

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

键组合 操作
ctrl + Space 选中包含焦点的列。
shift + Space 选中包含焦点的行。如果网格包含一列具有复选框以选中行的复选框,则即使焦点不在复选框上,也可以使用此键组合来选中该复选框。
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 #000;
  text-align: right;
  color: #767676;
}

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

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 row = 0;
let col = 0;
let maxrow = trs.length - 1;
let maxcol = 0;

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

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

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();
});

更多示例

无障碍问题

即使键盘使用已正确实现,一些用户可能不知道他们必须使用箭头键。确保可以通过使用网格角色来最佳地实现所需的功能和交互。

规范

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

另请参阅