Bài 27.3: Accessible forms trong React-TypeScript

Bài 27.3: Accessible forms trong React-TypeScript
Chào mừng trở lại với chuỗi bài viết về Lập trình Web Front-end! Hôm nay, chúng ta sẽ đào sâu vào một khía cạnh cực kỳ quan trọng nhưng thường bị bỏ qua: xây dựng các form thân thiện và dễ tiếp cận (accessible forms) trong các ứng dụng React và TypeScript của chúng ta. Form là cánh cổng để người dùng tương tác với ứng dụng – đăng nhập, gửi thông tin, đặt hàng... Nếu form không dễ tiếp cận, chúng ta đang vô tình loại trừ một lượng lớn người dùng, bao gồm những người sử dụng công nghệ hỗ trợ như trình đọc màn hình (screen reader), bàn phím (thay vì chuột), hoặc các thiết bị hỗ trợ khác.
Xây dựng form dễ tiếp cận không chỉ là tuân thủ các quy định (như WCAG) mà còn là thể hiện sự tôn trọng đối với mọi người dùng. May mắn thay, với sức mạnh của React và sự chặt chẽ của TypeScript, chúng ta có những công cụ tuyệt vời để làm điều này hiệu quả.
Tại sao Form lại đặc biệt cần Accessible?
Form là nơi người dùng cần nhập liệu và hiểu rõ mình đang nhập gì, kết quả ra sao. Đối với người dùng bình thường, việc nhìn thấy nhãn (label), ô nhập (input), thông báo lỗi là hiển nhiên. Nhưng với người dùng khiếm thị sử dụng trình đọc màn hình, hoặc người dùng chỉ dùng bàn phím:
- Họ cần trình đọc màn hình thông báo rõ ràng ô nhập này là gì (ví dụ: "Tên của bạn, trường nhập văn bản").
- Họ cần biết trường nào là bắt buộc.
- Họ cần biết ngay lập tức khi có lỗi xảy ra và lỗi đó là gì, liên quan đến trường nào.
- Họ cần di chuyển qua lại giữa các trường nhập một cách logic chỉ bằng phím
Tab
.
Nếu thiếu đi các kỹ thuật accessible, form của bạn có thể trở nên vô hình hoặc không thể sử dụng đối với họ.
Nền tảng: Semantic HTML và Label
Nguyên tắc đầu tiên và quan trọng nhất khi làm form accessible, bất kể bạn dùng framework nào, là sử dụng Semantic HTML. Điều này có nghĩa là sử dụng đúng các thẻ HTML cho đúng mục đích: <form>
, <label>
, <input>
, <textarea>
, <select>
, <button>
, <fieldset>
, <legend>
, <datalist>
, <option>
, <optgroup>
.
Đặc biệt, thẻ <label>
là không thể thiếu cho mọi trường nhập liệu (<input>
, <textarea>
, <select>
). Thẻ <label>
giúp trình đọc màn hình liên kết văn bản mô tả với chính trường nhập đó. Cách kết nối chuẩn nhất là sử dụng thuộc tính htmlFor
của <label>
trỏ đến id
của trường nhập tương ứng.
<label htmlFor="usernameInput">Tên đăng nhập:</label>
<input type="text" id="usernameInput" name="username" />
Trong React, chúng ta sử dụng htmlFor
thay vì for
(vì for
là từ khóa trong JavaScript).
- Giải thích: Khi trình đọc màn hình gặp
<input id="usernameInput">
, nó sẽ tìm thẻ<label>
cóhtmlFor="usernameInput"
và đọc nội dung của label ("Tên đăng nhập:") cho người dùng. Người dùng cũng có thể click vào văn bản của label để tự động focus vào trường nhập tương ứng – một lợi ích lớn cho người dùng gặp khó khăn về vận động hoặc dùng thiết bị di động.
Bàn Phím Là Công Cụ Điều Hướng Chính
Nhiều người dùng công nghệ hỗ trợ, cũng như những người dùng thành thạo, sử dụng bàn phím để điều hướng qua các form bằng phím Tab
. Trình duyệt mặc định thường xử lý tốt việc này với các phần tử form chuẩn. Tuy nhiên, khi bạn xây dựng các thành phần tùy chỉnh (custom components) cho form (ví dụ: custom checkbox, select box), bạn cần đảm bảo:
- Phần tử đó có thể nhận focus (sử dụng
tabindex="0"
nếu cần, nhưng ưu tiên các phần tử tương tác gốc). - Các phím điều hướng như
Space
(để chọn/bỏ chọn),Enter
(để kích hoạt), phím mũi tên (để di chuyển trong danh sách) hoạt động như mong đợi. - Trạng thái focus được hiển thị rõ ràng (đường viền
outline
mặc định của trình duyệt thường tốt, đừng ẩn nó bằng CSSoutline: none;
).
Trong React, việc quản lý focus có thể đòi hỏi sử dụng ref
và focus()
API, đặc biệt cho các component phức tạp.
Xử Lý Lỗi Một Cách Dễ Tiếp Cận (Accessible Error Handling)
Đây là một trong những thử thách lớn nhất trong form accessibility. Chỉ hiển thị viền đỏ quanh ô nhập hoặc một dòng chữ lỗi nhỏ màu đỏ không đủ cho người dùng khiếm thị. Trình đọc màn hình cần được thông báo rằng:
- Trường này hiện đang có lỗi.
- Có một thông báo lỗi liên quan đến trường này.
- Nội dung của thông báo lỗi đó là gì.
Chúng ta sử dụng các thuộc tính ARIA (Accessible Rich Internet Applications) để làm điều này. ARIA là một bộ các thuộc tính đặc biệt bạn thêm vào các phần tử HTML để cung cấp thêm ngữ nghĩa cho công nghệ hỗ trợ mà HTML gốc chưa có hoặc không đủ rõ ràng.
Các thuộc tính ARIA quan trọng cho xử lý lỗi form:
aria-invalid="true"
: Đặt thuộc tính này trên trường nhập khi dữ liệu nhập vào không hợp lệ. Trình đọc màn hình sẽ thông báo "invalid entry" hoặc tương tự khi focus vào trường này.aria-describedby
: Thuộc tính này tạo ra một liên kết chương trình giữa trường nhập và một phần tử khác chứa thông tin mô tả hoặc thông báo lỗi. Giá trị củaaria-describedby
phải làid
của phần tử chứa thông báo lỗi.
Ví dụ:
import React, { useState } from 'react'; interface InputFieldProps { id: string; label: string; type?: string; value: string; onChange: (e: React.ChangeEvent<HTMLInputElement>) => void; error?: string; // Thêm prop error } const InputField: React.FC<InputFieldProps> = ({ id, label, type = 'text', value, onChange, error, }) => { const hasError = !!error; // Kiểm tra xem có lỗi không return ( <div> <label htmlFor={id}>{label}</label> <input type={type} id={id} value={value} onChange={onChange} aria-invalid={hasError ? 'true' : undefined} // Sử dụng aria-invalid aria-describedby={hasError ? `${id}-error` : undefined} // Liên kết với ID lỗi // ... các thuộc tính khác như name, required /> {/* Phần tử chứa thông báo lỗi, chỉ hiển thị khi có lỗi */} {hasError && ( <div id={`${id}-error`} style={{ color: 'red', fontSize: '0.9em' }}> {error} </div> )} </div> ); }; // Cách sử dụng const MyForm: React.FC = () => { const [email, setEmail] = useState(''); const [emailError, setEmailError] = useState(''); const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => { setEmail(e.target.value); // Logic validation đơn giản if (e.target.value && !e.target.value.includes('@')) { setEmailError('Email phải chứa ký tự "@"'); } else { setEmailError(''); } }; return ( <form> <InputField id="email" label="Địa chỉ Email" type="email" value={email} onChange={handleEmailChange} error={emailError} // Truyền thông báo lỗi vào component InputField /> {/* Các trường khác */} <button type="submit">Gửi</button> </form> ); };
- Giải thích code:
- Chúng ta tạo một component
InputField
có thể tái sử dụng. Component này nhận thêm properror
. - Thuộc tính
aria-invalid
được đặt là'true'
khi properror
tồn tại. Nếu không có lỗi, chúng ta không đặt thuộc tính này (hoặc đặt làundefined
) để trình duyệt không thêm nó vào DOM. - Thuộc tính
aria-describedby
được đặt bằngid
của trường nhập cộng thêm-error
(ví dụ:email-error
) khi có lỗi. - Một thẻ
div
(hoặcp
) chứa thông báo lỗi được tạo ngay sau trường nhập, và bắt buộc phải cóid
khớp với giá trị củaaria-describedby
.div
này chỉ hiển thị khi có lỗi (hasError
). - Với setup này, khi người dùng focus vào trường Email và có lỗi, trình đọc màn hình sẽ đọc: "Địa chỉ Email, trường nhập văn bản, invalid entry, Email phải chứa ký tự "@"".
- Chúng ta tạo một component
Chỉ Định Trường Bắt Buộc (Required Fields)
Việc cho người dùng biết trường nào là bắt buộc là rất quan trọng.
HTML Attribute: Sử dụng thuộc tính
required
trên thẻ<input>
,<textarea>
,<select>
. Trình duyệt sẽ xử lý việc kiểm tra trống và hiển thị thông báo mặc định khi cố gắng submit form.<input type="text" id="name" name="name" required />
ARIA Attribute: Sử dụng
aria-required="true"
trên trường nhập. Thuộc tính này thông báo cho trình đọc màn hình rằng trường này là bắt buộc. Mặc dù thuộc tính HTMLrequired
thường đủ để trình đọc màn hình nhận diện, sử dụngaria-required="true"
là một cách khẳng định rõ ràng và đôi khi cần thiết cho các custom component.<input type="text" id="name" name="name" aria-required="true" />
(Lưu ý: Bạn có thể sử dụng cả
required
vàaria-required="true"
cùng lúc.)Visual Indication: Thêm ký hiệu trực quan, phổ biến nhất là dấu hoa thị (
*
), bên cạnh label của trường bắt buộc. Quan trọng: Bạn cần cung cấp một lời giải thích ở đâu đó (ví dụ: ở đầu form) rằng các trường được đánh dấu (*
) là bắt buộc. Chỉ mỗi dấu*
có thể không rõ ràng cho tất cả mọi người. Hoặc bạn có thể thêm trực tiếp "(bắt buộc)" vào văn bản của label.<label htmlFor="name">Tên của bạn *</label> <input type="text" id="name" name="name" required aria-required="true" /> {/* Và ở đầu form: <p>* Trường bắt buộc</p> */}
TypeScript Mang Lại Điều Gì Cho Accessible Forms?
Như đã nói ở trên, TypeScript không tự động tạo ra các form accessible. Tuy nhiên, nó giúp chúng ta ngăn ngừa lỗi có thể làm hỏng accessibility và củng cố cấu trúc code của chúng ta.
Buộc Sử Dụng Props Quan Trọng: Khi tạo các component input có thể tái sử dụng, TypeScript cho phép bạn định nghĩa
interface
hoặctype
yêu cầu các prop accessibility quan trọng phải có mặt. Ví dụ:interface AccessibleInputProps { id: string; // Bắt buộc phải có ID để liên kết với label label: string; // Bắt buộc phải có label text htmlFor: string; // Buộc label phải trỏ đến ID // ... other input props error?: string; // Opional error message 'aria-describedby'?: string; // Optional ARIA attribute 'aria-invalid'?: boolean; // Optional ARIA attribute 'aria-required'?: boolean; // Optional ARIA attribute } const MyAccessibleInput: React.FC<AccessibleInputProps> = (props) => { // ... implementation using props.id, props.label, props.htmlFor etc. return ( <div> <label htmlFor={props.htmlFor}>{props.label}</label> <input id={props.id} // ... other input attributes aria-describedby={props['aria-describedby']} aria-invalid={props['aria-invalid']} aria-required={props['aria-required']} /> {/* Error message area */} </div> ); };
- Giải thích code: Bằng cách định nghĩa
AccessibleInputProps
, TypeScript sẽ báo lỗi nếu bạn cố gắng sử dụngMyAccessibleInput
mà không truyền đủ các propid
,label
,htmlFor
. Điều này giúp đảm bảo các liên kết label-input cơ bản luôn được thiết lập đúng.
- Giải thích code: Bằng cách định nghĩa
Kiểm Soát State và Logic Validation: TypeScript giúp bạn định nghĩa kiểu dữ liệu cho state của form và các hàm xử lý validation, giảm thiểu lỗi runtime khi làm việc với dữ liệu form, từ đó giúp logic hiển thị lỗi và cập nhật trạng thái ARIA chính xác hơn.
Tổng Kết Các Kỹ Thuật Chính
Để xây dựng Accessible forms trong React-TypeScript, hãy luôn nhớ:
- Sử dụng Semantic HTML (thẻ
<form>
,<label>
,<input>
, v.v.). - Luôn liên kết
<label>
với trường nhập bằnghtmlFor
vàid
. - Đảm bảo các trường nhập có thể được điều hướng và tương tác bằng bàn phím.
- Sử dụng ARIA attributes (
aria-invalid
,aria-describedby
,aria-required
, v.v.) để cung cấp thêm ngữ nghĩa cho công nghệ hỗ trợ, đặc biệt là khi xử lý lỗi và trường bắt buộc. - Hiển thị thông báo lỗi rõ ràng và liên kết chúng với trường nhập bằng
aria-describedby
. - Sử dụng thuộc tính
required
và/hoặcaria-required="true"
cho trường bắt buộc và cung cấp chỉ dẫn trực quan. - Leverage TypeScript để củng cố việc sử dụng đúng các props và quản lý state liên quan đến accessibility.
- Test! Sử dụng bàn phím, trình đọc màn hình (NVDA trên Windows, VoiceOver trên macOS, hoặc tiện ích mở rộng như Axe/Lighthouse trong trình duyệt) để kiểm tra form của bạn.
Xây dựng form accessible không chỉ là một kỹ thuật, mà là một tư duy khi phát triển giao diện người dùng. Bằng cách áp dụng các nguyên tắc và kỹ thuật trên trong các ứng dụng React và TypeScript của mình, bạn sẽ tạo ra những trải nghiệm tốt hơn, công bằng hơn cho mọi người dùng.
Comments