Hướng dẫn về khả năng truy cập bàn phím: JavaScript (Phần 2)

theanh

Administrator
Nhân viên
Trong bài viết trước, chúng ta đã nói về cách cải thiện khả năng truy cập cho người dùng bàn phím bằng HTML và CSS. Những ngôn ngữ đó có thể thực hiện công việc hầu hết thời gian, nhưng một số yêu cầu thiết kế và bản chất của một số thành phần tạo ra nhu cầu về các tương tác phức tạp hơn và đây là lúc JavaScript phát huy tác dụng.

Đối với mục đích truy cập bằng bàn phím, hầu hết công việc được thực hiện bằng các công cụ cơ bản mở ra nhiều khả năng tương tác bằng bàn phím. Bài viết này đề cập đến một bộ công cụ mà bạn có thể kết hợp vào các thành phần khác nhau để cải thiện khả năng truy cập cho người dùng bàn phím.

Những điều cơ bản​

Hầu hết thời gian, công việc của bạn với JavaScript để cải thiện khả năng truy cập bàn phím của các thành phần sẽ được thực hiện chỉ bằng một số ít công cụ, bao gồm việc sử dụng trình lắng nghe sự kiện và một số phương thức JavaScript của một số API Web có thể giúp chúng ta thực hiện nhiệm vụ này.

Một trong những công cụ quan trọng nhất mà chúng ta có để thêm tính tương tác vào các dự án của mình là sự tồn tại của sự kiện, đây là quá trình thực thi các hàm kích hoạt khi phần tử bạn đang kiểm tra nhận được thay đổi.

Sự kiện keydown

Một ví dụ về sự kiện mà bạn có thể lắng nghe bằng API Web này là sự kiện keydown, sự kiện này kiểm tra thời điểm một phím được nhấn.

Hiện tại, điều này không được sử dụng để thêm khả năng truy cập bàn phím vào các phần tử như nút hoặc liên kết vì theo mặc định, khi bạn thêm Trình lắng nghe sự kiện click cho chúng, điều này cũng sẽ kích hoạt sự kiện khi bạn sử dụng phím Enter (cho nút và liên kết) và phím Space (chỉ dành cho nút). Thay vào đó, tiện ích của sự kiện keydown xuất hiện khi bạn cần thêm chức năng cho các phím khác.

Để thêm một ví dụ, chúng ta hãy quay lại chú giải công cụ mà chúng ta đã tạo trong phần đầu của bài viết này. Tôi đã đề cập rằng chú giải công cụ này cần được đóng khi bạn nhấn phím Esc. Chúng ta cần một trình lắng nghe sự kiện keydown để kiểm tra xem phím được nhấn có phải là Esc hay không. Để làm được điều đó, chúng ta cần phát hiện phím được nhấn của sự kiện. Trong trường hợp này, chúng ta sẽ kiểm tra thuộc tính của sự kiện key.

Chúng ta sẽ sử dụng keycode.info để kiểm tra bản dump sự kiện cho phím này. Nếu bạn nhấn phím Esc trên trang này, bạn sẽ thấy rằng e.key bằng "Escape".

Lưu ý: Có hai cách khác để phát hiện phím đã nhấn, đó là kiểm tra e.keyCodee.which. Chúng sẽ trả về một số. Trong trường hợp của phím Esc, nó sẽ là 27. Nhưng hãy nhớ rằng đó là những phương án thay thế đã lỗi thời và mặc dù chúng vẫn hoạt động, e.key là lựa chọn được ưu tiên.

Với phương án đó, chúng ta cần chọn các nút và thêm trình lắng nghe sự kiện. Cách tiếp cận của tôi đối với vấn đề này là sử dụng trình lắng nghe sự kiện này để thêm một lớp vào nút và thêm lớp này làm ngoại lệ để hiển thị nó bằng cách sử dụng lớp giả :not(). Hãy bắt đầu thay đổi CSS của chúng ta một chút:
Mã:
button:not(.hide-tooltip):hover + [role="tooltip"],button:not(.hide-tooltip):focus + [role="tooltip"],[role="tooltip"]:hover { display: block;}
Bây giờ, với ngoại lệ này được thêm vào, hãy tạo trình lắng nghe sự kiện của chúng ta!
Mã:
const buttons = [...document.querySelectorAll("button")]buttons.forEach(element => { element.addEventListener("keydown", (e) => { if (e.key === "Escape") { element.classList.add("hide-tooltip") } })})
Và thế là xong! Chỉ với một chút JavaScript, chúng ta đã thêm một hàm trợ năng vào chú giải công cụ của mình. Và đó chỉ là khởi đầu cho những gì chúng ta có thể làm với trình lắng nghe sự kiện keydown. Đây sẽ là một công cụ quan trọng để cải thiện khả năng truy cập bàn phím cho nhiều thành phần, nhưng có một trình lắng nghe sự kiện khác mà chúng ta nên cân nhắc.

blur Sự kiện​

Có một sự kiện khác mà chúng ta sẽ thường xuyên sử dụng. Cái này phát hiện khi phần tử ngừng nhận tiêu điểm. Trình lắng nghe sự kiện này rất quan trọng và hầu hết thời gian, bạn sẽ sử dụng nó để đảo ngược các thay đổi có thể đã thực hiện với trình lắng nghe sự kiện keydown.

Chúng ta hãy quay lại với chú giải công cụ. Hiện tại, nó có một vấn đề: nếu bạn nhấn phím Esc để đóng chú giải công cụ, sau đó bạn lại tập trung vào cùng một phần tử, chú giải công cụ sẽ không xuất hiện. Tại sao? Bởi vì chúng tôi đã thêm lớp hide-tooltip khi bạn nhấn phím Esc, nhưng chúng tôi chưa bao giờ xóa lớp này. Đây là lúc blur phát huy tác dụng. Hãy thêm một trình lắng nghe sự kiện để khôi phục chức năng này.
Mã:
element.addEventListener("blur", (e) => { if (element.classList.contains("hide-tooltip")) { element.classList.remove("hide-tooltip"); }});

Các trình lắng nghe sự kiện khác (và lý do tại sao bạn có thể không cần chúng)​

Tôi đã đề cập rằng chúng ta sẽ cần hai trình lắng nghe sự kiện trong bộ công cụ của mình, nhưng có những trình lắng nghe sự kiện khác mà bạn có thể sử dụng, như focusout hoặc focus. Tuy nhiên, tôi nghĩ rằng các trường hợp sử dụng cho chúng khá khan hiếm. Có một đề cập đặc biệt đến focus vì ngay cả khi bạn có thể tìm thấy các trường hợp sử dụng tốt cho nó, bạn vẫn cần phải rất cẩn thận. Rốt cuộc, nếu bạn không sử dụng nó đúng cách, bạn có thể gây ra thay đổi ngữ cảnh.

WCAG định nghĩa thay đổi ngữ cảnh là "những thay đổi lớn, nếu được thực hiện mà người dùng không nhận thức được, có thể làm mất phương hướng những người dùng không thể xem toàn bộ trang cùng một lúc". Một số ví dụ về thay đổi ngữ cảnh bao gồm:
  • Mở một cửa sổ mới;
  • Thay đổi đáng kể bố cục trang web của bạn;
  • Di chuyển tiêu điểm đến một phần khác của trang web.
Điều này rất quan trọng cần ghi nhớ vì việc tạo ra sự thay đổi ngữ cảnh tại thời điểm tập trung vào một phần tử là vi phạm tiêu chí WCAG 3.2.1:
Khi bất kỳ thành phần giao diện người dùng nào nhận được tiêu điểm, nó không khởi tạo sự thay đổi ngữ cảnh.

— Tiêu chí thành công 3.2.1: Thứ tự tiêu điểm
Nếu bạn không cẩn thận, việc sử dụng sai một hàm lắng nghe sự kiện focus có thể tạo ra sự thay đổi ngữ cảnh. Điều đó có nghĩa là bạn không nên sử dụng nó không? Không hẳn vậy, nhưng thành thật mà nói, tôi khó có thể tìm ra cách sử dụng cho sự kiện này. Hầu hết thời gian, bạn sẽ sử dụng lớp giả :focus để tạo các chức năng tương tự.

Với những điều đã nói, có ít nhất một mẫu thành phần có thể hưởng lợi từ trình lắng nghe sự kiện này trong một số trường hợp, nhưng tôi sẽ đề cập đến nó sau khi tôi bắt đầu nói về các thành phần, vì vậy chúng ta hãy ghim chủ đề đó vào lúc này.

focus() Phương pháp​

Bây giờ, đây là thứ chúng ta sẽ sử dụng thường xuyên! Phương pháp này từ API HTMLElement cho phép chúng ta đưa tiêu điểm bàn phím vào một phần tử cụ thể. Theo mặc định, nó sẽ vẽ chỉ báo tiêu điểm trong phần tử và sẽ cuộn trang đến vị trí của phần tử. Hành vi này có thể được thay đổi bằng một vài tham số:
  • preventScroll
    Khi được đặt thành true, sẽ khiến trình duyệt không cuộn cho đến khi phần tử được lấy nét theo chương trình được hiển thị.
  • focusVisible
    Khi được đặt thành false, sẽ khiến phần tử được lấy nét theo chương trình được hiển thị không hiển thị chỉ báo lấy nét của nó. Thuộc tính này hiện chỉ hoạt động trên Firefox.
Hãy nhớ rằng để lấy nét phần tử, phần tử đó cần phải có thể lấy nét hoặc có thể dùng tab. Nếu bạn cần đưa tiêu điểm đến một phần tử thường không thể dùng tab (như cửa sổ hộp thoại), bạn sẽ cần thêm thuộc tính tabindex với một số nguyên âm để có thể lấy nét. Bạn có thể xem cách tabindex hoạt động trong phần đầu tiên của hướng dẫn này.
Mã:
Bring focus [HEADING=2]Nội dung modal[/HEADING]
Sau đó, chúng ta sẽ thêm trình lắng nghe sự kiện click vào nút để làm cho cửa sổ hộp thoại được lấy nét:
Mã:
const button = document.querySelector("#openModal");const modal = document.querySelector("#modal")button.addEventListener("click", () => { modal.focus()})
Và thế là xong! Phương pháp này sẽ rất hữu ích trong nhiều thành phần kết hợp với thuộc tính keydown, vì vậy, việc hiểu cách thức hoạt động của cả hai là rất quan trọng.

Thay đổi thuộc tính HTML bằng JavaScript​

Một số thuộc tính HTML cần được sửa đổi bằng JavaScript để tạo khả năng truy cập trong các mẫu thành phần phức tạp. Hai thuộc tính quan trọng nhất đối với khả năng truy cập bằng bàn phím là tabindexinert mới được thêm vào gần đây. tabindex có thể được sửa đổi bằng setAttribute. Thuộc tính này yêu cầu hai tham số:
  • name
    Nó kiểm tra tên của thuộc tính mà bạn muốn sửa đổi.
  • value
    Nó sẽ thêm chuỗi mà thuộc tính này yêu cầu nếu nó không yêu cầu một thuộc tính cụ thể (ví dụ, nếu bạn thêm các thuộc tính hidden hoặc contenteditable, bạn sẽ cần sử dụng một chuỗi rỗng).
Chúng ta hãy xem một ví dụ nhanh về cách sử dụng nó:
Mã:
const button = document.querySelector("button")button.setAttribute("tabindex", "-1")
setAttribute sẽ giúp ích rất nhiều cho khả năng truy cập nói chung. (Tôi sử dụng nó rất nhiều để thay đổi các thuộc tính ARIA khi cần!) Nhưng khi chúng ta nói về khả năng truy cập bàn phím, tabindex gần như là thuộc tính duy nhất bạn sẽ sửa đổi bằng phương pháp này.

Tôi đã đề cập đến thuộc tính inert trước đó và thuộc tính này hoạt động hơi khác một chút vì nó có thuộc tính riêng trong HTMLElement Web API. HTMLElement.inert là giá trị boolean cho phép chúng ta chuyển đổi thuộc tính inert.

Hãy ghi nhớ một vài điều trước khi nghĩ đến việc sử dụng thuộc tính này:
  • Bạn sẽ cần một polyfill vì nó chưa được triển khai đầy đủ trong tất cả các trình duyệt và vẫn còn khá mới. Polyfill này do các kỹ sư Chrome tạo ra hoạt động khá tốt trong các thử nghiệm mà tôi đã thực hiện, vì vậy nếu bạn cần thuộc tính này, đây là một cách tiếp cận an toàn, nhưng hãy nhớ rằng nó có thể có những hành vi không mong muốn.
  • Bạn cũng có thể sử dụng setAttribute để thay đổi thuộc tính này! Cả hai đều hoạt động tốt như nhau, ngay cả với polyfill. Tùy thuộc vào bạn quyết định sử dụng cái nào.
Mã:
const button = document.querySelector("button")// Cú pháp với HTMLElement.inertbutton.inert = true// Cú pháp với Element.setAttribute()button.setAttribute("inert", "")
Sự kết hợp các công cụ này sẽ hữu ích cho mục đích trợ năng bàn phím. Bây giờ chúng ta hãy bắt đầu xem chúng hoạt động như thế nào!

Các mẫu thành phần​

Toggletips​




Chúng ta đã học cách tạo chú giải công cụ trong phần trước và tôi đã đề cập đến cách cải thiện chú giải công cụ này bằng JavaScript, nhưng có một mẫu khác cho loại thành phần này được gọi là toggletip, đó là chú giải công cụ hoạt động khi bạn nhấp vào chúng, thay vì di chuột qua chúng.

Chúng ta hãy kiểm tra danh sách nhanh những gì chúng ta cần để đảm bảo điều đó xảy ra:
  • Khi bạn nhấn nút, thông tin sẽ được thông báo cho trình đọc màn hình. Điều đó sẽ xảy ra khi bạn nhấn lại nút. Nhấn nút sẽ không đóng toggletip.
  • Toggletip sẽ đóng khi bạn nhấp ra ngoài toggletip, dừng tập trung vào nút hoặc nhấn phím Esc.
Tôi sẽ áp dụng cách tiếp cận của Heydon Pickering mà ông ấy nói đến trong cuốn sách Inclusive Components. Vì vậy, chúng ta hãy bắt đầu với đánh dấu:
Mã:
Nếu bạn cần kiểm tra thêm thông tin, hãy kiểm tra tại đây   ? Thông tin thêm
Ý tưởng là đưa HTML cần thiết vào bên trong phần tử với role="status". Điều đó sẽ khiến trình đọc màn hình thông báo nội dung khi bạn nhấp vào. Chúng tôi đang sử dụng phần tử button để làm cho nó có thể tab. Bây giờ, hãy tạo tập lệnh để hiển thị nội dung!
Mã:
toggletipButton.addEventListener("click", () => { toggletipInfo.innerHTML = ""; setTimeout(() => { toggletipInfo.innerHTML = toggletipContent; }, 100);});
Như Heydon đề cập trong cuốn sách của mình, chúng tôi sử dụng phương pháp này bằng cách đầu tiên xóa nội dung HTML của vùng chứa rồi sử dụng setTimeout để thêm nội dung đó nhằm đảm bảo rằng mỗi khi bạn nhấp vào, nội dung đó sẽ được thông báo cho người dùng trình đọc màn hình. Bây giờ chúng ta cần kiểm tra xem khi bạn nhấp vào nơi khác, nội dung sẽ ngừng hiển thị.
Mã:
document.addEventListener("click", (e) => { if (toggletipContainer !== e.target) { toggletipInfo.innerHTML = "" }})
Sau khi hoàn thành, đã đến lúc thêm khả năng truy cập bằng bàn phím vào thành phần này. Chúng ta không cần phải làm cho nội dung của toggletip hiển thị khi bạn nhấn nút vì một ngữ nghĩa HTML tốt đã làm điều đó cho chúng ta. Chúng ta cần làm cho nội dung của toggletip ngừng hiển thị khi bạn nhấn phím Esc và khi bạn ngừng tập trung vào nút này. Nó hoạt động rất giống với những gì chúng ta đã làm đối với chú giải công cụ trong phần trước làm ví dụ, vì vậy hãy bắt đầu làm việc với điều đó. Đầu tiên, chúng ta sẽ sử dụng trình lắng nghe sự kiện keydown để kiểm tra thời điểm phím Esc được nhấn:
Mã:
toggletipContainer.addEventListener("keydown", (e) => { if (e.key === "Escape") { toggletipInfo.innerHTML = "" }})
Và bây giờ, chúng ta cần kiểm tra sự kiện blur để thực hiện tương tự. Cái này phải nằm trên phần tử button thay vì trên chính container.
Mã:
toggletipButton.addEventListener("blur", () => { toggletipInfo.innerHTML = "";});
Và đây là kết quả của chúng ta!

Xem Bút [Bản demo Toggletip [phân nhánh]](https://codepen.io/smashingmag/pen/WNyjeJK) của Cristian Diaz.
Xem Bút Bản demo Toggletip [phân nhánh] của Cristian Diaz.
Như tôi đã đề cập, bản demo này hoạt động rất giống với chú giải công cụ mà chúng ta đã tạo, nhưng tôi làm vậy vì một lý do. Trừ khi bạn đang tạo ra thứ gì đó rất khác thường, những mẫu đó sẽ tự lặp lại khá thường xuyên.

Như Stephanie Eckles đã đề cập trong bài viết “4 bài kiểm tra bắt buộc trước khi vận chuyển các tính năng mới”, có một số hành vi dự kiến mà người dùng bàn phím mong đợi từ các thành phần, chẳng hạn như có thể đóng thứ gì đó bạn vừa mở bằng cách nhấn phím Esc hoặc có thể điều hướng một nhóm các tùy chọn liên quan bằng các phím Mũi tên.

Nếu bạn ghi nhớ những mẫu đó, bạn sẽ nhận thấy sự chồng chéo trong các hành vi của một số thành phần nhất định và điều đó sẽ lặp lại khi bạn bắt đầu tạo mã JavaScript để đảm bảo khả năng truy cập bằng bàn phím. Vì vậy, hãy ghi nhớ danh sách này để hiểu được các yêu cầu mà các thành phần bạn đang tạo sẽ cần.

Nói về điều đó, chúng ta hãy kiểm tra một mẫu thành phần phổ biến khác.

Tab​



Roving tabindex
Giao diện có tab là các mẫu mà bạn vẫn có thể thấy thỉnh thoảng. Chúng có một chức năng rất thú vị khi chúng ta nói về điều hướng bàn phím: khi bạn nhấn phím Tab, nó sẽ chuyển đến bảng tab đang hoạt động. Để điều hướng giữa danh sách tab, bạn sẽ cần sử dụng các phím Mũi tên. Đây là một kỹ thuật được gọi là di chuyển tabindex bao gồm việc loại bỏ khả năng của các phần tử không hoạt động để được tababble bằng cách thêm thuộc tính tabindex="-1" và sau đó sử dụng các phím khác để cho phép điều hướng giữa các mục đó.

Với các tab, đây là hành vi mong đợi đối với những phần tử sau:
  • Khi bạn nhấn các phím Trái hoặc Lên, nó sẽ di chuyển tiêu điểm bàn phím vào tab trước đó. Nếu tiêu điểm nằm ở tab đầu tiên, nó sẽ di chuyển tiêu điểm đến tab cuối cùng.
  • Khi bạn nhấn các phím Phải hoặc Xuống, nó sẽ di chuyển tiêu điểm bàn phím đến tab tiếp theo. Nếu tiêu điểm nằm ở tab đầu tiên, nó sẽ di chuyển tiêu điểm đến tab cuối cùng.
Việc tạo chức năng này là sự kết hợp của ba kỹ thuật mà chúng ta đã thấy trước đó: sửa đổi tabindex bằng setAttribute, trình lắng nghe sự kiện keydown và phương thức focus(). Hãy bắt đầu bằng cách kiểm tra đánh dấu của thành phần này:
Mã:
[LIST] 
[*]  Tomato  
[*]  Onion  
[*]  Cần tây  
[*]  Cà rốt [/LIST]
Chúng tôi đang sử dụng aria-selected="true" để hiển thị tab nào đang hoạt động và chúng tôi đang thêm tabindex="-1" để không thể chọn các tab không hoạt động bằng phím Tab. Tabpanels phải có thể tab nếu không có phần tử có thể tab bên trong nó, vì vậy đây là lý do tại sao tôi đã thêm thuộc tính tabindex="0" và các tabpanels không hoạt động được ẩn bằng cách sử dụng thuộc tính hidden.

Đã đến lúc thêm điều hướng bằng các phím mũi tên. Đối với điều này, chúng ta sẽ cần tạo một mảng với các tab và sau đó tạo một hàm cho nó. Bước tiếp theo của chúng ta là kiểm tra tab nào là tab đầu tiên và tab cuối cùng trong danh sách. Điều này rất quan trọng vì hành động sẽ xảy ra khi bạn nhấn một phím sẽ thay đổi nếu tiêu điểm bàn phím nằm trên một trong những phần tử đó.
Mã:
const TABS = [...TABLIST.querySelectorAll("[role='tab']")];const createKeyboardNavigation = () => { const firstTab = TABS[0]; const lastTab = TABS[TABS.length - 1];}
Sau đó, chúng ta sẽ thêm trình lắng nghe sự kiện keydown vào mỗi tab. Tôi sẽ bắt đầu bằng cách thêm chức năng với các mũi tên Left và Up.
Mã:
// Mã trước của hàm createKeyboardNavigationTABS.forEach((element) => { element.addEventListener("keydown", function (e) { if (e.key === "ArrowUp" || e.key === "ArrowLeft") { e.preventDefault(); if (element === firstTab) { lastTab.focus(); } else { const focusableElement = TABS.indexOf(element) - 1; TABS[focusableElement].focus(); } } }}
Đây là những gì đang xảy ra ở đây:
  • Đầu tiên, chúng ta kiểm tra xem phím được nhấn có phải là Up hay Mũi tên Trái. Đối với điều đó, chúng ta kiểm tra event.key.
  • Nếu đúng như vậy, chúng ta cần ngăn các phím đó cuộn trang vì hãy nhớ rằng, theo mặc định, chúng sẽ làm như vậy. Chúng ta có thể sử dụng e.preventDefault() cho mục tiêu này.
  • Nếu phím được lấy nét là tab đầu tiên, nó sẽ tự động đưa tiêu điểm bàn phím đến tab cuối cùng. Điều này được thực hiện bằng cách gọi phương thức focus() để lấy tiêu điểm vào tab cuối cùng (mà chúng ta lưu trữ trong một biến).
  • Nếu không phải vậy, chúng ta cần kiểm tra vị trí của tab đang hoạt động. Khi chúng ta lưu trữ các phần tử tab trong một mảng, chúng ta có thể sử dụng phương thức indexOf() để kiểm tra vị trí.
  • Khi chúng ta đang cố gắng điều hướng đến tab trước đó, chúng ta có thể trừ 1 khỏi kết quả của indexOf() rồi tìm kiếm phần tử tương ứng trong mảng TABS và lập trình để tập trung vào phần tử đó bằng phương thức focus().
Bây giờ chúng ta cần thực hiện một quy trình rất giống với các phím Down và Right:
Mã:
// Mã trước của hàm createKeyboardNavigationelse if (e.key === "ArrowDown" || e.key === "ArrowRight") { e.preventDefault(); if (element == lastTab) { firstTab.focus(); } else { const focusableElement = TABS.indexOf(element) + 1; TABS[focusableElement].focus(); }}
Như tôi đã đề cập, đây là một quy trình rất giống nhau. Thay vì trừ một khỏi kết quả indexOf(), chúng ta thêm 1 vì chúng ta muốn đưa tiêu điểm bàn phím đến phần tử tiếp theo.
Hiển thị nội dung và thay đổi thuộc tính HTML​
Chúng ta đã tạo điều hướng và bây giờ chúng ta cần hiển thị và ẩn nội dung cũng như thao tác các thuộc tính aria-selectedtabindex. Hãy nhớ rằng, chúng ta cần thực hiện điều đó khi tiêu điểm bàn phím nằm trên bảng điều khiển đang hoạt động và bạn nhấn Shift + Tab, tiêu điểm sẽ nằm trong tab đang hoạt động.

Trước tiên, hãy tạo hàm hiển thị bảng điều khiển.
Mã:
const showActivePanel = (element) => { const selectedId = element.target.id; TABPANELS.forEach((e) => { e.hidden = "true"; }); const activePanel = document.querySelector( `[aria-labelledby="${selectedId}"]` ); activePanel.removeAttribute("hidden");}; { const selectedId = element.target.id; TABS.forEach((e) => { const id = e.getAttribute("id"); if (id === selectedId) { e.removeAttribute("tabindex", "0"); e.setAttribute("aria-selected", "true"); } else { e.setAttribute("tabindex", "-1"); e.setAttribute("aria-selected", "false"); } });};
Những gì chúng ta đang làm ở đây là, một lần nữa, kiểm tra thuộc tính id và sau đó xem xét từng tab. Chúng ta sẽ kiểm tra xem id của tab này có tương ứng với id của phần tử được nhấn hay không.

Nếu đúng như vậy, chúng ta sẽ làm cho nó có thể tab bằng bàn phím bằng cách xóa thuộc tính tabindex (vì nó là button, do đó theo mặc định, nó có thể tab bằng bàn phím) hoặc bằng cách thêm thuộc tính tabindex="0". Ngoài ra, chúng tôi sẽ thêm một chỉ báo cho người dùng trình đọc màn hình biết rằng đây là tab đang hoạt động bằng cách thêm thuộc tính aria-selected="true".

Nếu không tương ứng, tabindexaria-selected sẽ được đặt thành -1false tương ứng.

Bây giờ, tất cả những gì chúng ta cần làm là thêm trình lắng nghe sự kiện click vào mỗi tab để xử lý cả hai chức năng.
Mã:
TABS.forEach((element) => { element.addEventListener("click", (element) => { showActivePanel(element), handleSelectedTab(element); });});
Và thế là xong! Chúng tôi đã tạo chức năng để làm cho các tab hoạt động, nhưng chúng tôi có thể làm một số việc khác nếu cần.
Kích hoạt Tab khi lấy nét​
Bạn có nhớ những gì tôi đã đề cập về trình lắng nghe sự kiện focus không? Bạn nên cẩn thận khi sử dụng nó vì nó có thể vô tình tạo ra sự thay đổi ngữ cảnh, nhưng nó có một số công dụng và thành phần này là cơ hội hoàn hảo để sử dụng nó!

Theo ARIA Authoring Practices Guide (APG), chúng ta có thể làm cho nội dung được hiển thị hiển thị khi bạn lấy nét vào tab. Khái niệm này thường được gọi là theo dõi tiêu điểm và có thể hữu ích cho người dùng bàn phím và trình đọc màn hình vì nó cho phép điều hướng dễ dàng hơn qua nội dung.

Tuy nhiên, bạn cần lưu ý một số điều sau:
  • Nếu việc hiển thị nội dung có nghĩa là phải tạo nhiều bản kiến nghị và theo đó, làm chậm mạng, thì việc làm cho nội dung được hiển thị theo tiêu điểm là không mong muốn.
  • Nếu nó thay đổi bố cục theo cách đáng kể, thì đó có thể được coi là thay đổi ngữ cảnh. Điều đó phụ thuộc vào loại nội dung bạn muốn hiển thị và việc thay đổi ngữ cảnh theo tiêu điểm là vấn đề về khả năng truy cập, như tôi đã giải thích trước đó.
Trong trường hợp này, lượng nội dung không cho thấy sự thay đổi lớn về mạng hoặc bố cục, vì vậy tôi sẽ làm cho nội dung được hiển thị theo tiêu điểm của các tab. Đây là một tác vụ rất đơn giản với trình lắng nghe sự kiện tiêu điểm. Chúng ta có thể chỉ cần sao chép và dán trình lắng nghe sự kiện mà chúng ta đã tạo và chỉ cần thay đổi click thành focus.
Mã:
TABS.forEach((element) => { element.addEventListener("click", (element) => { showActivePanel(element), handleSelectedTab(element); }); element.addEventListener("focus", (element) => { showActivePanel(element), handleSelectedTab(element); });});
Và thế là xong! Bây giờ, nội dung được hiển thị sẽ hoạt động mà không cần phải nhấp vào tab. Thực hiện điều đó hay chỉ hoạt động khi nhấp là tùy thuộc vào bạn và đáng ngạc nhiên là một câu hỏi rất tinh tế. Cá nhân tôi sẽ chỉ làm cho nó hiển thị khi bạn nhấn tab vì tôi nghĩ rằng trải nghiệm thay đổi thuộc tính aria-selected chỉ bằng cách tập trung vào phần tử có thể hơi khó hiểu. Tuy nhiên, đây chỉ là giả thuyết của tôi nên hãy cân nhắc những gì tôi nói và luôn kiểm tra với người dùng.
Trình lắng nghe sự kiện keydown bổ sung​
Chúng ta hãy quay lại createKeyboardNavigation một lúc. Có một vài phím chúng ta có thể thêm vào. Chúng ta có thể làm cho phím Home và End đưa tiêu điểm bàn phím đến tab đầu tiên và tab cuối cùng. Điều này hoàn toàn tùy chọn, vì vậy không sao nếu bạn không làm, nhưng chỉ để nhắc lại cách trình lắng nghe sự kiện keydown giúp ích, tôi sẽ làm điều đó.

Đây là một nhiệm vụ rất dễ dàng. Chúng ta có thể tạo thêm một cặp câu lệnh if để kiểm tra xem phím Home và End có được nhấn hay không và vì chúng ta đã lưu tab đầu tiên và tab cuối cùng trong các biến, nên bạn có thể tập trung chúng bằng phương thức focus().
Mã:
// Mã trước của hàm createKeyboardNavigationelse if (e.key === "Home") { e.preventDefault(); firstTab.focus()} else if (e.key === "End") { e.preventDefault(); lastTab.focus()}
Và đây là kết quả của chúng ta!

Xem Bút [Bản demo tab [phân nhánh]](https://codepen.io/smashingmag/pen/YzvVXWw) của Cristian Diaz.
Xem Bút Bản demo tab [phân nhánh] của Cristian Diaz.
Với mã này, chúng tôi đã làm cho thành phần này có thể truy cập được đối với người dùng bàn phím và trình đọc màn hình. Điều này cho thấy các khái niệm cơ bản của trình lắng nghe sự kiện keydown, phương thức focus() và những thay đổi mà chúng ta có thể thực hiện với setAttribute hữu ích như thế nào để thao tác các thành phần phức tạp. Hãy kiểm tra thêm một điều nữa, một điều rất phức tạp, và cách thuộc tính inert có thể giúp chúng ta xử lý nhiệm vụ này một cách dễ dàng.

Modals​



Mở và đóng hộp thoại​
Hộp thoại là một mẫu khá phức tạp khi chúng ta nói về khả năng truy cập bằng bàn phím, vì vậy chúng ta hãy bắt đầu bằng một nhiệm vụ dễ dàng — mở và đóng hộp thoại.

Thực sự rất dễ, nhưng bạn cần lưu ý một điều: rất có thể nút sẽ mở hộp thoại và hộp thoại nằm rất xa trong DOM. Vì vậy, bạn cần quản lý tiêu điểm theo chương trình khi bạn quản lý thành phần này. Có một vấn đề nhỏ ở đây: bạn cần lưu trữ phần tử nào đã mở modal để chúng ta có thể trả về tiêu điểm bàn phím trở lại phần tử này tại thời điểm chúng ta đóng nó.

May mắn thay, có một cách dễ dàng để thực hiện điều đó, nhưng hãy bắt đầu bằng cách tạo đánh dấu cho trang web của chúng ta:
Mã:
      Mở modal        [HEADING=2]Nội dung modal[/HEADING] [LIST] 
[*] [URL=#]Liên kết modal 1[/URL] 
[*] [URL=#]Liên kết modal 2[/URL] 
[*] [URL=#]Liên kết modal 3[/URL] [/LIST] Đóng modal
Như tôi đã đề cập, modal và button cách xa nhau trong DOM. Điều này sẽ giúp tạo bẫy tiêu điểm dễ dàng hơn sau này, nhưng bây giờ, chúng ta hãy kiểm tra ngữ nghĩa của modal:
  • role="dialog" sẽ cung cấp cho phần tử ngữ nghĩa cần thiết cho trình đọc màn hình. Nó cần có nhãn để được nhận dạng là cửa sổ hộp thoại, vì vậy chúng ta sẽ sử dụng tiêu đề của modal làm nhãn bằng cách sử dụng thuộc tính aria-labelledby.
  • aria-modal="true" giúp người dùng trình đọc màn hình chỉ có thể đọc nội dung của các phần tử con của phần tử, vì vậy nó chặn quyền truy cập từ trình đọc màn hình. Tuy nhiên, như bạn có thể thấy trên trang aria-modal của a11ysupport.com, nó không được hỗ trợ đầy đủ, vì vậy bạn không thể chỉ dựa vào đó cho nhiệm vụ này. Nó sẽ hữu ích cho trình đọc màn hình hỗ trợ nó, nhưng bạn sẽ thấy có một cách khác để đảm bảo người dùng trình đọc màn hình không tương tác với bất kỳ thứ gì ngoài modal sau khi nó được mở.
  • Như tôi đã đề cập, chúng ta cần đưa tiêu điểm bàn phím vào modal của mình, vì vậy đây là lý do tại sao chúng ta thêm thuộc tính tabindex="-1".
Với suy nghĩ đó, chúng ta cần tạo hàm để mở modal của mình. Chúng ta cần kiểm tra phần tử nào đã mở nó và để làm điều đó, chúng ta có thể sử dụng thuộc tính document.activeElement để kiểm tra phần tử nào đang được tập trung vào bàn phím ngay bây giờ và lưu trữ nó trong một biến. Đây là cách tiếp cận của tôi cho nhiệm vụ này:
Mã:
letfocusedElementBeforeModalconst modal = document.querySelector("[role='dialog']");const modalOpenButton = document.querySelector("#openModal")const modalCloseButton = document.querySelector("#closeModal")const openModal = () => {focusedElementBeforeModal = document.activeElement modal.hidden = false; modal.focus();};
Rất đơn giản:
  1. Chúng ta lưu trữ nút đã mở hộp thoại;
  2. Sau đó, chúng ta hiển thị nút đó bằng cách xóa thuộc tính hidden;
  3. Sau đó, chúng ta đưa tiêu điểm vào hộp thoại bằng phương thức focus().
Điều cần thiết là bạn phải lưu trữ nút trước khi đưa tiêu điểm vào hộp thoại. Nếu không, phần tử sẽ được lưu trữ trong trường hợp này sẽ là chính hộp thoại và bạn không muốn điều đó.

Bây giờ, chúng ta cần tạo hàm để đóng hộp thoại:
Mã:
const closeModal = () => { modal.hidden = true;focusedElementBeforeModal.focus()}
Đây là lý do tại sao việc lưu trữ phần tử phù hợp lại quan trọng. Khi chúng ta đóng hộp thoại, chúng ta sẽ đưa tiêu điểm bàn phím trở lại phần tử đã mở hộp thoại đó. Với những hàm đã tạo, tất cả những gì chúng ta phải làm là thêm trình lắng nghe sự kiện cho những hàm đó! Hãy nhớ rằng chúng ta cũng cần phải đóng hộp thoại khi bạn nhấn phím Esc.
Mã:
modalOpenButton.addEventListener("click", () => openModal())modalCloseButton.addEventListener("click", () => closeModal())modal.addEventListener("keydown", (e) => { if (e.key === "Escape") { closeModal() }})
Hiện tại, trông nó rất đơn giản. Nhưng nếu chỉ có vậy, hộp thoại sẽ không được coi là một mẫu phức tạp cho khả năng truy cập, phải không? Đây là nơi chúng ta cần tạo một tác vụ rất quan trọng cho thành phần này và chúng ta có hai cách để thực hiện.
Tạo Focus Trap​
Focus trap đảm bảo tiêu điểm bàn phím không thể thoát khỏi thành phần. Điều này rất quan trọng vì nếu người dùng bàn phím có thể tương tác với bất kỳ thứ gì bên ngoài một hộp thoại sau khi hộp thoại đó được mở, thì nó có thể tạo ra trải nghiệm rất khó hiểu. Chúng ta có hai cách để thực hiện điều đó ngay bây giờ.

Một trong số đó là kiểm tra từng phần tử có thể được tab bằng bàn phím, sau đó lưu trữ phần tử đầu tiên và phần tử cuối cùng, và thực hiện như sau:
  • Khi người dùng nhấn Shift + Tab và tiêu điểm bàn phím nằm trên phần tử có thể tab đầu tiên (hãy nhớ rằng, bạn có thể kiểm tra điều đó bằng document.activeElement), tiêu điểm sẽ chuyển đến phần tử có thể tab cuối cùng.
  • Khi người dùng nhấn Tab và tiêu điểm bàn phím nằm trên phần tử có thể tab cuối cùng, tiêu điểm bàn phím sẽ chuyển đến phần tử có thể tab đầu tiên.
Thông thường, tôi sẽ chỉ cho bạn cách tạo mã này, nhưng tôi nghĩ A11y solutions đã tạo ra tập lệnh rất hay để tạo bẫy lấy nét. Nó hoạt động giống như điều hướng bằng bàn phím với các phím mũi tên mà chúng tôi đã tạo cho các phần tử tab (như tôi đã đề cập trước đó, các mẫu tự lặp lại!), vì vậy tôi mời bạn kiểm tra trang này.

Tôi không muốn sử dụng cách tiếp cận này làm giải pháp chính vì nó không hoàn hảo. Đó là một số tình huống mà cách tiếp cận này không đề cập đến.

Đầu tiên là nó không tính đến trình đọc màn hình, đặc biệt là trình đọc màn hình di động. Như Rahul Kumar đã đề cập trong bài viết “Bẫy lấy nét để trợ năng (A11Y)”, Talkback và Voiceover cho phép người dùng cử chỉ và chạm đúp để điều hướng đến phần tử có thể lấy nét tiếp theo hoặc trước đó và những cử chỉ đó không thể được phát hiện bằng trình lắng nghe sự kiện vì về mặt kỹ thuật, những cử chỉ đó là điều không xảy ra trong trình duyệt. Có một giải pháp cho vấn đề đó, nhưng tôi sẽ ghim chủ đề đó vào một lúc.

Mối quan tâm khác là cách tiếp cận bẫy tập trung này có thể dẫn đến những hành vi kỳ lạ nếu bạn sử dụng một số kết hợp các thành phần có thể lập tab. Ví dụ, hãy lấy ví dụ về hộp thoại này:



Về mặt kỹ thuật, phần tử có thể tab đầu tiên là đầu vào đầu tiên. Tuy nhiên, tất cả các đầu vào trong ví dụ này phải tập trung vào phần tử có thể tab cuối cùng (trong trường hợp này là phần tử button) khi người dùng nhấn các phím Shift + Tab. Nếu không, nó có thể gây ra hành vi kỳ lạ nếu người dùng nhấn các phím đó khi tiêu điểm bàn phím đang ở đầu vào thứ hai hoặc thứ ba.

Nếu chúng ta muốn tạo ra một giải pháp đáng tin cậy hơn, cách tiếp cận tốt nhất là sử dụng thuộc tính inert để làm cho nội dung bên ngoài không thể truy cập được đối với trình đọc màn hình và người dùng bàn phím, đảm bảo họ chỉ có thể tương tác với nội dung của hộp thoại. Hãy nhớ rằng, điều này sẽ yêu cầu polyfill inert để tăng thêm độ mạnh mẽ cho kỹ thuật này.

Lưu ý: Điều quan trọng cần lưu ý là mặc dù bẫy tiêu điểm và sử dụng inert trong thực tế giúp đảm bảo khả năng truy cập bằng bàn phím cho các hộp thoại, nhưng chúng không hoạt động giống hệt nhau. Sự khác biệt chính là khi đặt tất cả các tài liệu ngoại trừ hộp thoại là inert, thì nó vẫn cho phép bạn di chuyển ra khỏi trang web và tương tác với các thành phần của trình duyệt. Điều này có thể tốt hơn cho các mối quan ngại về bảo mật nhưng quyết định xem bạn muốn tạo bẫy tiêu điểm theo cách thủ công hay sử dụng thuộc tính inert là tùy thuộc vào bạn.

Điều đầu tiên chúng ta sẽ làm là chọn tất cả các khu vực không có vai trò dialog.inert sẽ xóa mọi tương tác của bàn phím và trình đọc màn hình với các phần tử và các phần tử con của chúng, chúng ta sẽ chỉ cần chọn các phần tử con trực tiếp của body. Đây là lý do tại sao chúng ta để vùng chứa modal tồn tại ở cùng cấp với các thẻ như main, header hoặc footer.
Mã:
// Bộ chọn này hoạt động tốt cho cấu trúc HTML cụ thể này. Điều chỉnh theo dự án của bạn.const nonModalAreas = document.querySelectorAll("body > *:not([role='dialog'])")
Bây giờ chúng ta cần quay lại hàm openModal. Sau khi mở modal, chúng ta cần thêm thuộc tính inert vào các phần tử đó. Đây sẽ là bước cuối cùng trong hàm:
Mã:
const openModal = () => { // Mã đã thêm trước đó nonModalAreas.forEach((element) => { element.inert = true })};
Còn khi bạn đóng modal thì sao? Bạn cần đến hàm closeModal và xóa thuộc tính này. Thuộc tính này cần phải được thực hiện trước khi mọi thứ khác trong mã chạy. Nếu không, trình duyệt sẽ không thể tập trung vào nút đã mở hộp thoại này.
Mã:
const closeModal = () => { nonModalAreas.forEach((element) => { element.inert = false; });// Mã đã thêm trước đó};
Và đây là kết quả của chúng ta!

Xem Bút [Kiểm tra chế độ [phân nhánh]](https://codepen.io/smashingmag/pen/NWzjqYM) của Cristian Diaz.
Xem Bút Kiểm tra chế độ [phân nhánh] của Cristian Diaz.
Giả sử bạn không cảm thấy thoải mái khi sử dụng thuộc tính inert ngay bây giờ và muốn tạo bẫy tiêu điểm theo cách thủ công, như A11y Solutions đã chỉ ra. Bạn có thể làm gì để đảm bảo người dùng trình đọc màn hình không thể thoát khỏi chế độ modal? aria-modal có thể giúp bạn, nhưng hãy nhớ rằng, hỗ trợ cho thuộc tính này khá không ổn định, đặc biệt là đối với Talkback và VoiceOver cho iOS. Vì vậy, điều tốt nhất tiếp theo mà chúng ta có thể làm là thêm thuộc tính aria-hidden="true" vào tất cả các phần tử không phải là chế độ modal. Đây là một quy trình rất giống với quy trình chúng ta đã thực hiện cho thuộc tính inert và bạn cũng có thể sử dụng các phần tử giống nhau trong mảng mà chúng ta đã sử dụng cho chủ đề này!
Mã:
const openModal = () => { //Mã đã thêm trước đó nonModalAreas.forEach((element) => { element.setAttribute("aria-hidden", "true") });};const closeModal = () => { nonModalAreas.forEach((element) => { element.removeAttribute("aria-hidden") }); // Mã đã thêm trước đó};
Vì vậy, cho dù bạn quyết định sử dụng thuộc tính inert hay tạo bẫy tiêu điểm theo cách thủ công, bạn vẫn có thể đảm bảo trải nghiệm người dùng cho người dùng bàn phím và trình đọc màn hình hoạt động tốt nhất.
Phần tử​
Bạn có thể nhận thấy đánh dấu mà tôi đã sử dụng và tôi đã không sử dụng phần tử tương đối mới và có lý do cho điều đó. Đúng vậy, phần tử này giúp ích rất nhiều bằng cách quản lý tiêu điểm vào hộp thoại và nút mở dễ dàng, nhưng như Scott O’Hara chỉ ra trong bài viết “Có hộp thoại mở”, nó vẫn có một số vấn đề về khả năng truy cập mà ngay cả với polyfill vẫn chưa được giải quyết hoàn toàn. Vì vậy, tôi quyết định sử dụng một cách tiếp cận mạnh mẽ hơn ở đó với đánh dấu.

Nếu bạn chưa nghe về phần tử này, nó có một số chức năng để mở và đóng hộp thoại, cũng như một số chức năng mới sẽ hữu ích khi chúng ta tạo hộp thoại. Nếu bạn muốn kiểm tra cách thức hoạt động của nó, bạn có thể xem video của Kevin Powell về phần tử này.

Điều đó không có nghĩa là bạn không nên sử dụng nó. Tình hình của Accessibility về phần tử này đang được cải thiện, nhưng hãy nhớ rằng bạn vẫn cần cân nhắc một số chi tiết nhất định để đảm bảo nó hoạt động bình thường.

Các mẫu thành phần khác​

Tôi có thể tiếp tục với nhiều mẫu thành phần, nhưng thành thật mà nói, tôi nghĩ rằng nó sẽ bắt đầu trở nên thừa thãi vì thực tế là các mẫu đó khá giống nhau giữa các loại thành phần khác nhau mà bạn có thể tạo ra. Trừ khi bạn phải tạo ra thứ gì đó rất khác thường, thì những mẫu mà chúng ta đã thấy ở đây là đủ rồi!

Với những điều đã nói, làm sao bạn có thể biết được những yêu cầu nào bạn sẽ cần cho một thành phần? Đây là câu trả lời có nhiều sắc thái mà bài viết này không thể đề cập đến. Có một số tài nguyên như Kho lưu trữ các thành phần có thể truy cập của Scott O’Hara hoặc Hệ thống thiết kế của chính phủ Anh, nhưng đây là một câu hỏi không có câu trả lời đơn giản. Điều quan trọng nhất về chủ đề này là luôn thử nghiệm chúng với người dùng khuyết tật để biết những lỗi nào có thể xảy ra về mặt khả năng truy cập.

Kết thúc​

Khả năng truy cập bằng bàn phím có thể khá khó, nhưng bạn có thể đạt được điều đó khi hiểu cách người dùng bàn phím tương tác với trang web và những nguyên tắc bạn cần ghi nhớ. Hầu hết thời gian, HTML và CSS sẽ thực hiện tốt công việc đảm bảo khả năng truy cập bằng bàn phím, nhưng đôi khi bạn sẽ cần JavaScript cho các mẫu phức tạp hơn.

Thật ấn tượng khi thấy những gì bạn có thể làm để có khả năng truy cập bằng bàn phím khi bạn nhận thấy hầu hết thời gian, công việc được thực hiện bằng cùng các công cụ cơ bản. Khi bạn hiểu những gì mình cần làm, bạn có thể kết hợp các công cụ đó để tạo ra trải nghiệm người dùng tuyệt vời cho người dùng bàn phím!
 
Back
Bên trên