ARIA: vai trò grid

Vai trò grid dành cho một widget chứa một hoặc nhiều hàng ô. Vị trí của từng ô là quan trọng và có thể được đưa tiêu điểm bằng nhập liệu bàn phím.

Mô tả

Vai trò grid là một widget tổ hợp chứa một tập hợp gồm một hoặc nhiều hàng với một hoặc nhiều ô, trong đó một số hoặc tất cả ô trong grid có thể nhận tiêu điểm bằng các phương pháp điều hướng hai chiều, chẳng hạn các phím mũi tên.

html
<table role="grid" aria-labelledby="id-select-your-seat">
  <caption id="id-select-your-seat">
    Chọn ghế của bạn
  </caption>
  <tbody role="presentation">
    <tr role="presentation">
      <td></td>
      <th>Hàng A</th>
      <th>Hàng B</th>
    </tr>
    <tr>
      <th scope="row">Lối đi 1</th>
      <td tabindex="0">
        <button id="btn-1a" tabindex="-1">1A</button>
      </td>
      <td tabindex="-1">
        <button id="btn-1b" tabindex="-1">1B</button>
      </td>
      <!-- Thêm các cột khác -->
    </tr>
    <tr>
      <th scope="row">Lối đi 2</th>
      <td tabindex="-1">
        <button id="btn-2a" tabindex="-1">2A</button>
      </td>
      <td tabindex="-1">
        <button id="btn-2b" tabindex="-1">2B</button>
      </td>
      <!-- Thêm các cột khác -->
    </tr>
  </tbody>
</table>

Một widget grid chứa một hoặc nhiều hàng với một hoặc nhiều ô có nội dung tương tác liên quan theo chủ đề. Mặc dù nó không ngụ ý một cách trình bày trực quan cụ thể, nó ngụ ý một mối quan hệ giữa các phần tử. Các cách dùng được chia thành hai nhóm: trình bày thông tin dạng bảng (data grid) và nhóm các widget khác (layout grid). Dù cả data grid và layout grid đều dùng cùng các vai trò, trạng thái và thuộc tính ARIA, sự khác nhau về nội dung và mục đích làm phát sinh các yếu tố quan trọng cần cân nhắc trong thiết kế tương tác bàn phím. Xem ARIA Authoring Practices Guide để biết thêm chi tiết.

Các phần tử ô có vai trò gridcell, trừ khi chúng là tiêu đề hàng hoặc cột, khi đó các phần tử tương ứng là rowheadercolumnheader. Các phần tử ô cần được sở hữu bởi các phần tử có vai trò row. Các hàng có thể được nhóm bằng vai trò rowgroup.

Nếu grid được dùng như một widget tương tác, cần triển khai tương tác bàn phím.

Các vai trò, trạng thái và thuộc tính ARIA liên quan

Vai trò

treegrid (vai trò con)

Nếu grid có các cột có thể mở rộng hoặc thu gọn, có thể dùng treegrid.

row

Một hàng bên trong grid.

rowgroup

Một nhóm chứa một hoặc nhiều hàng row.

Trạng thái và thuộc tính

aria-level

Cho biết cấp độ phân cấp của grid trong các cấu trúc khác.

aria-multiselectable

Nếu aria-multiselectable được đặt thành true, có thể chọn nhiều mục trong grid. Giá trị mặc định là false.

aria-readonly

Nếu người dùng có thể điều hướng grid nhưng không thể thay đổi giá trị của grid, thuộc tính aria-readonly nên được đặt thành true. Giá trị mặc định là false.

Note: Với nhiều trường hợp sử dụng, phần tử HTML <table> là đủ vì nó và các phần tử bảng khác đã bao gồm nhiều vai trò ARIA.

Tương tác bàn phím

Khi người dùng bàn phím gặp một grid, họ điều hướng các hàng và cột bằng các phím left, right, topdown. Để kích hoạt thành phần tương tác, họ sẽ dùng các phím returnspace.

Phím Hành động
Di chuyển tiêu điểm sang một ô bên phải. Tùy chọn (layout grid), nếu tiêu điểm đang ở ô ngoài cùng bên phải của hàng, tiêu điểm có thể chuyển tới ô đầu tiên của hàng tiếp theo. Nếu tiêu điểm đang ở ô cuối cùng của grid, tiêu điểm không di chuyển.
Di chuyển tiêu điểm sang một ô bên trái. Tùy chọn (layout grid), nếu tiêu điểm đang ở ô ngoài cùng bên trái của hàng, tiêu điểm có thể chuyển tới ô cuối cùng của hàng trước đó. Nếu tiêu điểm đang ở ô đầu tiên của grid, tiêu điểm không di chuyển.
Di chuyển tiêu điểm xuống một ô. Tùy chọn (layout grid), nếu tiêu điểm đang ở ô dưới cùng của cột, tiêu điểm có thể chuyển tới ô trên cùng của cột kế tiếp. Nếu tiêu điểm đang ở ô cuối cùng của grid, tiêu điểm không di chuyển.
Di chuyển tiêu điểm lên một ô. Tùy chọn (layout grid), nếu tiêu điểm đang ở ô trên cùng của cột, tiêu điểm có thể chuyển tới ô dưới cùng của cột trước đó. Nếu tiêu điểm đang ở ô đầu tiên của grid, tiêu điểm không di chuyển.
Page Down Di chuyển tiêu điểm xuống một số hàng do tác giả quyết định, thường là cuộn sao cho hàng dưới cùng trong tập hàng hiện đang hiển thị trở thành một trong những hàng đầu tiên được nhìn thấy. Nếu tiêu điểm đang ở hàng cuối cùng của grid, tiêu điểm không di chuyển.
Page Up Di chuyển tiêu điểm lên một số hàng do tác giả quyết định, thường là cuộn sao cho hàng trên cùng trong tập hàng hiện đang hiển thị trở thành một trong những hàng cuối cùng được nhìn thấy. Nếu tiêu điểm đang ở hàng đầu tiên của grid, tiêu điểm không di chuyển.
Home Di chuyển tiêu điểm tới ô đầu tiên trong hàng đang chứa tiêu điểm.
End Di chuyển tiêu điểm tới ô cuối cùng trong hàng đang chứa tiêu điểm.
ctrl + Home Di chuyển tiêu điểm tới ô đầu tiên trong hàng đầu tiên.
ctrl + End Di chuyển tiêu điểm tới ô cuối cùng trong hàng cuối cùng.

Nếu có thể chọn ô, hàng hoặc cột, các tổ hợp phím sau thường được dùng:

Tổ hợp phím Hành động
ctrl + Space Chọn cột chứa tiêu điểm.
shift + Space Chọn hàng chứa tiêu điểm. Nếu grid bao gồm một cột có checkbox để chọn hàng, tổ hợp này có thể được dùng để đánh dấu checkbox đó ngay cả khi tiêu điểm không nằm trên checkbox.
ctrl + A Chọn tất cả các ô.
shift + Mở rộng vùng chọn sang một ô bên phải.
shift + Mở rộng vùng chọn sang một ô bên trái.
shift + Mở rộng vùng chọn xuống một ô.
shift + Mở rộng vùng chọn lên một ô.

Ví dụ

Ví dụ lịch

HTML

html
<table role="grid" aria-labelledby="calendarheader">
  <caption id="calendarheader">
    Tháng 9 năm 2018
  </caption>
  <thead role="rowgroup">
    <tr role="row">
      <td></td>
      <th role="columnheader" aria-label="Chủ nhật">CN</th>
      <th role="columnheader" aria-label="Thứ hai">T2</th>
      <th role="columnheader" aria-label="Thứ ba">T3</th>
      <th role="columnheader" aria-label="Thứ tư">T4</th>
      <th role="columnheader" aria-label="Thứ năm">T5</th>
      <th role="columnheader" aria-label="Thứ sáu">T6</th>
      <th role="columnheader" aria-label="Thứ bảy">T7</th>
    </tr>
  </thead>
  <tbody role="rowgroup">
    <tr role="row">
      <th scope="row" role="rowheader">Tuần 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">Tuần 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">Tuần 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">Tuần 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">Tuần 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">Tuần 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();
});

Thêm ví dụ

Các vấn đề về khả năng truy cập

Ngay cả khi việc dùng bàn phím đã được triển khai đúng, một số người dùng có thể không nhận ra rằng họ phải dùng các phím mũi tên. Hãy bảo đảm chức năng và tương tác cần thiết có thể đạt được tốt nhất bằng vai trò grid.

Đặc tả

Specification
Accessible Rich Internet Applications (WAI-ARIA)
# grid

Xem thêm