Declarative Dialog and Popover Control

תכונות באינטרנט: פקודות Invoker Popover <dialog>

אתם יכולים להשתמש ב-Invoker Commands API כדי להחליף את מצב החשיפה של רכיבי <dialog> ו-[popover] ישירות מלחצני HTML, בלי צורך ברכיבי event listener מותאמים אישית של JavaScript.

כשמחילים את המאפיינים commandfor (מזהה היעד) ו-command (פעולה) על <button>, הדפדפן מטפל באופן אוטומטי בשינויים במצב הפתיחה/הסגירה, בניהול המיקוד ובקישורי הנגישות (כמו aria-expanded). הגישה ההצהרתית הזו מומלצת כי היא מסירה קוד שחוזר על עצמו (boilerplate) ועלול לגרום לשגיאות, מבטיחה שהאינטראקציות יפעלו מיד אחרי ניתוח ה-HTML ומבטיחה חוויית משתמש חזקה ונגישה באופן מובנה.

הטמעת חלונות קופצים הצהרתיים

אפשר לפתוח ולסגור חלונות קופצים באמצעות כפתור אחד.

<!-- MANDATORY: The commandfor attribute links the invoker to the ID of the target element so the browser knows what to control. -->
<!-- MANDATORY: The command attribute specifies the action to perform. Use 'toggle-popover' to handle both open and close states automatically. -->
<button commandfor="my-popover" command="toggle-popover">
  Toggle Popover
</button>

<!-- MANDATORY: The target element must have the popover attribute to be controlled as a popover. -->
<div id="my-popover" popover>
  <p>Popover content goes here.</p>
</div>

אם אתם צריכים לשלוט בפתיחה ובסגירה באמצעות כפתורים נפרדים, אתם יכולים להשתמש בפקודות show-popover ו-hide-popover.

<!-- MANDATORY: Use 'show-popover' to explicitly open the popover. It will not close the popover if clicked again. -->
<button commandfor="my-explicit-popover" command="show-popover">
  Show Popover
</button>

<div id="my-explicit-popover" popover="manual">
  <p>This popover is explicitly opened and closed by separate buttons.</p>

  <!-- MANDATORY: Use 'hide-popover' to explicitly close the targeted popover. -->
  <button commandfor="my-explicit-popover" command="hide-popover">
    Hide Popover
  </button>
</div>

הטמעה של תיבות דו-שיח מודאליות הצהרתיות

בניגוד לחלונות קופצים, בתיבות דו-שיח מודאליות בדרך כלל יש כפתורים נפרדים לפתיחה ולסגירה. השתמשו בפקודה show-modal במיוחד כשאתם צריכים לפתוח תיבת דו-שיח כמודאלית.

<!-- MANDATORY: Use command="show-modal" to trigger the dialog as a modal, trapping focus and preventing interaction with the rest of the page. -->
<!-- MANDATORY: The commandfor attribute connects this button to the dialog ID. -->
<button commandfor="confirm-dialog" command="show-modal">
  Open Confirmation
</button>

<dialog id="confirm-dialog">
  <p>Are you sure you want to proceed?</p>

  <!-- MANDATORY: Use command="close" to dismiss the dialog safely. -->
  <button commandfor="confirm-dialog" command="close">
    Cancel
  </button>
</dialog>

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

מכיוון שעדיין אין תמיכה אוניברסלית בפקודות של Invoker ובחלונות קופצים, אתם חייבים להשתמש ב-polyfills כחלופות לדפדפנים ישנים יותר.

הוספת פוליפיל לפקודות של Invoker

חובה: צריך לזהות את התמיכה בתכונה על ידי בדיקת המאפיין commandForElement באב הטיפוס HTMLButtonElement. אל תבדקו את האובייקט window או document. חובה לייבא באופן דינמי את ה-polyfill רק כשחסרה התכונה המקורית. אל תטענו את ה-polyfill ללא תנאי. חובה: צריך להאזין לאירוע command ישירות ברכיב היעד כי אירוע command המקורי לא מבעבע.

אפשרות 1: שימוש בכלי לאריזת קוד מתקינים את ה-polyfill דרך npm (npm install invokers-polyfill). הגישה הזו מתאימה לפרויקטים שמשתמשים בכלי לאריזת קוד (כמו Vite או Webpack) או במפות ייבוא. בכל שאר ההגדרות, צריך להשתמש באפשרות CDN שבהמשך.

// MANDATORY: Feature detect 'commandForElement' on HTMLButtonElement.prototype.
// Conditionally load the invokers-polyfill only in browsers lacking native support.
if (!('commandForElement' in HTMLButtonElement.prototype)) {
  import('invokers-polyfill');
}

אפשרות 2: שימוש ב-CDN בפרויקטים בלי כלי לאיסוף חבילות, מייבאים באופן דינמי את ה-polyfill ישירות מ-CDN בתוך <script type="module">.

<script type="module">
  // MANDATORY: Feature detect 'commandForElement' on HTMLButtonElement.prototype.
  // Conditionally load the invokers-polyfill from a CDN only in browsers lacking native support.
  if (!('commandForElement' in HTMLButtonElement.prototype)) {
    import('https://esm.run/invokers-polyfill');
  }
</script>

מגבלות של Invokers Polyfill חובה: ה-polyfill הזה לא מטפל במצבי ARIA (למשל, aria-expanded) של לחצן הפקודה כמו דפדפנים מקוריים. מומלץ מאוד לטפל במצבים האלה בעצמך כדי לוודא שהאתר שלך נגיש לחלוטין.

אם ה-Invoker Commands API לא נתמך, האירוע command לא יופעל. כדי לקבל תמיכה מלאה בכל הדפדפנים המודרניים, מומלץ להשתמש ב-invokers-polyfill מ-https://github.com/keithamus/invokers-polyfill דרך npm install או CDN.

ה-polyfill הזה תומך באופן מלא בפעולות מותאמות אישית (החל מ---) ושולח את האירוע command בדיוק כמו ה-API המקורי.

ייבוא דינמי (אופטימיזציה של הביצועים)

כדי לשפר את הביצועים, כדאי לטעון את ה-polyfill רק אם הדפדפן לא תומך ב-API באופן מקורי. כך אפשר לחסוך ברוחב הפס ולקצר את זמן ההפעלה של הסקריפט למשתמשים בדפדפנים מודרניים.

הערה: ה-polyfill הזה לא מנהל מצבי ARIA (כמו aria-pressed או aria-expanded) לפקודות בהתאמה אישית. כדי לוודא שהאתר נגיש, צריך לסנכרן את המצבים האלה באופן ידני ברכיב ה-event listener.

// 1. Conditionally load the polyfill
const hasNativeSupport = 'commandForElement' in HTMLButtonElement.prototype;

if (!hasNativeSupport) {
  // Wrap in an async IIFE to avoid top-level await issues in older browsers
  (async () => {
    try {
      await import('https://esm.run/invokers-polyfill');
    } catch (err) {
      console.error('Error loading fallback:', err);
    }
  })();
}

// 2. Manually manage ARIA states in your listener
document.getElementById('action-target').addEventListener('command', (event) => {
  const command = event.command;
  const target = event.target;
  const source = event.source; // The button that triggered the command

  if (command === '--spin') {
    const isSpun = target.classList.toggle('is-spun');
    
    // Polyfill tip: Manually update ARIA to match the new state
    source?.setAttribute('aria-pressed', isSpun);
  }
});

גיבוי ידני (תבנית רגילה)

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

// 1. **Optional:** Define a registry of requested actions for cleaner logic
const commandRegistry = {
  '--spin': (target) => target.classList.toggle('is-spun'),
  '--grow': (target) => target.classList.toggle('is-grown'),
  '--reset': (target) => target.classList.remove('is-spun', 'is-grown'),
};

// 2. If CommandEvent doesn't exist, we assume no native support and provide the fallback
if (!globalThis.CommandEvent) {
  globalThis.CommandEvent = class CommandEvent extends Event {
    constructor(type, { source, command, ...options } = {}) {
      super(type, options);
      this.source = source;
      this.command = command;
    }
  }
}

// 3. The fallback: Dispatch events manually if native support is missing
  document.addEventListener('click', (event) => {
    const button = event.composedPath().find((el) => el.matches?.("button[commandfor]"));
    if (!button) return;

    const target = document.getElementById(button.getAttribute('commandfor'));
    const command = button.getAttribute('command');

    if (target && command) {
      target.dispatchEvent(new CommandEvent('command', { 
        command, 
        source: button,
      }));
    }
  });

// 4. **Mandatory:** Register the unified listener directly on the target element
document.getElementById('action-target').addEventListener('command', (event) => {
  const command = event.command;
  const target = event.target;
  const action = commandRegistry[command];

  if (action) {
    action(target);
  }
 });

תמיכה בדפדפנים וחלופות ל-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 אם זה מתאים לתרחיש לדוגמה.