# dismiss-watcher

One dismissal Adapter for drawers, menus, dialogs and popovers — Escape / back gesture and outside click, without each panel element owning the logic.


```html
<div>
  <quark-sheet>
    :scope {
      @on menu-open { data-is-open: ""; }
      @on dismiss-watcher-dismiss { data-is-open: none; }
      &[data-is-open] dismiss-watcher { is-active: ""; }
      &:not([data-is-open]) dismiss-watcher { is-active: none; }
    }
  </quark-sheet>
  <style>
    [data-demo-menu] { display: none; }
    [data-is-open] > [data-demo-menu] { display: block; }
  </style>
  <event-handler role="button" fire-event="menu-open">Open menu</event-handler>
  <nav data-demo-menu>
    <dismiss-watcher></dismiss-watcher>
    <ul>
      <li><a href="#">Profile</a></li>
      <li><a href="#">Settings</a></li>
      <li><a href="#">Sign out</a></li>
    </ul>
    <p>Press Escape or click outside to close.</p>
  </nav>
</div>
```


## Features

- **Escape / back gesture** Through a `CloseWatcher` (Android back, Escape) via `watch-escape`
- **Outside click** A `mouseup` outside the target via `watch-outside-click`
- **One event** `dismiss-watcher-dismiss` with `detail.reason`; cancel it with `preventDefault()`
- **Default action** `command-name` commands invoked on the target (`--close`), or `fire-event` names dispatched at it
- **Any target** The parent by default, or `target-ref` (`:scope`-relative)
- **Gated by State** Live only while `is-active` — set it from Quark on the panel's open state

## Installation


`@excom/dismiss-watcher` v0.1.8

```bash
pnpm add @excom/dismiss-watcher
```

```bash
npm install @excom/dismiss-watcher
```

```bash
yarn add @excom/dismiss-watcher
```

### Import

```ts
import "@excom/dismiss-watcher";
```



## Usage

Drop it inside the element being dismissed, gate `is-active` on that element's open state, and either react to `dismiss-watcher-dismiss` or let `command-name` / `fire-event` close the panel for you. When neither `watch-escape` nor `watch-outside-click` is present, both watchers are on.

```html
<nav>
  <dismiss-watcher fire-event="menu-close"></dismiss-watcher>
  …
</nav>
```

```quark
:scope {
  @on menu-open { data-is-open: ""; }
  @on menu-close { data-is-open: none; }
  &[data-is-open] dismiss-watcher { is-active: ""; }
  &:not([data-is-open]) dismiss-watcher { is-active: none; }
}
```

Why a separate element: dismissal is the same request whether the panel is a drawer, a menu, a dialog or a popover. Panel elements keep their own state (`is-open`); this Adapter only asks them to close. It renders nothing, listens only while `is-active`, and tears down when unset or removed. Write the inverse rule for `is-active` — Quark rules do not revert.

`dismiss-watcher-dismiss` bubbles and is cancelable. `detail.reason` is `"escape"` / `"outside-click"`. The default action invokes each `command-name` on the target (a `command` event, as a `<button command commandfor>` would) and dispatches each `fire-event` name at it as a bubbling `CustomEvent`; `preventDefault()` keeps the panel open (an unsaved-changes guard, for instance).

### API Reference


#### Attributes

| Name | Surface | Type | Default | Values | Description |
| --- | --- | --- | --- | --- | --- |
| `is-active` | hybrid | `boolean` |  |  | Watchers are live while set. Gate it from Quark on the panel's open state, and write the inverse rule. |
| `watch-escape` | option | `boolean` |  |  | Watch Escape / the back gesture through a `CloseWatcher`. When neither `watch-*` attribute is present both watchers are on. |
| `watch-outside-click` | option | `boolean` |  |  | Watch for a `mouseup` on the document outside the target. When neither `watch-*` attribute is present both watchers are on. |
| `target-ref` | option | `string` |  |  | Selector for the element being dismissed, `:scope`-relative (`:scope ~ nav`). Unset = the parent element. |
| `command-name` | option | `tokenlist` |  | `<command>…` | Commands invoked on the target as the default action of `dismiss-watcher-dismiss` — `--close` for a `<content-drawer>`, or any `--verb` the panel handles. |
| `fire-event` | option | `tokenlist` |  |  | Event names dispatched at the target (bubbling `CustomEvent`s) as the default action of `dismiss-watcher-dismiss`, for panels driven by events rather than commands (`menu-close`). |

#### Fires

| Name | Type | Description |
| --- | --- | --- |
| `dismiss-watcher-dismiss` | `DismissWatcherDismissEvent` (`CustomEvent & { type: "dismiss-watcher-dismiss"; detail: { reason: "escape" \| "outside-click" }; bubbles: true; cancelable: true; composed: true }`) | A dismissal was requested; `detail.reason` is `"escape"` or `"outside-click"`. Default action: invoke each `command-name` on the target and dispatch each `fire-event` name at it as a bubbling `CustomEvent`. `preventDefault()` skips both. |



### Examples

#### Menu panel

Both watchers on (neither `watch-*` set), target = the parent `<nav>`. The sheet opens on `menu-open`, closes on `dismiss-watcher-dismiss`, and gates `is-active` on `data-is-open` with its inverse rule.


```html
<div>
  <quark-sheet>
    :scope {
      @on menu-open { data-is-open: ""; }
      @on dismiss-watcher-dismiss { data-is-open: none; }
      &[data-is-open] dismiss-watcher { is-active: ""; }
      &:not([data-is-open]) dismiss-watcher { is-active: none; }
    }
  </quark-sheet>
  <style>
    [data-demo-menu] { display: none; }
    [data-is-open] > [data-demo-menu] { display: block; }
  </style>
  <event-handler role="button" fire-event="menu-open">Open menu</event-handler>
  <nav data-demo-menu>
    <dismiss-watcher></dismiss-watcher>
    <ul>
      <li><a href="#">Profile</a></li>
      <li><a href="#">Settings</a></li>
      <li><a href="#">Sign out</a></li>
    </ul>
    <p>Press Escape or click outside to close.</p>
  </nav>
</div>
```


#### Content drawer

First child of `<content-drawer>`, `command-name="--close"` — the drawer needs no dismissal logic of its own. `is-active` follows `content-drawer[is-open]`.

Open with `--open`, not `--toggle`: an outside `mouseup` on the button closes the drawer first, then the button's `click` re-opens it. With `--toggle` the same click would close it again.


```html
<section>
  <quark-sheet>
    :scope {
      content-drawer[is-open] dismiss-watcher { is-active: ""; }
      content-drawer:not([is-open]) dismiss-watcher { is-active: none; }
    }
  </quark-sheet>
  <button type="button" command="--open" commandfor="demo-dismiss-drawer">
    Open drawer
  </button>
  <content-drawer id="demo-dismiss-drawer" class="absolute">
    <dismiss-watcher watch-escape watch-outside-click command-name="--close"></dismiss-watcher>
    <header>
      <h2>Drawer</h2>
    </header>
    <p>Press Escape or click outside to close.</p>
  </content-drawer>
  <event-handler class="tag-backdrop" role="presentation" target-ref="content-drawer:has(+ :scope)" command-name="--close"></event-handler>
</section>
```

## Release notes

### 0.1.1 (2026-09-23)

- Ship only dist (and declared extras) in the npm tarball; drop build logs, tests and sources
