رفتار استاندارد به جای اتصال دستی تکراری

برای بازکردن یک پنجره راهنما معمولاً دکمه را با JavaScript پیدا می‌کنیم و به showModal وصل می‌کنیم. ویژگی‌های command و commandfor می‌توانند بخشی از این ارتباط را در خود HTML بیان کنند. این قابلیت برای رفتارهای استاندارد مانند بازکردن dialog مفید است، اما جای طراحی تعامل را نمی‌گیرد. مرورگرهای مخاطب و رفتار دقیق مورد نیاز باید پیش از حذف کد قبلی بررسی شوند.

در نمونه، commandfor به id پنجره اشاره می‌کند و show-modal دستور بازکردن است. دکمه type="button" دارد تا اگر بعداً داخل فرم قرار گرفت، ناخواسته submit نکند. عنوان با aria-labelledby به dialog متصل شده و بستن از مسیر فرم method="dialog" انجام می‌شود. این اجزا کوچک‌اند، ولی در کنار هم قرارداد قابل فهم‌تری از یک کلیک بی‌نام می‌سازند.

پشتیبانی مرورگر و راه جایگزین

پشتیبانی قابلیت‌های جدید را از روی ظاهر صفحه نتیجه نگیرید. تشخیص commandForElement در prototype دکمه کمک می‌کند فقط در نبود پشتیبانی، handler قدیمی را اضافه کنیم. افزودن هم‌زمان handler و رفتار بومی می‌تواند باعث اجرای تکراری شود. fallback نمونه هنوز به پشتیبانی HTML dialog و showModal نیاز دارد؛ برای مرورگرهای بسیار قدیمی‌تر باید راه ساده نمایش محتوا را جدا در محصول تعریف کنید.

dialog بومی بخشی از رفتار focus و modal را فراهم می‌کند، ولی محتوای داخل آن همچنان به آزمون نیاز دارد. با Tab میان کنترل‌ها حرکت کنید، با Escape ببندید و بازگشت focus به دکمه آغازگر را ببینید. روی موبایل با متن بلند و بزرگ‌نمایی آزمایش کنید. اگر پنجره اطلاعات لازم برای تصمیم را پنهان می‌کند یا دکمه بستن دیده نمی‌شود، انتخاب API تازه مشکل طراحی را حل نکرده است.

برای محتوای ساده، گاهی نمایش درون صفحه یا details از modal مناسب‌تر است. پنجره را فقط وقتی انتخاب کنید که قطع موقت جریان کار دلیل روشنی داشته باشد. اگر مرحله حساس مانند تأیید حذف داخل آن است، وضعیت انتظار، شکست درخواست و جلوگیری از ارسال دوباره باید جدا پیاده شود. مثال فقط راهنمای خواندنی است و هیچ درخواست شبکه یا اقدام مخربی انجام نمی‌دهد.

نمونه کد و روش بررسی

پیش‌نیاز و خروجی مورد انتظار در توضیح مثال آمده است. ابتدا نمونه را در محیط آزمایشی اجرا کنید و سپس آن را با داده و محدودیت‌های پروژه خود تطبیق دهید.

1. فایل HTML مستقل؛ مرورگر مدرن با fallback ساده

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Dialog command demo</title>
<button id="open-help" type="button"
        commandfor="help" command="show-modal">Open help</button>
<dialog id="help" aria-labelledby="help-title">
  <h2 id="help-title">Saving a draft</h2>
  <p>Save your draft before leaving the page.</p>
  <form method="dialog"><button>Close</button></form>
</dialog>
<script>
if (!("commandForElement" in HTMLButtonElement.prototype)) {
  document.querySelector("#open-help").addEventListener("click", () => {
    document.querySelector("#help").showModal();
  });
}
</script>
</html>

فایل را در مرورگر هدف باز کنید؛ دکمه باید پنجره را باز و Close یا Escape آن را ببندد. در مسیر fallback، مرورگر باید showModal را داشته باشد. این مثال inline script دارد و در سایت دارای CSP باید به فایل مجاز یا nonce هماهنگ منتقل شود.

2. فرم dialog با نام قابل دسترس و نتیجه مشخص

این نسخه فراتر از راهنمای خواندنی اول مقاله است و نتیجه فرم را تفکیک می‌کند. دکمه Cancel حتی با ورودی خالی باید کار کند، برای همین formnovalidate دارد. method=dialog نتیجه دکمه را به returnValue منتقل می‌کند و درخواست شبکه نمی‌فرستد.

<!-- dialog-demo.html; place beside dialog-ui.js. -->
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Command dialog exercise</title>
<button id="open" type="button" commandfor="editor" command="show-modal">
  Open form
</button>
<dialog id="editor" aria-labelledby="editor-title">
  <h1 id="editor-title">Choose a display name</h1>
  <form method="dialog">
    <label for="name">Display name</label>
    <input id="name" required maxlength="80">
    <button value="cancel" formnovalidate>Cancel</button>
    <button value="save">Use this name</button>
  </form>
</dialog>
<p id="result" role="status" aria-live="polite">No selection</p>
<script type="module" src="./dialog-ui.js"></script>
</html>

با فایل بعدی، نام انتخاب‌شده بیرون پنجره نمایش داده می‌شود. عنوان صفحه و عنوان dialog دو نقش متفاوت دارند. برچسب input نیز باید مستقل از placeholder قابل خواندن باشد؛ ناپدیدشدن placeholder بعد از تایپ نباید راهنمای کنترل را حذف کند.

3. fallback و رسیدگی به Cancel، Escape و انتخاب نام

این ماژول را کنار HTML بگذارید و پوشه را با python -m http.server 8000 سرو کنید. در مرورگر دارای commandfor، handler فقط نتیجه قبلی را reset می‌کند؛ showModal دوباره فراخوانی نمی‌شود. در مرورگر فاقد commandfor ولی دارای dialog، همان handler راه جایگزین است.

// dialog-ui.js
const opener = document.querySelector("#open");
const dialog = document.querySelector("#editor");
const name = document.querySelector("#name");
const result = document.querySelector("#result");
const nativeCommand = "commandForElement" in HTMLButtonElement.prototype;

opener.addEventListener("click", () => {
  dialog.returnValue = "cancel";
  if (!nativeCommand) dialog.showModal();
});
dialog.addEventListener("close", () => {
  result.textContent = dialog.returnValue === "save"
    ? "Selected: " + name.value
    : "Cancelled";
});

پنجره را با مقدار ذخیره‌شده ببندید، دوباره باز کنید و Escape بزنید؛ این بار باید Cancelled نمایش داده شود، نه نتیجه قبلی Save. متن با textContent نمایش داده می‌شود. این مثال نام را به سرور ارسال یا ذخیره دائمی نمی‌کند؛ چنین مرحله‌ای باید وضعیت موفقیت و خطای خودش را داشته باشد.

4. آزمون مرورگر برای بستن، بازگشت focus و ارسال فرم

در پوشه آزمایشی جدا، @playwright/test و مرورگر مورد نیاز آن را نصب کنید و درحالی‌که وب‌سرور محلی اجراست، npx playwright test dialog.spec.js را بزنید. تست‌ها با نام قابل دسترس کنترل‌ها را پیدا می‌کنند؛ تغییر متن محصول باید در آزمون هم بازتاب داشته باشد.

// dialog.spec.js; @playwright/test installed in a separate test folder.
import { test, expect } from "@playwright/test";

test("Escape cancels and restores focus", async ({ page }) => {
  await page.goto("http://127.0.0.1:8000/dialog-demo.html");
  const opener = page.getByRole("button", { name: "Open form" });
  await opener.click();
  await expect(page.getByRole("dialog")).toBeVisible();
  await page.keyboard.press("Escape");
  await expect(page.getByRole("dialog")).toBeHidden();
  await expect(opener).toBeFocused();
  await expect(page.getByRole("status")).toHaveText("Cancelled");
});

test("validated form returns the chosen name", async ({ page }) => {
  await page.goto("http://127.0.0.1:8000/dialog-demo.html");
  await page.getByRole("button", { name: "Open form" }).click();
  await page.getByLabel("Display name", { exact: true }).fill("Liyan");
  await page.getByRole("button", { name: "Use this name" }).click();
  await expect(page.getByRole("status")).toHaveText("Selected: Liyan");
});

هر دو تست باید موفق باشند. برای پوشش واقعی، آن‌ها را روی مرورگرهای هدف و مسیر fallback نیز اجرا کنید و Tab، متن بلند و بزرگ‌نمایی را دستی بررسی کنید. موفقیت در یک مرورگر، پشتیبانی همه مرورگرها از قابلیت جدید را ثابت نمی‌کند.

دسترسی‌پذیری با آزمون تعامل کامل می‌شود

قبل از انتشار، جدول مرورگرهای هدف را ثبت و مسیر بومی و fallback را جدا امتحان کنید. تست با صفحه‌کلید را کنار تست لمس و کلیک نگه دارید. برای دانش‌آموز، این نمونه تمرینی درباره progressive enhancement است: HTML رفتار پایه را توضیح می‌دهد و JavaScript تنها جایی اضافه می‌شود که نیاز مشخصی وجود دارد. خروجی خوب، کد کمتر همراه با رفتار قابل پیش‌بینی است.

چک‌لیست اجرای عملی

  • id هدف و commandfor دقیقاً یکسان باشند.
  • handler جایگزین فقط در نبود پشتیبانی اضافه شود.
  • Tab، Escape و بازگشت focus بررسی شوند.
  • fallback وابسته به dialog از پشتیبانی عمومی HTML جدا بررسی شود.

منابع

تدوین و راهنمای عملی: دانشنامه لیان.