دسترسپذیری با صفحهکلید
در دو درس قبل دیدیم تگهای معنایی و Label درست، چه کمکی به ابزارهای کمکی میکنند. اما گروه بزرگی از کاربران، اصلاً از ماوس استفاده نمیکنند؛ یا به دلیل ناتوانی حرکتی، یا به دلیل استفاده از صفحهخوان که ذاتاً با صفحهکلید کار میکند. این درس به این میپردازد که یک رابط کاربری React، چگونه باید بدون ماوس هم کاملاً قابلاستفاده باشد.
مفهوم پایه: ترتیب Tab
با فشردن کلید Tab، مرورگر فوکوس را بین عناصر «قابلفوکوس» صفحه جابهجا میکند: لینکها، دکمهها، فیلدهای فرم. همانطور که در درس اول این فصل دیدیم، یک div با onClick، بهطور پیشفرض در این مسیر قرار نمیگیرد؛ چون مرورگر آن را یک عنصر تعاملی نمیداند.
مشکل واقعی: عناصر تعاملی که Tab آنها را رد میکند
// کاربر صفحهکلید هرگز به این نمیرسد
<div onClick={handleDelete}>حذف</div>
حتی اگر این عنصر از نظر بصری کاملاً شبیه یک دکمه باشد، کاربری که فقط با Tab حرکت میکند، هرگز به آن نمیرسد؛ و اگر هم برسد (مثلاً با افزودن tabIndex)، فشردن Enter یا Space هنوز رویداد onClick را فعال نمیکند، چون این رفتار فقط برای عناصر واقعاً تعاملی داخلی مرورگر وجود دارد.
راهحل اول و بهترین: استفاده از تگ درست
همانطور که در درس اول این فصل گفتیم، بهترین راهحل، جایگزینی div با button یا a است؛ در آن صورت، Tab و فعالسازی با صفحهکلید، بدون هیچ کد اضافهای کار میکنند.
وقتی واقعاً مجبور به استفاده از div هستیم
در موقعیتهای نادری که از نظر طراحی باید یک عنصر غیراستاندارد (مثل یک کارت کامل قابلکلیک) بسازیم، باید این رفتارهای ازدسترفته را دستی اضافه کنیم:
function ClickableCard({ onActivate, children }) {
function handleKeyDown(e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onActivate();
}
}
return (
<div
role="button"
tabIndex={0}
onClick={onActivate}
onKeyDown={handleKeyDown}
>
{children}
</div>
);
}
در این کد، tabIndex={0} عنصر را به مسیر Tab اضافه میکند، role="button" به صفحهخوان اعلام میکند نقش آن چیست، و onKeyDown دستی، همان رفتار فعالسازی با Enter/Space را شبیهسازی میکند. با این حال، این همیشه راهحل دوم است؛ اگر button واقعی جواب میدهد، باید همان را ترجیح داد.
مدیریت Focus پس از یک تغییر مهم
یک جنبه دیگر دسترسپذیری صفحهکلید، این است که پس از یک تغییر مهم در رابط کاربری (مثل باز شدن یک Modal)، فوکوس باید آگاهانه به مکان جدید منتقل شود؛ وگرنه کاربر صفحهکلید، گم میشود و نمیداند الان کجای صفحه است. این دقیقاً همان کاربرد useRef و useEffect است که در فصل هفتم دیدیم:
function Modal({ isOpen }) {
const closeButtonRef = useRef(null);
useEffect(() => {
if (isOpen) {
closeButtonRef.current.focus();
}
}, [isOpen]);
return (
<div role="dialog">
<button ref={closeButtonRef}>بستن</button>
</div>
);
}نباید outline فوکوس را بدون جایگزین حذف کرد
یک اشتباه رایج در استایلدهی، حذف کامل حلقه آبی پیشفرض دور عنصر فوکوسشده (outline: none) فقط به این دلیل که از نظر طراحی زیبا نیست. بدون این نشانه بصری، کاربر صفحهکلید هیچوقت نمیفهمد الان فوکوس روی کدام عنصر است. اگر این استایل پیشفرض واقعاً باید عوض شود، باید با یک استایل جایگزین و قابلمشاهده (نه حذف کامل) جایگزین شود.
مثال قابل اجرا
import { useState } from "react";
function ClickableCard({ onActivate, children }) {
function handleKeyDown(e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onActivate();
}
}
return (
<div
role="button"
tabIndex={0}
onClick={onActivate}
onKeyDown={handleKeyDown}
style={{ border: "1px solid gray", padding: "10px", cursor: "pointer" }}
>
{children}
</div>
);
}
function App() {
const [count, setCount] = useState(0);
return (
<div>
<ClickableCard onActivate={() => setCount(count + 1)}>
روی من کلیک کن یا با Tab بیا و Enter بزن
</ClickableCard>
<p>تعداد فعالسازی: {count}</p>
</div>
);
}جمعبندی
عناصر تعاملی باید در مسیر Tab قرار بگیرند و با Enter/Space قابل فعالسازی باشند؛ بهترین راه رسیدن به این رفتار، استفاده از تگهای استاندارد مثل button است، و فقط در موارد خاص باید tabIndex و onKeyDown دستی اضافه شوند. فوکوس باید پس از تغییرات مهم رابط کاربری آگاهانه مدیریت شود و هرگز نباید بدون جایگزین حذف شود. در درس بعدی، به مدیریت Focus با جزئیات بیشتر میپردازیم.
