مدیریت Focus با جزئیات بیشتر

در درس قبل، به‌طور مختصر دیدیم که پس از باز شدن یک Modal، باید فوکوس را آگاهانه جابه‌جا کنیم. در این درس، مدیریت Focus را به‌عنوان یک موضوع مستقل، با چند الگوی رایج دیگر، بررسی می‌کنیم.

چرا مدیریت دستی Focus لازم است

مرورگر به‌طور پیش‌فرض فقط می‌داند چگونه فوکوس را بین عناصر از پیش موجود در صفحه جابه‌جا کند؛ اما وقتی React بخشی از رابط کاربری را پویا اضافه یا حذف می‌کند (مثل باز شدن یک Modal یا تغییر یک مرحله در فرم)، مرورگر به‌تنهایی نمی‌داند فوکوس باید به کجا برود. این تصمیم، باید آگاهانه در کد ما گرفته شود.

الگوی اول: انتقال Focus هنگام باز شدن یک Modal

jsx
function Modal({ isOpen, onClose }) {
  const dialogRef = useRef(null);

  useEffect(() => {
    if (isOpen) {
      dialogRef.current.focus();
    }
  }, [isOpen]);

  if (!isOpen) return null;

  return (
    <div role="dialog" ref={dialogRef} tabIndex={-1}>
      <p>محتوای Modal</p>
      <button onClick={onClose}>بستن</button>
    </div>
  );
}

نکته‌ای که اینجا اضافه شده، tabIndex={-1} روی خود div است؛ این مقدار به عنصر اجازه می‌دهد با جاوااسکریپت فوکوس بگیرد (focus())، بدون اینکه خودش را به مسیر عادی Tab کاربر اضافه کند.

الگوی دوم: بازگرداندن Focus پس از بسته شدن

یک جزئیات مهم که اغلب فراموش می‌شود: وقتی Modal بسته می‌شود، فوکوس باید به همان عنصری که Modal را باز کرده بود برگردد، نه اینکه به ابتدای صفحه یا به‌طور کلی گم شود:

jsx
function App() {
  const [isOpen, setIsOpen] = useState(false);
  const openButtonRef = useRef(null);

  function handleClose() {
    setIsOpen(false);
    openButtonRef.current.focus();
  }

  return (
    <div>
      <button ref={openButtonRef} onClick={() => setIsOpen(true)}>
        باز کردن Modal
      </button>
      <Modal isOpen={isOpen} onClose={handleClose} />
    </div>
  );
}

الگوی سوم: محبوس کردن Focus داخل یک Modal (Focus Trap)

وقتی یک Modal باز است، کاربر صفحه‌کلید نباید بتواند با Tab، به عناصر پشت Modal (که از نظر بصری هم معمولاً تیره یا غیرفعال شده‌اند) برود. این الگو، Focus Trap نام دارد: محدود کردن چرخه Tab فقط به داخل Modal، تا وقتی بسته شود. پیاده‌سازی دستی این رفتار، نیازمند گوش‌دادن به رویداد Tab و محاسبه اولین و آخرین عنصر قابل‌فوکوس است؛ به همین دلیل، در عمل اغلب از کتابخانه‌های تخصصی یا کامپوننت‌های آماده Modal (که این منطق را از قبل پیاده‌سازی کرده‌اند) استفاده می‌شود، نه نوشتن آن از صفر.

الگوی چهارم: اعلام تغییرات پویا به صفحه‌خوان

گاهی محتوایی بدون تغییر فوکوس، به‌صورت پویا در صفحه ظاهر می‌شود؛ مثلاً یک پیام «ذخیره شد» پس از ارسال فرم. چون فوکوس جابه‌جا نشده، صفحه‌خوان به‌طور پیش‌فرض متوجه این محتوای جدید نمی‌شود. برای این موارد، یک ناحیه aria-live استفاده می‌شود که هر تغییر محتوای داخلش را خودکار اعلام می‌کند:

jsx
<div aria-live="polite">{status}</div>

با تغییر status (مثلاً از رشته خالی به «ذخیره شد»)، صفحه‌خوان بدون نیاز به فوکوس گرفتن، این متن جدید را برای کاربر می‌خواند.

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

مدیریت Focus هنگام باز و بسته شدن Modal
import { useState, useRef, useEffect } from "react";

function Modal({ isOpen, onClose }) {
  const dialogRef = useRef(null);

  useEffect(() => {
    if (isOpen) {
      dialogRef.current.focus();
    }
  }, [isOpen]);

  if (!isOpen) return null;

  return (
    <div role="dialog" ref={dialogRef} tabIndex={-1} style={{ border: "2px solid black", padding: "10px" }}>
      <p>محتوای Modal</p>
      <button onClick={onClose}>بستن</button>
    </div>
  );
}

function App() {
  const [isOpen, setIsOpen] = useState(false);
  const openButtonRef = useRef(null);

  function handleClose() {
    setIsOpen(false);
    openButtonRef.current.focus();
  }

  return (
    <div>
      <button ref={openButtonRef} onClick={() => setIsOpen(true)}>
        باز کردن Modal
      </button>
      <Modal isOpen={isOpen} onClose={handleClose} />
    </div>
  );
}
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

مدیریت آگاهانه Focus شامل چند بخش است: انتقال فوکوس به محتوای جدید هنگام باز شدن یک عنصر مثل Modal، بازگرداندن آن به عنصر اولیه هنگام بسته شدن، محبوس کردن Tab داخل یک Modal باز، و اعلام تغییرات پویا با aria-live برای محتوایی که فوکوس به آن منتقل نمی‌شود. در درس بعدی، به ARIA و نقش دقیق‌تر آن در دسترس‌پذیری می‌پردازیم.