Điều khiển hộp thoại và cửa sổ bật lên mang tính khai báo

Sử dụng Invoker Commands API để bật/tắt chế độ hiển thị của các phần tử <dialog> và [popover] ngay từ các nút HTML, nhờ đó không cần đến trình nghe sự kiện JavaScript tuỳ chỉnh.

Bằng cách áp dụng các thuộc tính commandfor (mã nhận dạng mục tiêu) và command (hành động) cho <button>, trình duyệt sẽ tự động xử lý các thay đổi về trạng thái mở/đóng, quản lý tiêu điểm và các liên kết hỗ trợ tiếp cận (chẳng hạn như aria-expanded). Bạn nên sử dụng phương pháp khai báo này vì phương pháp này giúp loại bỏ mã nguyên mẫu dễ hỏng, đảm bảo các hoạt động tương tác hoạt động ngay sau khi phân tích cú pháp HTML và đảm bảo trải nghiệm người dùng mạnh mẽ, dễ tiếp cận.

Triển khai cửa sổ bật lên khai báo

Bạn có thể bật và tắt cửa sổ bật lên bằng một nút duy nhất.

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

Nếu cần kiểm soát việc mở và đóng bằng các nút riêng biệt, bạn có thể sử dụng lệnh show-popover và 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>

Triển khai hộp thoại phương thức khai báo

Không giống như cửa sổ bật lên, hộp thoại phương thức thường sử dụng các nút riêng biệt để mở và đóng. Sử dụng lệnh show-modal cụ thể khi bạn cần mở hộp thoại dưới dạng phương thức.

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

Chiến lược dự phòng

Vì Lệnh trình gọi và Cửa sổ bật lên chưa được hỗ trợ rộng rãi, nên bạn PHẢI sử dụng polyfill làm phương án dự phòng cho các trình duyệt cũ.

Polyfill cho lệnh của trình gọi

BẮT BUỘC: Phát hiện tính năng hỗ trợ bằng cách kiểm tra thuộc tính commandForElement trên nguyên mẫu HTMLButtonElement. KHÔNG kiểm tra đối tượng cửa sổ hoặc tài liệu. Bạn PHẢI nhập polyfill một cách linh động chỉ khi thiếu tính năng gốc. KHÔNG tải polyfill một cách vô điều kiện. Bắt buộc: Nghe sự kiện "command" ngay trên phần tử mục tiêu vì sự kiện "command" gốc không nổi lên.

Cách 1: Sử dụng trình đóng gói Cài đặt polyfill thông qua npm (npm install invokers-polyfill). Phương pháp này dành cho các dự án sử dụng trình đóng gói (như Vite hoặc Webpack) hoặc bản đồ nhập. Đối với tất cả các chế độ thiết lập khác, hãy sử dụng lựa chọn CDN bên dưới.

// 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');
}

Cách 2: Sử dụng CDN Đối với các dự án không có trình đóng gói, hãy nhập động polyfill trực tiếp từ CDN bên trong <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>

Các hạn chế của Polyfill cho trình gọi BẮT BUỘC: Polyfill này không xử lý các trạng thái ARIA (ví dụ: aria-expanded) của nút lệnh theo cách của trình duyệt gốc. Bạn nên tự xử lý các trạng thái này để đảm bảo trang web của bạn có thể truy cập đầy đủ.

Nếu API Lệnh của trình gọi không được hỗ trợ, thì sự kiện command sẽ không kích hoạt. Để được hỗ trợ đầy đủ trên tất cả trình duyệt hiện đại, bạn nên sử dụng invokers-polyfill từ https://github.com/keithamus/invokers-polyfill thông qua npm install hoặc CDN.

Polyfill này hỗ trợ đầy đủ các thao tác tuỳ chỉnh (bắt đầu bằng --) và gửi sự kiện command giống hệt như API gốc.

Nhập động (Tối ưu hoá hiệu suất)

Để có hiệu suất tốt nhất, bạn chỉ nên tải polyfill nếu trình duyệt không hỗ trợ API gốc. Điều này giúp tiết kiệm băng thông và giảm thời gian thực thi tập lệnh cho người dùng trên các trình duyệt hiện đại.

LƯU Ý: Polyfill này không quản lý các trạng thái ARIA (như aria-pressed hoặc aria-expanded) cho các lệnh tuỳ chỉnh. Bạn phải đồng bộ hoá các trạng thái này theo cách thủ công trong trình nghe sự kiện để đảm bảo trang web của bạn có thể truy cập được.

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

Dự phòng thủ công (Mẫu truyền thống)

Nếu không muốn sử dụng polyfill, bạn có thể kết hợp uỷ quyền sự kiện để gửi sự kiện và sổ đăng ký lệnh để xử lý các hành động. Đây là một mẫu kiến trúc phổ biến trong quá trình phát triển JavaScript truyền thống, vẫn mang lại hiệu quả cao và có khả năng mở rộng.

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

Phương án dự phòng và hỗ trợ trình duyệt cho Popover

Popover API chủ yếu là cải tiến tăng dần, nhưng các hành vi xác định của API này (quảng bá lớp trên cùng, đóng nhẹ và gọi popovertarget) không có hành vi tương đương chỉ dùng CSS. Các trình duyệt cũ cần có polyfill hoặc phương án dự phòng thủ công nếu bạn không muốn cung cấp polyfill.

Polyfill: Để hỗ trợ thuộc tính popover trong các trình duyệt cũ, hãy tải @oddbird/popover-polyfill theo điều kiện. BẮT BUỘC: Phát hiện tính năng bằng cách kiểm tra thuộc tính popover trên HTMLElement.prototype và chỉ tải polyfill khi thiếu tính năng hỗ trợ gốc — KHÔNG tải polyfill vô điều kiện.

Với trình đóng gói hoặc bản đồ nhập:

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

Nếu không có trình đóng gói, hãy nhập từ CDN bên trong <script type="module">:

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

Lưu ý về kiểu: Polyfill không thể xác định lớp giả :popover-open thực, vì vậy, lớp này sẽ áp dụng lớp .\:popover-open. BẮT BUỘC: Kết hợp cả hai bằng :is() hoặc :where(), nếu không, các trình duyệt thiếu :popover-open sẽ loại bỏ toàn bộ quy tắc:

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

Ngoài ra, đối với phương án dự phòng cũ không có polyfill, hãy sử dụng position: fixed và tính toán toạ độ theo cách thủ công thông qua getBoundingClientRect() hoặc dựa vào vị trí mặc định bằng inset: auto nếu điều đó phù hợp với trường hợp sử dụng.