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