דו שיח

סקירה כללית

משתמשים ברכיב <dialog> המקורי לתיבות דו-שיח מודאליות ולא מודאליות. הוא מספק את הסמנטיקה של הנגישות, הטיפול במיקוד, הרינדור של השכבה העליונה והטיפול ב-Esc ששכבות-על בהתאמה אישית צריכות לבנות מחדש באופן ידני.

בוחרים את סוג שכבת-העל

בוחרים את הכלל הראשון שתואם. אם אתם לא בטוחים, השתמשו בכלל 1.

כלל 1: שכבת-העל מפריעה למשתמש וחוסמת את הדף (ברירת מחדל)

השתמשו בזה לאישורים, טפסים, כניסה, פעולות הרסניות וכל בקשה מסוג "מודאלי", "תיבת דו-שיח", "תיבת lightbox" או "חלון קופץ" שלא מצוין בה אחרת.

משתמשים ב-<dialog> שנפתח כמודל, באופן הצהרתי עם command="show-modal" (ראו תיבת דו-שיח הצהרתית ופקד Popover) או מסקריפט עם 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;
}

לא לתפריטים או לתיאורי כלים: ראו כלל 3.

כלל 2: המשתמש ממשיך לעבוד בדף כשהחלונית פתוחה

משתמשים בו לסרגלי חיפוש, פקחים, פלטות וחלוניות כתיבה שעדיין נחשבים לתיבות דו-שיח מבחינה סמנטית.

כדי להשתמש בחלונית בשכבה העליונה עם סגירת חלון קופץ, צריך להשתמש ב-<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(), שחוסם את הדף.

לא לחסימת אישורים: ראו כלל 1.

כלל 3: שכבת-העל היא לא תיבת דו-שיח

משתמשים בהם לתפריטים, לתפריטים נפתחים, לטיפים, למידע בכרטיסים צפים ולהודעות קופצות.

צריך להשתמש ברכיב סמנטי עם המאפיין 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 כשיפור הדרגתי (progressive enhancement). Esc ותנועת החזרה בנייד כבר סוגרות מודלים בלעדיו.

התחברות לסגירה

  • מומלץ לקרוא את dialog.returnValue במאזין close כדי לדעת באיזה לחצן נעשה שימוש, ואז לאפס אותו באמצעות dialog.returnValue = ''. הערך של Esc ובקשות דומות אחרות לא מוגדר, והדפדפנים לא תמיד מנקים את הערך הקודם. לכן, בלי האיפוס, ערך לא עדכני עלול לדלוף. התייחסות אל '' כאל "נסגר בלי לבחור".
  • מומלץ להשתמש ב-cancel כדי ליירט את Esc ו-request-close, אבל לא להסתמך על preventDefault() ללחיצות חוזרות על Esc בלי פעילות משתמש.
  • מומלץ להשתמש ב-closedby="none" עם שלב אישור מפורש לטפסים עם שינויים שלא נשמרו.
  • מומלץ להסביר למשתמש למה תיבת הדו-שיח נשארה פתוחה כשנמנעת הפעולה cancel: מציגים הודעה גלויה ומפנים אליה באמצעות aria-describedby מהפקד שמקבל את המיקוד (כמו "מחיקה"), כדי שקורא המסך יקריא אותה כשהמיקוד עובר. בלי התכונה הזו, כשהמיקוד עובר ללחצן חדש, קורא המסך מקריא רק את השם של הלחצן.
  • אל תאפשרו לאישור להזיז את אמצעי הבקרה מתחת לסמן. שמרו את המקום של ההודעה (הסתירו אותה באמצעות visibility, לא באמצעות display או hidden) והוסיפו את הכפתור ההרסני במקום שבו הכפתורים הקיימים לא זזים (לדוגמה, ראשון בשורה שמיושרת לימין), כדי שקליק שני באותו מקום לא יבטל את השינויים של המשתמש.

תיבות דו-שיח לא מודאליות

  • מומלץ להשתמש ב-<dialog popover> (auto או manual) לחלונית בשכבה העליונה, וב-dialog.show() כשלא רוצים את השכבה העליונה.
  • חשוב לכלול לחצן סגירה גלוי בכל תיבת דו-שיח לא מודאלית.

רקע וסטיילינג

נגישות

  • אל תוסיפו role="dialog", aria-modal, מלכודת מיקוד ידנית או החלפה של inert למודל מקורי.
  • כן משתמשים ברכיב <button> מקורי עם תווית נגישות לפקדי סגירה.
  • כן משתמשים ברמת כותרת עקבית אחת לכותרות של תיבות דו-שיח.

אסטרטגיות חלופיות

‫closedby (dialog-closedby)

בלי closedby, עדיין אפשר לסגור את המודל באמצעות Esc ותנועת החזרה. רק הלחיצה מחוץ למודל לא עובדת, אז זו הפחתה חיננית (graceful degradation). המערכת מתעלמת מ-closedby="none" באותו אופן: Esc מפעיל את cancel ואז סוגר את תיבת הדו-שיח. כדאי להשאיר את ההגנה על שינויים שלא נשמרו ב-handler של 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

התייחסו להכלת הגלילה כשיפור הדרגתי (progressive enhancement). אל תזהו את התכונה: @supports מדווח על המאפיין כנתמך גם במקרים שבהם מאגרים שלא ניתנים לגלילה מתעלמים ממנו. במקרים שבהם המאפיין לא נאכף, תיבת הדו-שיח עדיין מכילה גלילה בכל פעם שהתוכן שלה חורג, והמאפיין showModal() עדיין הופך את הדף ללא פעיל. אל תשתמשו ב-overflow: hidden או ב-position: fixed כחלופה ב-html או ב-body, כי זה מסיר את סרגל הגלילה הבסיסי ומשנה את הפריסה.

פקודות להפעלה (command / commandfor)

אם יעד ה-Baseline שלכם לא כולל פקודות Invoker, אתם יכולים להשאיר את אותו תיוג ולהטעין את ה-polyfill באופן מותנה (אותה חוויה, כמו שיפור הדרגתי). מזהים בממשק ואף פעם לא טוענים אותו ללא תנאי:

// 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
}

רק כללים 2 ו-3 משתמשים ב-Popover.

תמיכה בדפדפנים וחלופות ל-Popover

‫Popover API הוא בעיקר שיפור הדרגתי (progressive enhancement), אבל להתנהגויות שמגדירות אותו – קידום לשכבה העליונה, סגירה בלחיצה מחוץ לאלמנט והפעלה של popovertarget – אין מקבילה ב-CSS בלבד. בדפדפנים ישנים יותר צריך להשתמש ב-polyfill, או להגדיר חלופה ידנית אם אתם מעדיפים לא להשתמש ב-polyfill.

Polyfill: כדי לתמוך במאפיין popover בדפדפנים ישנים יותר, צריך לטעון את @oddbird/popover-polyfill באופן מותנה. חובה: צריך לזהות את התכונה על ידי בדיקת המאפיין popover ב-HTMLElement.prototype, ולטעון את ה-polyfill רק כשאין תמיכה מקומית – אל תטענו אותו ללא תנאי.

עם bundler או מפת ייבוא:

// 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>

אזהרה לגבי סגנון: ה-polyfill לא יכול להגדיר את המחלקה המדומה :popover-open, אז הוא מחיל במקום זאת מחלקה .\:popover-open. חובה: צריך לשלב את שניהם עם :is() או :where(), אחרת דפדפנים שאין בהם :popover-open יתעלמו מהכלל כולו:

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

לחלופין, כדי להשתמש ב-fallback מדור קודם בלי polyfill, אפשר להשתמש ב-position: fixed ולחשב קואורדינטות באופן ידני באמצעות getBoundingClientRect() או להסתמך על מיקום ברירת המחדל עם inset: auto אם זה מתאים לתרחיש לדוגמה.