طراحی کامپوننتهای قابل دسترس: جمعبندی عملی فصل
در طول این فصل، تگهای معنایی، Label، دسترسپذیری صفحهکلید، مدیریت Focus و ARIA را جداگانه دیدیم. در این درس آخر، همه این اصول را در طراحی یک کامپوننت واقعی و کامل، یک Dropdown Menu ساده، کنار هم میگذاریم تا ببینیم چگونه با هم ترکیب میشوند.
مسئله: یک منوی کشویی ساده
یک Dropdown، دکمهای است که با کلیک، لیستی از گزینهها را باز میکند. از نظر بصری ساختن آن ساده است؛ اما نسخه کاملاً قابلدسترس آن، باید چند نیاز را همزمان برآورده کند که در ادامه قدمبهقدم اضافه میکنیم.
قدم اول: تگ معنایی درست برای دکمه بازکننده
function Dropdown({ label, options }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>
{label}
</button>
{isOpen && (
<ul>
{options.map((opt) => (
<li key={opt}>{opt}</li>
))}
</ul>
)}
</div>
);
}
همانطور که در درس اول فصل دیدیم، button بهجای div با onClick، و ul/li بهجای divهای پشتسرهم، پایه کار را بهطور رایگان از مرورگر میگیرد: Tab، فعالسازی با Enter، و اعلام «لیست» به صفحهخوان.
قدم دوم: اعلام وضعیت باز/بسته با ARIA
طبق چیزی که در درس قبل درباره aria-expanded دیدیم، باید وضعیت باز بودن منو را هم اعلام کنیم:
<button aria-expanded={isOpen} aria-haspopup="listbox" onClick={() => setIsOpen(!isOpen)}>
{label}
</button>
aria-haspopup="listbox" از قبل به صفحهخوان اعلام میکند این دکمه، یک لیست باز میکند؛ و aria-expanded، که مستقیم به همان State وصل است، وضعیت فعلی باز یا بسته بودن را نشان میدهد.
قدم سوم: بسته شدن با کلید Escape
یک کاربر صفحهکلید، انتظار دارد با فشردن Escape، منوی باز بسته شود؛ این رفتار بهطور خودکار وجود ندارد و باید دستی اضافه شود:
function Dropdown({ label, options }) {
const [isOpen, setIsOpen] = useState(false);
function handleKeyDown(e) {
if (e.key === "Escape") {
setIsOpen(false);
}
}
return (
<div onKeyDown={handleKeyDown}>
<button aria-expanded={isOpen} onClick={() => setIsOpen(!isOpen)}>
{label}
</button>
{isOpen && (
<ul>
{options.map((opt) => (
<li key={opt}>{opt}</li>
))}
</ul>
)}
</div>
);
}قدم چهارم: بازگرداندن Focus پس از بسته شدن
طبق همان الگویی که در درس چهارم این فصل برای Modal دیدیم، وقتی منو با Escape بسته میشود، فوکوس باید به همان دکمه برگردد، نه اینکه گم شود:
function Dropdown({ label, options }) {
const [isOpen, setIsOpen] = useState(false);
const buttonRef = useRef(null);
function handleKeyDown(e) {
if (e.key === "Escape") {
setIsOpen(false);
buttonRef.current.focus();
}
}
return (
<div onKeyDown={handleKeyDown}>
<button ref={buttonRef} aria-expanded={isOpen} onClick={() => setIsOpen(!isOpen)}>
{label}
</button>
{isOpen && (
<ul>
{options.map((opt) => (
<li key={opt}>{opt}</li>
))}
</ul>
)}
</div>
);
}یک اصل کلی که از این مثال بیرون میآید
هیچکدام از این قدمها، بهتنهایی کافی نبودند؛ دسترسپذیری واقعی، از ترکیب چند لایه ساده (تگ درست، ARIA درست، رفتار صفحهکلید درست، مدیریت Focus درست) به دست میآید، نه یک راهحل جادویی واحد. خبر خوب این است که در React، هرکدام از این لایهها، همان ابزارهایی هستند که در فصلهای قبل این دوره (State، Event Handler، Ref، Effect) یاد گرفتیم؛ دسترسپذیری، دانش جدیدی نیست، بلکه کاربرد همان دانش با توجه بیشتر به جزئیات است.
مثال قابل اجرا
import { useState, useRef } from "react";
function Dropdown({ label, options }) {
const [isOpen, setIsOpen] = useState(false);
const buttonRef = useRef(null);
function handleKeyDown(e) {
if (e.key === "Escape") {
setIsOpen(false);
buttonRef.current.focus();
}
}
return (
<div onKeyDown={handleKeyDown}>
<button
ref={buttonRef}
aria-expanded={isOpen}
aria-haspopup="listbox"
onClick={() => setIsOpen(!isOpen)}
>
{label}
</button>
{isOpen && (
<ul>
{options.map((opt) => (
<li key={opt}>{opt}</li>
))}
</ul>
)}
</div>
);
}
function App() {
return <Dropdown label="انتخاب شهر" options={["تهران", "مشهد", "اصفهان"]} />;
}جمعبندی
یک کامپوننت کاملاً قابلدسترس، نیازمند ترکیب چند لایه است: تگ معنایی درست، اعلام وضعیت با ARIA هماهنگ با State، رفتار صفحهکلید (مثل بستن با Escape)، و مدیریت آگاهانه Focus. هیچکدام از اینها بهتنهایی کافی نیست؛ دسترسپذیری، نتیجه دقت همزمان به همه این جزئیات است. با این درس، فصل هجدهم به پایان میرسد و در ادامه به آزمون فصل ۱۸ میرویم.
