کادر گفتگو

نمای کلی

از عنصر بومی <dialog> برای کادرهای گفتگوی مدال و غیرمدال استفاده کنید. این عنصر معناشناسی دسترس‌پذیری، مدیریت تمرکز، رندرینگ لایه بالایی، و مدیریت Esc را ارائه می‌دهد که هم‌پوشانی‌های سفارشی باید به‌صورت دستی بازسازی کنند.

نوع هم‌پوشانی را انتخاب کنید

اولین قانون منطبق را انتخاب کنید. اگر مطمئن نیستید، از «قانون ۱» استفاده کنید.

قانون ۱: هم‌پوشانی باعث وقفه در کار کاربر می‌شود و صفحه را مسدود می‌کند (پیش‌فرض)

از این دکمه برای تأیید، فرم، ورود به سیستم، کنش‌های مخرب، و هر درخواست «مدال»، «کادر گفتگو»، «لایت‌باکس»، یا «بالاپر» که خلاف آن را بیان نمی‌کند استفاده کنید.

از <dialog> که به‌عنوان مدال باز شده است، به‌صورت اعلانی با command="show-modal" (به کنترل پاپ‌اور و کادر گفتگوی اعلانی مراجعه کنید) یا از اسکریپت با showModal() استفاده کنید.

<button type="button" command="show-modal" commandfor="confirm">Delete</button>

<!-- MANDATORY: no role="dialog" or aria-modal; the native modal already provides them -->
<dialog id="confirm" aria-labelledby="confirm-title">
  <h2 id="confirm-title">Delete this item?</h2>
  <form method="dialog">
    <button value="cancel" autofocus>Cancel</button>
    <button value="delete">Delete</button>
  </form>
</dialog>

مرورگر این موارد را ارائه می‌دهد: صفحه بی‌اثر، مهار تمرکز، لایه بالایی، ::backdrop، Esc و رد کردن اشاره برگشت، و بازگشت تمرکز به فراخواننده.

  • role="dialog"، aria-modal، تله تمرکز، inert، یا گرداننده Esc را اضافه نکنید.
  • نباید آن را با show() یا مشخصه open باز کنید.
  • باید با CSS زیر، پیمایش صفحه را در پشت آن متوقف کنید. برای جزئیات، جلوگیری از پیمایش صفحه زیر کادر گفتگوی مقید را ببینید.
  • پیمایش را با overflow: hidden یا position: fixed در html یا body قفل نکنید.
/* MANDATORY (both rules): without them, scrolling over the backdrop or inside the dialog scrolls the page behind it */
dialog:modal {
  overscroll-behavior: contain; /* stops scroll gestures in the dialog chaining to the page */
}
dialog::backdrop {
  overflow: hidden; /* ::backdrop is not a scroll container by default, so it would ignore overscroll-behavior */
  overscroll-behavior: contain;
}

برای منوها یا نکته‌ابزارها نیست: قانون ۳ را ببینید.

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

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

از <dialog popover> برای پانل لایه بالایی با رد کردن بالاپر استفاده کنید (auto رد کردن آسان دارد، manual ندارد). فقط زمانی از dialog.show() استفاده کنید که پانل نباید در لایه بالایی باشد.

<button type="button" popovertarget="find">Find</button>
<dialog id="find" popover="manual" aria-labelledby="find-title">
  <h2 id="find-title">Find</h2>
  <input type="search" aria-label="Search">
  <button type="button" popovertarget="find" popovertargetaction="hide">Close</button>
</dialog>

صفحه تعاملی باقی می‌ماند و غیرفعال نمی‌شود.

  • باید دکمه بستن قابل‌مشاهده داشته باشد.
  • از showModal() که صفحه را مسدود می‌کند استفاده نکنید.

نه برای مسدود کردن تأییدیه‌ها: قانون ۱ را ببینید.

قانون ۳: رونهاد کادر گفتگو نیست

از آن برای منوها، منوهای کرکره‌ای، نکته‌ابزارها، کارت‌های شناور، و پیام‌های موقت استفاده کنید.

از عنصر معنایی با مشخصه popover استفاده کنید، نه <dialog>.

<button type="button" popovertarget="menu">Actions</button>
<ul id="menu" popover>
  <li><a href="/rename">Rename</a></li>
</ul>

انتخاب نحوه بستن آن

پیش‌فرض به نحوه باز شدن کادر گفتگو بستگی دارد. اگر closedby وجود نداشته باشد یا نامعتبر باشد، showModal() مانند closerequest رفتار می‌کند و هر روش دیگری برای باز کردن مانند none رفتار می‌کند.

  • فقط کلید Esc و اشاره برگشت (پیش‌فرض مدال): هیچ‌چیز تنظیم نکنید، یا closedby="closerequest".
  • همچنین برای بستن، بیرون کلیک کنید: از closedby="any" استفاده کنید.
  • فقط توسعه‌دهنده آن را می‌بندد (برای مثال، فرمی با تغییرات ذخیره‌نشده): از closedby="none" استفاده کنید و مسیر تأیید اضافه کنید. بخش «بستن» را در زیر ببینید.

رهنمودها

نام دسترس‌پذیر و تمرکز اول

  • باید هر کادر گفتگویی را با aria-labelledby که به عنوان آن اشاره می‌کند یا aria-label وقتی عنوان نمایانی وجود ندارد نام‌گذاری کنید.
  • باید autofocus را روی کنترل اصلی یا فیلد اول قرار دهید.
  • باید autofocus را روی کنش ایمن (مثل «لغو» یا «نگه داشتن») در تأیید مخرب قرار دهید.
  • نباید کدی بنویسید که تمرکز را هنگام بسته شدن برگرداند. مرورگر آن را به فراخواننده برمی‌گرداند.

در حال باز کردن

  • باید به‌صورت اعلانی با command="show-modal" و commandfor در <button> باز شود. Declarative Dialog and Popover Control را ببینید.
  • باید از dialog.showModal() هنگام باز کردن از دستورگان استفاده کنید.

در حال بستن

  • باید با دکمه‌های <form method="dialog"> یا command="close" بسته شود.
  • باید closedby را آگاهانه انتخاب کنید: any (بستن با ضربه زدن در خارج از کادر و Esc)، closerequest (فقط Esc، پیش‌فرض مدال) یا none (فقط بستن تحت کنترل توسعه‌دهنده).
  • برای دکمه‌های «لغو» که باید از منطق تغییرات ذخیره‌نشده پیروی کنند، باید از command="request-close" یا requestClose() استفاده کنید. آن‌ها ابتدا cancel را اجرا می‌کنند و closedby را نادیده می‌گیرند.
  • باید با closedby به‌عنوان ارتقای پیش‌رو رفتار کنید. Esc و اشاره برگشت در تلفن همراه ازقبل بدون آن مدال‌ها را می‌بندند.

درحال اتصال به نزدیک

  • باید dialog.returnValue را در شنونده close بخوانید تا بدانید از کدام دکمه استفاده شده است، سپس آن را با dialog.returnValue = '' بازنشانی کنید. Esc و دیگر درخواست‌های بستن هیچ مقداری را تنظیم نمی‌کنند و مرورگرها در اینکه آیا مقدار قبلی را پاک می‌کنند یا نه متفاوت هستند، بنابراین بدون بازنشانی، مقدار قدیمی می‌تواند نشت کند. با '' به‌عنوان «بسته‌شده بدون انتخاب» رفتار شود.
  • باید از cancel برای رهگیری Esc و request-close استفاده کنید، اما برای فشردن مکرر Esc بدون فعال‌سازی کاربر به preventDefault() اتکا نکنید.
  • باید از closedby="none" با مرحله تأیید صریح برای فرم‌های کثیف استفاده کنید.
  • باید به کاربر بگویید چرا وقتی از cancel جلوگیری می‌شود کادر گفت‌وگو باز می‌ماند: پیام قابل‌مشاهده‌ای نشان دهید و آن را با aria-describedby از کنترلی که فوکوس را دریافت می‌کند (مثل «رد کردن») ارجاع دهید تا وقتی فوکوس جابه‌جا می‌شود صفحه‌خوان آن را بخواند. بدون آن، وقتی تمرکز به دکمه جدیدی منتقل می‌شود، فقط نام آن دکمه اعلام می‌شود.
  • اجازه ندهید تأیید، کنترل‌ها را به زیر اشاره‌گر منتقل کند. فضای پیام را رزرو کنید (آن را با visibility پنهان کنید، نه با display یا hidden) و دکمه مخرب را در جایی اضافه کنید که دکمه‌های موجود جابه‌جا نشوند (برای مثال، اولین دکمه در ردیف راست‌چین‌شده)، تا کلیک دوم در همان نقطه نتواند تغییرات کاربر را لغو کند.

کادرهای گفتگوی غیرمُدال

  • باید از <dialog popover> (auto یا manual) برای پانل لایه بالایی استفاده کنید و از dialog.show() زمانی که لایه بالایی را نمی‌خواهید.
  • باید در هر کادر گفتگوی غیرمدال، دکمه بستن نمایان اضافه کنید.

پساویز و سبک‌بندی

  • DO style ::backdrop and use dialog:modal to target modal-only styles.
  • باید پویانمایی را به Animate Elements To and From Top Layer، کشیدن را به Build a Draggable Element، و بازوالدین‌سازی را به Persistent Top Layer UI بسپارید.
  • display را بدون قیدوشرط روی dialog تنظیم نکنید. این گزینه وضعیت بسته را لغو می‌کند. با dialog[open] آن را بررسی کنید.

دسترس‌پذیری

  • role="dialog"، aria-modal، تله فوکوس دستی، یا inert تغییر وضعیت به مدال بومی را اضافه نکنید.
  • باید از <button> بومی با نام دسترس‌پذیر برای کنترل‌های بستن استفاده کنید.
  • باید از یک سطح عنوان یکسان برای عنوان‌های کادر گفتگو استفاده کنید.

راهبردهای جایگزین

‫closedby‏ (dialog-closedby)

بدون closedby، پنجره مانا همچنان با Esc و اشاره بازگشت بسته می‌شود؛ فقط کلیک کردن در بیرون از پنجره ازبین می‌رود، بنابراین این تنزل متین است. closedby="none" هم به همین روش نادیده گرفته می‌شود: Esc cancel را فعال می‌کند و سپس کادر گفتگو را می‌بندد. محافظ تغییرات ذخیره‌نشده را در گرداننده cancel نگه دارید، نه فقط در closedby="none"، تا از جاهایی که closedby وجود ندارد نیز محافظت کند. اگر هدف «خط مبنا» شما به رد کردن ساده نیاز دارد، این را در جایی که closedby وجود ندارد اضافه کنید:

if (!('closedBy' in HTMLDialogElement.prototype)) {
  // Test the pointer position, not event.target: the <dialog> is also the target for its own
  // padding and the gaps between its children, so a target check would close on those clicks
  const outside = (e, dialog) => {
    const r = dialog.getBoundingClientRect();
    return e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom;
  };
  for (const dialog of document.querySelectorAll('dialog[closedby="any"]')) {
    let pressedOutside = false;
    dialog.addEventListener('pointerdown', (e) => { pressedOutside = outside(e, dialog); });
    // Require the press and the release outside, so a text-selection drag that ends on the backdrop does not dismiss
    dialog.addEventListener('pointerup', (e) => { if (pressedOutside && outside(e, dialog)) dialog.close(); });
  }
}

overscroll-behavior

Treat scroll containment as progressive enhancement. آن را با ویژگی تشخیص ندهید: @supports این ویژگی را به‌عنوان پشتیبانی‌شده گزارش می‌کند، حتی در جایی که محفظه‌های غیرقابل‌پیمایش آن را نادیده می‌گیرند. در مواردی که این ویژگی اعمال نمی‌شود، کادر گفتگو همچنان حاوی پیمایش است، هر زمان که محتوای خودش سرریز شود، و showModal() همچنان صفحه را غیرفعال می‌کند. Do not fall back to overflow: hidden or position: fixed on html or body, which removes the root scrollbar and shifts the layout.

فرمان‌های فراخواننده (command / commandfor)

اگر هدف «خط پایه» شما شامل «فرمان‌های فراخواننده» نمی‌شود، همان نشانه‌گذاری را حفظ کنید و شبیه‌ساز مرورگر را به‌صورت شرطی بار کنید (همان تجربه، به‌عنوان ارتقای پیش‌رو). در واسط شناسایی کنید و هرگز آن را بدون قیدوشرط بار نکنید:

// MANDATORY: feature detect on HTMLButtonElement.prototype and load only when native support is missing
if (!('commandForElement' in HTMLButtonElement.prototype)) {
  import('invokers-polyfill'); // npm install invokers-polyfill
}

فقط قانون ۲ و ۳ از Popover استفاده می‌کنند.

پشتیبانی مرورگر و بازگشت به حالت قبلی برای Popover

«میانای برنامه‌سازی کاربردی Popover» عمدتاً ارتقای پیش‌رو است، اما رفتارهای تعریف‌کننده آن (ارتقای لایه بالایی، رد کردن با کلیک در خارج از عنصر، و فراخوانی popovertarget) معادل CSS-only ندارند. مرورگرهای قدیمی‌تر به شبیه‌ساز مرورگر نیاز دارند، یا اگر ترجیح می‌دهید شبیه‌ساز مرورگر ارسال نکنید، به بازگشت دستی نیاز دارند.

شبیه‌ساز مرورگر: برای پشتیبانی از مشخصه popover در مرورگرهای قدیمی‌تر، @oddbird/popover-polyfill را به‌صورت شرطی بار کنید. اجباری: ویژگی را با بررسی کردن ویژگی popover در HTMLElement.prototype شناسایی کنید و شبیه‌ساز مرورگر را فقط زمانی بار کنید که پشتیبانی بومی وجود ندارد — آن را بدون قیدوشرط بار نکنید.

با بسته‌بندی یا نقشه واردات:

// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
  import("@oddbird/popover-polyfill");
}

بدون بسته‌بندی، از CDN داخل <script type="module"> وارد کنید:

<script type="module">
  if (!("popover" in HTMLElement.prototype)) {
    import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
  }
</script>

هشدار سبک‌بندی: شبیه‌ساز مرورگر نمی‌تواند کلاس کاذب واقعی :popover-open را تعریف کند، بنابراین به‌جای آن کلاس .\:popover-open را اعمال می‌کند. اجباری: این دو را با :is() یا :where() ترکیب کنید، درغیراین‌صورت مرورگرهایی که فاقد :popover-open هستند کل قانون را نادیده می‌گیرند:

[popover]:is(:popover-open, .\:popover-open) {
  display: block;
}

یا برای بازگشت به نسخه قدیمی بدون شبیه‌ساز مرورگر، از position: fixed استفاده کنید و مختصات را به‌صورت دستی ازطریق getBoundingClientRect() محاسبه کنید یا اگر برای مورد استفاده قابل‌قبول است، به موقعیت‌یابی پیش‌فرض با inset: auto تکیه کنید.