선언적 대화상자 및 팝오버 컨트롤

Invoker Commands API를 사용하면 HTML 버튼에서 직접 <dialog> 및 [popover] 요소의 공개 상태를 전환할 수 있으므로 맞춤 JavaScript 이벤트 리스너가 필요하지 않습니다.

commandfor (타겟 ID) 및 command (작업) 속성을 <button>에 적용하면 브라우저가 열기/닫기 상태 변경, 포커스 관리, 접근성 바인딩 (예: aria-expanded)을 자동으로 처리합니다. 이 선언적 접근 방식은 불안정한 상용구 코드를 삭제하고, 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>

대체 전략

호출자 명령어와 팝오버는 아직 보편적으로 지원되지 않으므로 이전 브라우저의 대체 수단으로 폴리필을 사용해야 합니다(MUST).

Invoker 명령어 폴리필

필수: HTMLButtonElement 프로토타입에서 commandForElement 속성을 확인하여 기능 감지 지원 창 또는 문서 객체를 확인하지 마세요. 네이티브 기능이 누락된 경우에만 폴리필을 동적으로 가져와야 합니다(MUST). 폴리필을 무조건 로드하지 마세요. 필수: 네이티브 'command' 이벤트는 버블링되지 않으므로 타겟 요소에서 직접 'command' 이벤트를 수신 대기합니다.

옵션 1: 번들러 사용 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 사용 번들러가 없는 프로젝트의 경우 <script type="module"> 내의 CDN에서 직접 폴리필을 동적으로 가져옵니다.

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

호출자 폴리필 제한사항 필수: 이 폴리필은 네이티브 브라우저와 같은 방식으로 명령 버튼의 ARIA 상태 (예: aria-expanded)를 처리하지 않습니다. 사이트의 접근성을 완전히 보장하려면 이러한 상태를 직접 처리하는 것이 좋습니다.

Invoker Commands API가 지원되지 않으면 command 이벤트가 실행되지 않습니다. 모든 최신 브라우저에서 완벽하게 지원하려면 npm install 또는 CDN을 통해 https://github.com/keithamus/invokers-polyfill의 invokers-polyfill을 사용하는 것이 좋습니다.

이 폴리필은 맞춤 액션(--로 시작)을 완전히 지원하며 네이티브 API와 정확히 동일하게 command 이벤트를 전달합니다.

동적 가져오기 (성능 최적화)

최상의 성능을 위해서는 브라우저가 API를 기본적으로 지원하지 않는 경우에만 폴리필을 로드해야 합니다. 이렇게 하면 최신 브라우저를 사용하는 사용자의 대역폭이 절약되고 스크립트 실행 시간이 단축됩니다.

참고: 이 polyfill은 맞춤 명령어의 ARIA 상태 (예: aria-pressed 또는 aria-expanded)를 관리하지 않습니다. 사이트에 액세스할 수 있도록 이벤트 리스너에서 이러한 상태를 수동으로 동기화해야 합니다.

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

수동 대체 (기존 패턴)

폴리필을 사용하지 않으려면 이벤트 위임을 사용하여 이벤트를 전달하고 명령어 레지스트리를 사용하여 작업을 처리할 수 있습니다. 이는 기존 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는 대부분 점진적 개선이지만, 최상위 레이어 승격, 가벼운 닫기, popovertarget 호출과 같은 정의 동작에는 CSS 전용에 상응하는 기능이 없습니다. 이전 브라우저에는 폴리필이 필요하며, 폴리필을 제공하지 않으려면 수동 대체가 필요합니다.

폴리필: 이전 브라우저에서 popover 속성을 지원하려면 조건부로 @oddbird/popover-polyfill를 로드합니다. 필수: HTMLElement.prototype에서 popover 속성을 확인하여 기능을 감지하고 네이티브 지원이 누락된 경우에만 폴리필을 로드합니다. 무조건 로드하지 마세요.

번들러 또는 가져오기 맵 사용:

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

번들러가 없는 경우 <script type="module"> 내의 CDN에서 가져옵니다.

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

스타일 지정 주의사항: 폴리필은 실제 :popover-open 의사 클래스를 정의할 수 없으므로 대신 .\:popover-open 클래스를 적용합니다. 필수: :is() 또는 :where()를 사용하여 두 가지를 결합합니다. 그렇지 않으면 :popover-open가 없는 브라우저에서 전체 규칙을 삭제합니다.

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

또는 폴리필이 없는 기존 대체 기능의 경우 position: fixed를 사용하고 getBoundingClientRect()를 통해 좌표를 수동으로 계산하거나, 사용 사례에 적합한 경우 inset: auto를 사용하여 기본 위치 지정에 의존합니다.