# listenable-element

Declarative event and lifecycle listening for Neutron elements —
filter, debounce, vibrate, and hand off to your `actionHandler`.

## Features

- **Event / lifecycle hooks** Listen for DOM events or `connected` /
  `disconnected` / `adopted`
- **Host retarget** `host-ref="window"` / `document` / any selector —
  Escape to dismiss, shortcuts outside the bubble path
- **Target filters** Selector, keycode (`shift+k` chords), and pathname gates
- **Debounce / delay** Coalesce noisy input
- **Event hygiene** `prevent-default` / `stop-propagation` /
  `stop-immediate-propagation`
- **Haptic pulse** Optional `vibrate-ms` on handle

## Installation


`@excom/listenable-element` v0.2.2

```bash
pnpm add @excom/listenable-element
```

```bash
npm install @excom/listenable-element
```

```bash
yarn add @excom/listenable-element
```

### Import

```ts
import { /* … */ } from "@excom/listenable-element";
```



## Usage

Compose `ListenableElement` and implement `actionHandler`. Concrete
consumers include `<spa-a>` and `<event-handler>`.

```ts
import { Neutron } from "@excom/neutron";
import { ListenableElement } from "@excom/listenable-element";

export const TapLog = Neutron.compose([
  ListenableElement,
  Neutron({ tag: "tap-log" }),
])
  .defineMethods({
    actionHandler: (_el, e) => {
      console.log("handled", e.type);
    },
  });

TapLog.define();
```

```html
<tap-log listen-for="click keydown" keycode-filter="enter">
  Tap or Enter
</tap-log>
```

`host-ref` moves listening off `:scope` — e.g. `host-ref="window"` for
global keydown. See `<event-handler>` for Escape-to-dismiss examples.

### API Reference


#### Attributes

| Name | Surface | Type | Default | Values | Description |
| --- | --- | --- | --- | --- | --- |
| `host-ref` | option | `string` |  | `<CSS Selector>` \| `"window"` \| `"document"` \| `"html"` \| `"body"` \| `"head"` | Listen on another element / `window` / `document` — e.g. Escape to dismiss a dialog from a global `keydown`. Defaults to `:scope`. Used with `listen-for`. Not compatible with `listen-for-lifecycle`. The selector MUST resolve when `host-ref` is set — it will not wait for a match to appear. |
| `listen-for` | option | `tokenlist` |  | `<EventName>…` | Space-separated event names to listen for. Defaults to `click` when unset (and no lifecycle list is set). |
| `listen-for-lifecycle` | option | `tokenlist` |  | `"connected"` \| `"disconnected"` \| `"adopted"` | Space-separated element lifecycles to handle. |
| `listen-once` | option | `boolean` |  |  | Handle each distinct event name / lifecycle at most once. |
| `selector-filter` | option | `string` |  | `<CSS Selector>` | Only handle events whose `event.target` matches this CSS selector. Does not support `:scope` in the selector. |
| `keycode-filter` | option | `tokenlist` |  | `<key` \| `mod+key>…` | Space-separated key filters (OR). Join modifiers with `+` (AND, any order): `shift+k tab` → Shift+K or Tab. Modifiers: `shift`, `alt`, `ctrl`/`control`, `meta`/`cmd`. Name the space bar `space` / `spacebar` and the plus key `plus` (`shift+space`). Case-insensitive. |
| `pathname-filter` | option | `tokenlist` |  | `<pathname>…` | Only handle when `location.pathname` is one of these values — route-aware behaviors without a separate router element. |
| `prevent-default` | option | `boolean` |  |  | Call `preventDefault()` on matched events (ignored for lifecycles). |
| `stop-propagation` | option | `boolean` |  |  | Call `stopPropagation()` on matched events (ignored for lifecycles). |
| `stop-immediate-propagation` | option | `boolean` |  |  | Call `stopImmediatePropagation()` on matched events (ignored for lifecycles). |
| `vibrate-ms` | option | `number` | `"20 (when attribute is present with no value)"` |  | Vibrate on handle (`navigator.vibrate`). Empty / `0` uses a 20ms pulse. |
| `delay-ms` | option | `number` |  |  | Delay handling by this many milliseconds. |
| `is-debounced` | option | `boolean` |  |  | With `delay-ms`, coalesce bursts into one trailing call (debounce). |



### Examples

#### Default click → navigate

`<spa-a>` inherits this base and defaults to click when `listen-for`
is unset:

```html
<spa-a route-href="/pricing">Pricing</spa-a>
```

#### Filter & debounce

```html
<event-handler
  listen-for="input"
  delay-ms="200"
  is-debounced
  fire-event="search-query"
>
  <input name="q" />
</event-handler>
```

See `<event-handler>` and `<spa-a>` package docs for more examples
built on this mixin.

## Release notes

### 0.2.0 (2026-09-30)

- Add the key names `space` / `spacebar` and `plus` to `keycode-filter` (`shift+space`), and `cmd` as an alias of `meta`
- Update a `keycode-filter` holding only spaces to match no key and warn once (it was no filter); write `space` for the space bar

### 0.1.1 (2026-09-23)

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