{"version":3,"file":"use-dialog.cjs","names":[],"sources":["../../../src/overlay/shared/use-dialog.ts"],"sourcesContent":["import { type Readable, type Signal, signal, watch } from '@vielzeug/ripple';\n\nimport { createFocusManager, type DialogCloseReason, lifecycleSignal, type OverlayOpenReason } from '../../core';\nimport { awaitExit } from './await-exit';\nimport { createBackgroundLock } from './background-lock';\n\n// Invoker Commands API — not yet in lib.dom.d.ts\ninterface CommandEvent extends Event {\n  command: string;\n}\n\n// ── Types ─────────────────────────────────────────────────────────────────────\n\nexport type UseDialogOptions = {\n  /** Called after showModal() but before isOpen = true. Use to clear drag styles etc. */\n  beforeOpen?: (dialog: HTMLDialogElement) => void;\n  /** Extra properties merged into the `close-request` custom event detail. */\n  closeRequestDetail?: (reason: Exclude<DialogCloseReason, 'programmatic'>) => Record<string, unknown>;\n  /** Initial uncontrolled open state. Ignored when `openProp` is defined. */\n  defaultOpen: Readable<boolean | undefined>;\n  dialogRef: { value: HTMLDialogElement | null | undefined };\n  getPanelEl: () => HTMLElement | null | undefined;\n  host: HTMLElement;\n  initialFocus: Readable<string | undefined>;\n  isPersistent: () => boolean;\n  /** Cleanup registrar from the component setup ctx. Automatically called on disconnect. */\n  onCleanup: (fn: () => void) => void;\n  /**\n   * Scoped event listener registrar from the component setup ctx.\n   * Automatically removed on component disconnect.\n   */\n  onEvent: <K extends keyof HTMLElementEventMap>(\n    target: EventTarget | null | undefined,\n    event: K,\n    listener: (e: HTMLElementEventMap[K]) => void,\n    options?: AddEventListenerOptions,\n  ) => void;\n  /**\n   * Called after the native `close` event fires — after background unlock,\n   * isOpen reset, and focus restore. Use to emit component events.\n   */\n  onNativeClose?: (reason: DialogCloseReason) => void;\n  /** Called by the overlay on open. Use to emit events. */\n  onOpen?: (reason: OverlayOpenReason) => void;\n  /** Open prop from the component — drives programmatic open/close via watch. */\n  openProp: Readable<boolean | undefined>;\n  /**\n   * Called by the overlay when closing. Orchestrates the close animation.\n   * Default: `dialog.close()`. Override in dialog.ts to add the CSS exit animation.\n   */\n  performClose?: (dialog: HTMLDialogElement, reason: DialogCloseReason) => void;\n  returnFocus: Readable<boolean | undefined>;\n};\n\nexport type UseDialogHandle = {\n  closeWithAnimation: () => void;\n  /**\n   * Dispatch a cancelable `close-request` event on the host. Returns true when\n   * the close is allowed (event not prevented), false when it was cancelled.\n   */\n  dispatchCloseRequest: (reason: Exclude<DialogCloseReason, 'programmatic'>) => boolean;\n  handleBackdropClick: (e: MouseEvent) => void;\n  handleCancel: (e: Event) => void;\n  isOpen: Signal<boolean>;\n  /** The modal dialog controller — open/close/toggle/dispose. */\n  overlay: ModalDialogController;\n  /**\n   * Dispatch `close-request`; if allowed, call `overlay.close()`.\n   */\n  requestClose: (reason: Exclude<DialogCloseReason, 'programmatic'>) => void;\n  /**\n   * Registers the open-prop watcher AND standard click/cancel event listeners on\n   * the dialog element (backdrop click-to-close, Escape via the native `cancel`\n   * event). Use for components that need standard dialog behavior.\n   * Call inside `onMounted()`.\n   */\n  setupNativeListeners: () => void;\n  /**\n   * Registers only the open-prop watcher (without adding click/cancel listeners).\n   * Use for components like drawer that register their own event handlers.\n   * Call inside `onMounted()`.\n   */\n  watchOpenProp: () => void;\n};\n\ntype ModalDialogController = {\n  [Symbol.dispose](): void;\n  close(reason?: DialogCloseReason): void;\n  dispose(): void;\n  open(reason?: OverlayOpenReason): void;\n  toggle(openReason?: OverlayOpenReason, closeReason?: DialogCloseReason): void;\n};\n\n// ── Factory ───────────────────────────────────────────────────────────────────\n\n/**\n * Shared dialog-control composable for `ore-dialog` and `ore-drawer`.\n *\n * Owns: `isOpen`, close-reason tracking, focus management, `overlay`,\n * `requestClose`, background lock/unlock, and the `close` event handler.\n * Components supply only their unique behavior via callbacks and `onNativeClose`.\n */\nexport function useDialogControl(options: UseDialogOptions): UseDialogHandle {\n  const abortSignal = lifecycleSignal(options.onCleanup);\n  const isOpen = signal(false);\n  // Internal close-reason: set atomically before dialog.close() fires.\n  let pendingCloseReason: DialogCloseReason = 'programmatic';\n  const bgLock = createBackgroundLock();\n  let isClosing = false;\n\n  // ── Focus management ──────────────────────────────────────────────────────\n  const focus = createFocusManager({\n    getInitialFocusSelector: () => options.initialFocus.value,\n    getReturnFocus: () => options.returnFocus.value,\n    host: options.host,\n    signal: abortSignal,\n  });\n  // ─────────────────────────────────────────────────────────────────────────\n\n  const closeWithAnimation = (): void => {\n    const dialog = options.dialogRef.value;\n\n    if (!dialog?.open || isClosing) return;\n\n    isClosing = true;\n    dialog.classList.add('closing');\n\n    const finish = () => {\n      dialog.close();\n      // Do NOT remove 'closing' here — removing it while the native ::backdrop is\n      // still held in the top layer by the 'overlay allow-discrete' transition\n      // would revert its opacity to 1 and cause a visible flash. The class is\n      // cleaned up at the start of the next open cycle (see setOpen below).\n      isClosing = false;\n    };\n\n    const panel = options.getPanelEl();\n\n    if (panel) {\n      awaitExit(panel, finish, 'transition');\n    } else {\n      finish();\n    }\n  };\n\n  const dispatchCloseRequest = (reason: Exclude<DialogCloseReason, 'programmatic'>): boolean => {\n    return options.host.dispatchEvent(\n      new CustomEvent('close-request', {\n        bubbles: true,\n        cancelable: true,\n        composed: true,\n        detail: { reason, ...options.closeRequestDetail?.(reason) },\n      }),\n    );\n  };\n\n  const overlay: ModalDialogController = {\n    close(reason: DialogCloseReason = 'programmatic'): void {\n      if (!isOpen.value) return;\n\n      const dialog = options.dialogRef.value;\n\n      if (dialog?.open) {\n        pendingCloseReason = reason;\n        (options.performClose ?? ((element, _reason) => element.close()))(dialog, reason);\n      } else {\n        isOpen.value = false;\n      }\n    },\n    dispose(): void {\n      overlay.close('programmatic');\n    },\n    open(reason: OverlayOpenReason = 'programmatic'): void {\n      if (isOpen.value) return;\n\n      const dialog = options.dialogRef.value;\n\n      if (!dialog || dialog.open) return;\n\n      // Clean up any leftover 'closing' class from the previous close animation\n      // before opening so the entry @starting-style and transition work correctly.\n      dialog.classList.remove('closing');\n      focus.captureReturnFocus();\n      options.beforeOpen?.(dialog);\n      dialog.showModal();\n      bgLock.lock(options.host);\n      focus.applyInitialFocus();\n      isOpen.value = true;\n      options.onOpen?.(reason);\n    },\n    [Symbol.dispose](): void {\n      overlay.dispose();\n    },\n    toggle(openReason: OverlayOpenReason = 'click', closeReason: DialogCloseReason = 'trigger'): void {\n      if (isOpen.value) overlay.close(closeReason);\n      else overlay.open(openReason);\n    },\n  };\n\n  abortSignal.addEventListener('abort', overlay.dispose, { once: true });\n\n  const requestClose = (reason: Exclude<DialogCloseReason, 'programmatic'>): void => {\n    const allowed = dispatchCloseRequest(reason);\n\n    if (!allowed) return;\n\n    overlay.close(reason);\n  };\n\n  // Invoker Commands API — allows declarative open/close from outside the component:\n  //   <button commandfor=\"ore-dialog-id\" command=\"show-modal\">Open</button>\n  //   <button commandfor=\"ore-dialog-id\" command=\"close\">Close</button>\n  options.host.addEventListener(\n    'command',\n    (e: Event) => {\n      const cmd = (e as CommandEvent).command;\n\n      if (cmd === 'show-modal') overlay.open('trigger');\n      else if (cmd === 'close' || cmd === 'request-close') requestClose('trigger');\n    },\n    { signal: abortSignal },\n  );\n\n  // The native `<dialog>` element auto-closes on Escape via its own close-watcher, which\n  // dispatches a cancelable `cancel` event before `close`. Suppressing/redirecting that is the\n  // spec-correct hook — relying on `keydown`'s `preventDefault()` to stop the close-watcher is\n  // implementation-specific, not guaranteed across browsers.\n  const handleCancel = (e: Event): void => {\n    // See `handleNativeClose`'s comment: guard against a same-named bubbling event from a\n    // descendant field being misread as this dialog's own native event.\n    if (e.target !== options.dialogRef.value) return;\n\n    e.preventDefault();\n\n    if (options.isPersistent()) return;\n\n    requestClose('escape');\n  };\n\n  const handleBackdropClick = (e: MouseEvent): void => {\n    if (options.isPersistent()) return;\n\n    if (e.target === options.dialogRef.value) requestClose('outsideClick');\n  };\n\n  // ── Internal native close handler ─────────────────────────────────────────\n  // `e.target` must be checked against the native `<dialog>` itself, the same way\n  // `handleBackdropClick` already does: a bubbling `close`-named event fired by *any* descendant\n  // component reaches this listener too — regardless of that event's own `composed` flag, since\n  // slot-assignment-based event-path computation (a slotted descendant's \"parent\" is the `<slot>`\n  // projecting it) isn't gated by `composed` the way crossing back out of a shadow root is. Some\n  // fields nested here (`ore-select`, `ore-combobox`, …) emit their own public `close` event when\n  // *their* dropdown closes — without this check, selecting an option in one of those closes this\n  // dialog too, misreading the field's own event as this dialog's native close.\n  const handleNativeClose = (e?: Event): void => {\n    if (e && e.target !== options.dialogRef.value) return;\n\n    bgLock.unlock();\n    options.host.removeAttribute('open');\n    isOpen.value = false;\n    focus.restoreFocus();\n\n    const reason = pendingCloseReason;\n\n    pendingCloseReason = 'programmatic';\n    options.onNativeClose?.(reason);\n  };\n\n  const watchOpenProp = (): void => {\n    const dialog = options.dialogRef.value;\n\n    if (dialog) options.onEvent(dialog, 'close', handleNativeClose);\n\n    let initialized = false;\n\n    watch(\n      options.openProp,\n      (open) => {\n        if (open === undefined) {\n          if (!initialized) {\n            if (options.defaultOpen.value) overlay.open('programmatic');\n\n            initialized = true;\n\n            return;\n          }\n\n          overlay.close('programmatic');\n\n          return;\n        }\n\n        initialized = true;\n\n        if (open) {\n          overlay.open('programmatic');\n        } else {\n          overlay.close('programmatic');\n        }\n      },\n      { immediate: true },\n    );\n  };\n\n  const setupNativeListeners = (): void => {\n    const dialog = options.dialogRef.value;\n\n    if (!dialog) return;\n\n    watchOpenProp();\n    options.onEvent(dialog, 'click', handleBackdropClick);\n    options.onEvent(dialog, 'cancel', handleCancel);\n  };\n\n  return {\n    closeWithAnimation,\n    dispatchCloseRequest,\n    handleBackdropClick,\n    handleCancel,\n    isOpen,\n    overlay,\n    requestClose,\n    setupNativeListeners,\n    watchOpenProp,\n  };\n}\n"],"mappings":"2KAsGA,SAAgB,EAAiB,EAA4C,CAC3E,IAAM,EAAc,EAAA,gBAAgB,EAAQ,SAAS,EAC/C,GAAA,EAAS,EAAA,OAAA,CAAO,EAAK,EAEvB,EAAwC,eACtC,EAAS,EAAA,qBAAqB,EAChC,EAAY,GAGV,EAAQ,EAAA,mBAAmB,CAC/B,4BAA+B,EAAQ,aAAa,MACpD,mBAAsB,EAAQ,YAAY,MAC1C,KAAM,EAAQ,KACd,OAAQ,CACV,CAAC,EAGK,MAAiC,CACrC,IAAM,EAAS,EAAQ,UAAU,MAEjC,GAAI,CAAC,GAAQ,MAAQ,EAAW,OAEhC,EAAY,GACZ,EAAO,UAAU,IAAI,SAAS,EAE9B,IAAM,MAAe,CACnB,EAAO,MAAM,EAKb,EAAY,EACd,EAEM,EAAQ,EAAQ,WAAW,EAE7B,EACF,EAAA,UAAU,EAAO,EAAQ,YAAY,EAErC,EAAO,CAEX,EAEM,EAAwB,GACrB,EAAQ,KAAK,cAClB,IAAI,YAAY,gBAAiB,CAC/B,QAAS,GACT,WAAY,GACZ,SAAU,GACV,OAAQ,CAAE,SAAQ,GAAG,EAAQ,qBAAqB,CAAM,CAAE,CAC5D,CAAC,CACH,EAGI,EAAiC,CACrC,MAAM,EAA4B,eAAsB,CACtD,GAAI,CAAC,EAAO,MAAO,OAEnB,IAAM,EAAS,EAAQ,UAAU,MAE7B,GAAQ,MACV,EAAqB,GACpB,EAAQ,gBAAkB,EAAS,IAAY,EAAQ,MAAM,GAAA,CAAI,EAAQ,CAAM,GAEhF,EAAO,MAAQ,EAEnB,EACA,SAAgB,CACd,EAAQ,MAAM,cAAc,CAC9B,EACA,KAAK,EAA4B,eAAsB,CACrD,GAAI,EAAO,MAAO,OAElB,IAAM,EAAS,EAAQ,UAAU,MAE7B,CAAC,GAAU,EAAO,OAItB,EAAO,UAAU,OAAO,SAAS,EACjC,EAAM,mBAAmB,EACzB,EAAQ,aAAa,CAAM,EAC3B,EAAO,UAAU,EACjB,EAAO,KAAK,EAAQ,IAAI,EACxB,EAAM,kBAAkB,EACxB,EAAO,MAAQ,GACf,EAAQ,SAAS,CAAM,EACzB,EACA,CAAC,OAAO,UAAiB,CACvB,EAAQ,QAAQ,CAClB,EACA,OAAO,EAAgC,QAAS,EAAiC,UAAiB,CAC5F,EAAO,MAAO,EAAQ,MAAM,CAAW,EACtC,EAAQ,KAAK,CAAU,CAC9B,CACF,EAEA,EAAY,iBAAiB,QAAS,EAAQ,QAAS,CAAE,KAAM,EAAK,CAAC,EAErE,IAAM,EAAgB,GAA6D,CACjE,EAAqB,CAEhC,GAEL,EAAQ,MAAM,CAAM,CACtB,EAKA,EAAQ,KAAK,iBACX,UACC,GAAa,CACZ,IAAM,EAAO,EAAmB,QAE5B,IAAQ,aAAc,EAAQ,KAAK,SAAS,GACvC,IAAQ,SAAW,IAAQ,kBAAiB,EAAa,SAAS,CAC7E,EACA,CAAE,OAAQ,CAAY,CACxB,EAMA,IAAM,EAAgB,GAAmB,CAGnC,EAAE,SAAW,EAAQ,UAAU,QAEnC,EAAE,eAAe,EAEb,GAAQ,aAAa,GAEzB,EAAa,QAAQ,EACvB,EAEM,EAAuB,GAAwB,CAC/C,EAAQ,aAAa,GAErB,EAAE,SAAW,EAAQ,UAAU,OAAO,EAAa,cAAc,CACvE,EAWM,EAAqB,GAAoB,CAC7C,GAAI,GAAK,EAAE,SAAW,EAAQ,UAAU,MAAO,OAE/C,EAAO,OAAO,EACd,EAAQ,KAAK,gBAAgB,MAAM,EACnC,EAAO,MAAQ,GACf,EAAM,aAAa,EAEnB,IAAM,EAAS,EAEf,EAAqB,eACrB,EAAQ,gBAAgB,CAAM,CAChC,EAEM,MAA4B,CAChC,IAAM,EAAS,EAAQ,UAAU,MAE7B,GAAQ,EAAQ,QAAQ,EAAQ,QAAS,CAAiB,EAE9D,IAAI,EAAc,IAElB,EAAA,EAAA,MAAA,CACE,EAAQ,SACP,GAAS,CACR,GAAI,IAAS,IAAA,GAAW,CACtB,GAAI,CAAC,EAAa,CACZ,EAAQ,YAAY,OAAO,EAAQ,KAAK,cAAc,EAE1D,EAAc,GAEd,MACF,CAEA,EAAQ,MAAM,cAAc,EAE5B,MACF,CAEA,EAAc,GAEV,EACF,EAAQ,KAAK,cAAc,EAE3B,EAAQ,MAAM,cAAc,CAEhC,EACA,CAAE,UAAW,EAAK,CACpB,CACF,EAYA,MAAO,CACL,qBACA,uBACA,sBACA,eACA,SACA,UACA,eACA,yBAlBuC,CACvC,IAAM,EAAS,EAAQ,UAAU,MAE5B,IAEL,EAAc,EACd,EAAQ,QAAQ,EAAQ,QAAS,CAAmB,EACpD,EAAQ,QAAQ,EAAQ,SAAU,CAAY,EAChD,EAWE,eACF,CACF"}