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

另見