Gunakan Invoker Commands API untuk mengalihkan visibilitas elemen <dialog> dan [popover] langsung dari tombol HTML, sehingga tidak memerlukan pemroses peristiwa JavaScript kustom.
Dengan menerapkan atribut commandfor (ID target) dan command (tindakan) ke <button>, browser akan otomatis menangani perubahan status buka/tutup, pengelolaan fokus, dan binding aksesibilitas (seperti aria-expanded). Pendekatan deklaratif ini direkomendasikan karena menghapus kode boilerplate yang rentan, memastikan interaksi langsung berfungsi setelah penguraian HTML, dan menjamin pengalaman pengguna yang andal dan dapat diakses secara native.
Menerapkan Popover Deklaratif
Popover dapat dibuka dan ditutup menggunakan satu tombol.
<!-- 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>
Jika Anda perlu mengontrol pembukaan dan penutupan dengan tombol terpisah, Anda dapat menggunakan perintah show-popover dan 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>
Menerapkan Dialog Modal Deklaratif
Tidak seperti popover, dialog modal biasanya menggunakan tombol terpisah untuk membuka dan menutup. Gunakan perintah show-modal secara khusus saat Anda perlu membuka dialog sebagai 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>
Strategi penggantian
Karena Perintah Invoker dan Popover belum didukung secara universal, Anda HARUS menggunakan polyfill sebagai pengganti untuk browser lama.
Polyfill Perintah Invoker
WAJIB: Deteksi dukungan fitur dengan memeriksa properti commandForElement pada prototipe HTMLButtonElement. JANGAN periksa objek window atau document. Anda HARUS mengimpor polyfill secara dinamis hanya jika fitur native tidak ada. JANGAN memuat polyfill tanpa syarat.
Wajib: Dengarkan peristiwa 'command' langsung di elemen target karena peristiwa 'command' native tidak menggelembung.
Opsi 1: Menggunakan pemaket
Instal polyfill melalui npm (npm install invokers-polyfill). Pendekatan ini ditujukan untuk project yang menggunakan pemaket (seperti Vite atau Webpack) atau peta impor. Untuk semua penyiapan lainnya, gunakan opsi CDN di bawah.
// 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');
}
Opsi 2: Menggunakan CDN
Untuk project tanpa pemaket, impor polyfill secara dinamis langsung dari CDN di dalam <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>
Batasan Polyfill Invoker
WAJIB: Polyfill ini tidak menangani status ARIA (misalnya, aria-expanded) tombol perintah seperti yang dilakukan browser native. Anda sangat dianjurkan untuk menangani status ini sendiri guna memastikan situs Anda dapat diakses sepenuhnya.
Jika Invoker Commands API tidak didukung, peristiwa command tidak akan diaktifkan. Untuk dukungan penuh di semua browser modern, sebaiknya gunakan invokers-polyfill dari https://github.com/keithamus/invokers-polyfill melalui npm install atau CDN.
Polyfill ini sepenuhnya mendukung tindakan kustom (dimulai dengan --) dan mengirimkan peristiwa command persis seperti API native.
Dynamic Import (Pengoptimalan Performa)
Untuk performa terbaik, Anda hanya boleh memuat polyfill jika browser tidak mendukung API secara native. Tindakan ini menghemat bandwidth dan mengurangi waktu eksekusi skrip bagi pengguna di browser modern.
CATATAN: Polyfill ini tidak mengelola status ARIA (seperti aria-pressed atau aria-expanded) untuk perintah kustom. Anda harus menyinkronkan status ini secara manual di pemroses peristiwa untuk memastikan situs Anda dapat diakses.
// 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);
}
});
Penggantian manual (Pola tradisional)
Jika tidak ingin menggunakan polyfill, Anda dapat menggunakan kombinasi pendelegasian peristiwa untuk mengirim peristiwa dan registry perintah untuk menangani tindakan. Ini adalah pola arsitektur umum dalam pengembangan JavaScript tradisional yang tetap sangat efisien dan skalabel.
// 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);
}
});
Penggantian & dukungan browser untuk Popover
Popover API sebagian besar merupakan progressive enhancement, tetapi perilaku penentuannya — promosi lapisan atas, penutupan ringan, dan pemanggilan popovertarget — tidak memiliki padanan khusus CSS. Browser lama memerlukan polyfill, atau penggantian manual jika Anda tidak ingin mengirimkannya.
Polyfill: Untuk mendukung atribut popover di browser lama, muat @oddbird/popover-polyfill secara bersyarat. WAJIB: Deteksi fitur dengan memeriksa properti popover di HTMLElement.prototype, dan muat polyfill hanya jika dukungan native tidak ada — JANGAN memuatnya tanpa syarat.
Dengan bundler atau peta impor:
// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
import("@oddbird/popover-polyfill");
}
Tanpa bundler, impor dari CDN di dalam <script type="module">:
<script type="module">
if (!("popover" in HTMLElement.prototype)) {
import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
}
</script>
Peringatan gaya: Polyfill tidak dapat menentukan pseudo-class :popover-open yang sebenarnya, sehingga menerapkan class .\:popover-open. WAJIB: Gabungkan keduanya dengan :is() atau :where(). Jika tidak, browser yang tidak memiliki :popover-open akan menghapus seluruh aturan:
[popover]:is(:popover-open, .\:popover-open) {
display: block;
}
Atau, untuk penggantian lama tanpa polyfill, gunakan position: fixed dan hitung koordinat secara manual melalui getBoundingClientRect() atau andalkan pemosisian default dengan inset: auto jika hal itu dapat diterima untuk kasus penggunaan.