طراحی کامپوننت‌های قابل دسترس: جمع‌بندی عملی فصل

در طول این فصل، تگ‌های معنایی، Label، دسترس‌پذیری صفحه‌کلید، مدیریت Focus و ARIA را جداگانه دیدیم. در این درس آخر، همه این اصول را در طراحی یک کامپوننت واقعی و کامل، یک Dropdown Menu ساده، کنار هم می‌گذاریم تا ببینیم چگونه با هم ترکیب می‌شوند.

مسئله: یک منوی کشویی ساده

یک Dropdown، دکمه‌ای است که با کلیک، لیستی از گزینه‌ها را باز می‌کند. از نظر بصری ساختن آن ساده است؛ اما نسخه کاملاً قابل‌دسترس آن، باید چند نیاز را هم‌زمان برآورده کند که در ادامه قدم‌به‌قدم اضافه می‌کنیم.

قدم اول: تگ معنایی درست برای دکمه باز‌کننده

jsx
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 دیدیم، باید وضعیت باز بودن منو را هم اعلام کنیم:

jsx
<button aria-expanded={isOpen} aria-haspopup="listbox" onClick={() => setIsOpen(!isOpen)}>
  {label}
</button>

aria-haspopup="listbox" از قبل به صفحه‌خوان اعلام می‌کند این دکمه، یک لیست باز می‌کند؛ و aria-expanded، که مستقیم به همان State وصل است، وضعیت فعلی باز یا بسته بودن را نشان می‌دهد.

قدم سوم: بسته شدن با کلید Escape

یک کاربر صفحه‌کلید، انتظار دارد با فشردن Escape، منوی باز بسته شود؛ این رفتار به‌طور خودکار وجود ندارد و باید دستی اضافه شود:

jsx
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 بسته می‌شود، فوکوس باید به همان دکمه برگردد، نه این‌که گم شود:

jsx
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) یاد گرفتیم؛ دسترس‌پذیری، دانش جدیدی نیست، بلکه کاربرد همان دانش با توجه بیشتر به جزئیات است.

مثال قابل اجرا

یک Dropdown کامل با پشتیبانی دسترس‌پذیری
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={["تهران", "مشهد", "اصفهان"]} />;
}
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

یک کامپوننت کاملاً قابل‌دسترس، نیازمند ترکیب چند لایه است: تگ معنایی درست، اعلام وضعیت با ARIA هماهنگ با State، رفتار صفحه‌کلید (مثل بستن با Escape)، و مدیریت آگاهانه Focus. هیچ‌کدام از این‌ها به‌تنهایی کافی نیست؛ دسترس‌پذیری، نتیجه دقت هم‌زمان به همه این جزئیات است. با این درس، فصل هجدهم به پایان می‌رسد و در ادامه به آزمون فصل ۱۸ می‌رویم.