@excom/event-handler 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/.rush/temp/chunked-rush-logs/event-handler.apply-exports.chunks.jsonl +1 -0
  2. package/.rush/temp/chunked-rush-logs/event-handler.build_docs.chunks.jsonl +1 -0
  3. package/.rush/temp/chunked-rush-logs/event-handler.build_package-metas.chunks.jsonl +1 -0
  4. package/.rush/temp/operation/apply-exports/all.log +1 -0
  5. package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
  6. package/.rush/temp/operation/apply-exports/state.json +3 -0
  7. package/.rush/temp/operation/build_docs/all.log +1 -0
  8. package/.rush/temp/operation/build_docs/log-chunks.jsonl +1 -0
  9. package/.rush/temp/operation/build_docs/state.json +3 -0
  10. package/.rush/temp/operation/build_package-metas/all.log +1 -0
  11. package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
  12. package/.rush/temp/operation/build_package-metas/state.json +3 -0
  13. package/.rush/temp/shrinkwrap-deps.json +3 -0
  14. package/config/rig.json +5 -0
  15. package/event-handler.ts +178 -0
  16. package/index.css +5 -0
  17. package/index.ts +17 -0
  18. package/package.json +44 -0
  19. package/rush-logs/event-handler.apply-exports.cache.log +1 -0
  20. package/rush-logs/event-handler.apply-exports.log +1 -0
  21. package/rush-logs/event-handler.build_docs.cache.log +1 -0
  22. package/rush-logs/event-handler.build_docs.log +1 -0
  23. package/rush-logs/event-handler.build_package-metas.cache.log +1 -0
  24. package/rush-logs/event-handler.build_package-metas.log +1 -0
  25. package/src/event-handler.css +19 -0
  26. package/support/custom-elements.json +245 -0
  27. package/support/demos/command-dialog.html +9 -0
  28. package/support/demos/debounce.html +21 -0
  29. package/support/demos/enter-close.html +13 -0
  30. package/support/demos/escape-close.html +16 -0
  31. package/support/demos/fire-event.html +13 -0
  32. package/support/demos/prevent-default.html +3 -0
  33. package/support/demos/retarget.html +14 -0
  34. package/support/demos/with-as.html +8 -0
  35. package/support/dist-docs/event-handler.md +277 -0
  36. package/support/docs/INTERNAL.md +1 -0
  37. package/support/docs/README.md +97 -0
  38. package/support/package-meta.json +249 -0
  39. package/support/tests/__snapshots__/debounce.view.test.ts.snap +23 -0
  40. package/support/tests/__snapshots__/fire-event.view.test.ts.snap +23 -0
  41. package/support/tests/__snapshots__/retarget.view.test.ts.snap +23 -0
  42. package/support/tests/command-dialog.view.test.ts +34 -0
  43. package/support/tests/debounce.view.test.ts +43 -0
  44. package/support/tests/enter-close.view.test.ts +35 -0
  45. package/support/tests/escape-close.view.test.ts +44 -0
  46. package/support/tests/event-handler.form-fallback.test.ts +49 -0
  47. package/support/tests/event-handler.test.ts +524 -0
  48. package/support/tests/fire-event.view.test.ts +38 -0
  49. package/support/tests/prevent-default.view.test.ts +21 -0
  50. package/support/tests/retarget.view.test.ts +38 -0
  51. package/support/tests/with-as.view.test.ts +31 -0
  52. package/tsconfig.json +5 -0
@@ -0,0 +1,249 @@
1
+ {
2
+ "shortName": "event-handler",
3
+ "package": {
4
+ "name": "@excom/event-handler",
5
+ "version": "0.1.0",
6
+ "description": "<event-handler> custom element",
7
+ "peerDependencies": {},
8
+ "excom": {
9
+ "packageType": "kit-element"
10
+ }
11
+ },
12
+ "demos": {
13
+ "command-dialog": "<section>\n <event-handler target-ref=\":scope + dialog\" command-name=\"show-modal\">\n Open dialog\n </event-handler>\n <dialog>\n Hello!\n <form method=\"dialog\"><button>Close</button></form>\n </dialog>\n</section>\n",
14
+ "debounce": "<div>\n <section>\n <event-handler listen-for=\"input\" delay-ms=\"200\" is-debounced fire-event=\"search-query\" form-ref=\":scope form\">\n <form>\n <label>\n <input name=\"detail.q\" type=\"search\" placeholder=\"Type to search…\" />\n </label>\n <label>\n Strict search\n <input name=\"detail.strict\" type=\"checkbox\" role=\"switch\" />\n </label>\n </form>\n </event-handler>\n <output>Waiting…</output>\n </section>\n <quark-sheet>\n @use \"/demo-utils\" as *;\n\n section { @on search-query (handle: setOutputFromDetail); }\n </quark-sheet>\n</div>",
15
+ "enter-close": "<section>\n <event-handler target-ref=\":scope + content-drawer\" command-name=\"--open\">\n Open drawer\n </event-handler>\n <content-drawer id=\"demo-enter-close-drawer\" is-open>\n <event-handler listen-for=\"keydown\" keycode-filter=\"enter\" target-ref=\"#demo-enter-close-drawer\" command-name=\"--close\">\n <label>\n Favorite color\n <input type=\"text\" placeholder=\"Type something, then press Enter to close drawer\" autofocus />\n </label>\n </event-handler>\n </content-drawer>\n</section>\n",
16
+ "escape-close": "<section>\n <event-handler target-ref=\":scope ~ dialog\" command-name=\"show-modal\">\n Open dialog\n </event-handler>\n <event-handler\n host-ref=\"window\"\n listen-for=\"keydown\"\n keycode-filter=\"escape\"\n target-ref=\":scope ~ dialog\"\n command-name=\"close\"\n ></event-handler>\n <dialog>\n Press Escape anywhere to close.\n <form method=\"dialog\"><button>Close</button></form>\n </dialog>\n</section>\n",
17
+ "fire-event": "<div>\n <section>\n <event-handler fire-event=\"cart-add\" detail-sku=\"sku-1\">\n Add to cart\n </event-handler>\n <output>Waiting…</output>\n </section>\n <quark-sheet>\n @use \"/demo-utils\" as *;\n\n section { @on cart-add (handle: setOutputFromDetail); }\n </quark-sheet>\n</div>\n",
18
+ "prevent-default": "<event-handler prevent-default>\n <a href=\"https://example.com\">This native link does nothing</a>\n</event-handler>\n",
19
+ "retarget": "<div>\n <output>Waiting…</output>\n <event-handler target-ref=\"output:has(+ :scope)\" fire-event=\"demo-ping\"\n detail-label=\"pong\">\n Ping previous output\n </event-handler>\n <quark-sheet>\n @use \"/demo-utils\" as *;\n\n output {\n @on demo-ping (handle: setOutputFromDetail);\n }\n </quark-sheet>\n</div>\n",
20
+ "with-as": "<section>\n <event-handler target-ref=\":scope + content-drawer\" command-name=\"--toggle\">\n Toggle drawer\n </event-handler>\n <content-drawer>\n <h2>Open!</h2>\n </content-drawer>\n</section>\n"
21
+ },
22
+ "readme": "<h1 id=\"md-event-handler\">event-handler</h1>\n<p>Stitch behaviors of your app together, primarily through events.</p>\n<p><include-content data-demo=\"with-as\"></include-content></p>\n<h2 id=\"md-features\">Features</h2>\n<ul>\n<li><strong>Fire custom events</strong> Map any input event, such as clicks, or lifecycles to custom output events</li>\n<li><strong>Commands</strong> <code>command-name</code> invokes the HTML Command API — built-in verbs (<code>show-modal</code>) and the <code>--verb</code> commands elements accept (<code>--submit</code>, <code>--close</code>)</li>\n<li><strong>Retarget</strong> Aim events/commands at any selector (<code>target-ref</code>) — where a <code>&lt;button commandfor&gt;</code> needs an id</li>\n<li><strong>Global / host listening</strong> Listen to events globally or on any element. Helpful for: escape to dismiss, global shortcuts, etc.</li>\n<li><strong>Keycode filter</strong> Escape to dismiss, Shift+K shortcuts — keys / modifier chords</li>\n<li><strong>Listen filters</strong> Debounce, selector, and pathname gates</li>\n<li><strong>Custom Event Payloads</strong> <code>detail-*</code> attributes and forms convert to <code>event.detail</code> JSON</li>\n</ul>\n<blockquote>\n<p>In a view that already has a <code>&lt;quark-sheet&gt;</code>, the same wiring is a rule: <code>@on click (target: &quot;[data-add]&quot;) { @dispatch cart-add (detail: (sku: attr(&quot;data-sku&quot;))); }</code> — <code>@on</code> options cover <code>selector-filter</code> / <code>keycode-filter</code> / <code>is-debounced</code> / <code>host-ref</code>, and <code>@dispatch</code> / <code>@command</code> cover <code>fire-event</code> / <code>target-ref</code> / <code>form-ref</code> / <code>command-name</code>. Keep <code>&lt;event-handler&gt;</code> for markup without a sheet.</p>\n</blockquote>\n<h2 id=\"md-installation\">Installation</h2>\n<p><include-content is-active template-ref=\"/views/install-section/install-section.html\"></include-content></p>\n<h2 id=\"md-usage\">Usage</h2>\n<p>Listen (default <code>click</code>), then either <code>fire-event</code> or <code>command-name</code>.</p>\n<include-content data-language=\"html\"><template>&lt;event-handler fire-event=\"cart-add\"&gt;\n Add to cart\n&lt;/event-handler&gt;</template></include-content>\n<p>Or, for example: a successful form submit re-fetches the related data by invoking the provider&#39;s <code>--fetch</code> command.</p>\n<include-content data-language=\"html\"><template>&lt;event-handler listen-for=\"super-form-success\" target-ref=\"#fetch-todos\" command-name=\"--fetch\"&gt;\n &lt;super-form&gt;\n &lt;form&gt;\n &lt;!-- form to create a new todo --&gt;\n &lt;/form&gt;\n &lt;/super-form&gt;\n&lt;/event-handler&gt;</template></include-content>\n<p>In a Nucleus Stack application, this will be one of the most heavily used elements. It is the primary method of linking a functional cause and effect.</p>\n<h3 id=\"md-api-reference\">API Reference</h3>\n<p><include-content is-active template-ref=\"/views/api-reference/api-reference.html\"></include-content></p>\n<h3 id=\"md-examples\">Examples</h3>\n<h4 id=\"md-fire-a-custom-event\">Fire a custom event</h4>\n<p>Click dispatches <code>cart-add</code> event with <code>{ sku: &quot;sku-1&quot; }</code> in <code>event.detail</code>.</p>\n<p><include-content data-demo=\"fire-event\"></include-content></p>\n<h4 id=\"md-open-a-dialog\">Open a dialog</h4>\n<p><code>command-name</code> invokes the HTML Command API — here the built-in <code>show-modal</code> opens the sibling <code>&lt;dialog&gt;</code>. Custom commands (<code>command-name=&quot;--close&quot;</code>) reach any element that handles them, such as <code>&lt;content-drawer&gt;</code>, through a relative <code>target-ref</code>.</p>\n<p><include-content data-demo=\"command-dialog\"></include-content></p>\n<h4 id=\"md-close-on-enter\">Close on Enter</h4>\n<p><code>keycode-filter</code> gates keyboard handling — here Enter, focused in the\n<code>input</code>, invokes the drawer&#39;s <code>--close</code> command (local / bubbling events only).</p>\n<p><include-content data-demo=\"enter-close\"></include-content></p>\n<h4 id=\"md-escape-to-dismiss-global\">Escape to dismiss (global)</h4>\n<p><code>host-ref=&quot;window&quot;</code> listens for <code>keydown</code> on the window — Escape closes\nthe dialog even when focus is outside it.</p>\n<p><include-content data-demo=\"escape-close\"></include-content></p>\n<h4 id=\"md-cancel-a-link-click\">Cancel a link click</h4>\n<p><code>prevent-default</code> cancels the native action — here the wrapped <code>&lt;a&gt;</code> never navigates.\nUse <code>stop-propagation</code> / <code>stop-immediate-propagation</code> the same way when you need to stop bubbling.</p>\n<p><include-content data-demo=\"prevent-default\"></include-content></p>\n<h4 id=\"md-retarget\">Retarget</h4>\n<p><code>target-ref</code> aims the outgoing event at another element — useful when the target sits outside of the bubble path.</p>\n<p><include-content data-demo=\"retarget\"></include-content></p>\n<h4 id=\"md-debounced-input-form-data\">Debounced input / form data</h4>\n<p>Inherited <code>delay-ms</code> + <code>is-debounced</code> coalesce noisy <code>input</code> into one <code>search-query</code> event. Demo below is debounced every 200ms.</p>\n<p>Also demonstrated is form conversion into JSON: <code>text</code> -&gt; <code>string</code>, <code>checkbox</code> -&gt; <code>boolean</code>, etc. Including the data structure: <code>detail.strict</code> -&gt; <code>{detail: {strict}}</code>.</p>\n<p><include-content data-demo=\"debounce\"></include-content></p>\n",
23
+ "docs": {
24
+ "readme": "<h1 id=\"md-event-handler\">event-handler</h1>\n<p>Stitch behaviors of your app together, primarily through events.</p>\n<p><include-content data-demo=\"with-as\"></include-content></p>\n<h2 id=\"md-features\">Features</h2>\n<ul>\n<li><strong>Fire custom events</strong> Map any input event, such as clicks, or lifecycles to custom output events</li>\n<li><strong>Commands</strong> <code>command-name</code> invokes the HTML Command API — built-in verbs (<code>show-modal</code>) and the <code>--verb</code> commands elements accept (<code>--submit</code>, <code>--close</code>)</li>\n<li><strong>Retarget</strong> Aim events/commands at any selector (<code>target-ref</code>) — where a <code>&lt;button commandfor&gt;</code> needs an id</li>\n<li><strong>Global / host listening</strong> Listen to events globally or on any element. Helpful for: escape to dismiss, global shortcuts, etc.</li>\n<li><strong>Keycode filter</strong> Escape to dismiss, Shift+K shortcuts — keys / modifier chords</li>\n<li><strong>Listen filters</strong> Debounce, selector, and pathname gates</li>\n<li><strong>Custom Event Payloads</strong> <code>detail-*</code> attributes and forms convert to <code>event.detail</code> JSON</li>\n</ul>\n<blockquote>\n<p>In a view that already has a <code>&lt;quark-sheet&gt;</code>, the same wiring is a rule: <code>@on click (target: &quot;[data-add]&quot;) { @dispatch cart-add (detail: (sku: attr(&quot;data-sku&quot;))); }</code> — <code>@on</code> options cover <code>selector-filter</code> / <code>keycode-filter</code> / <code>is-debounced</code> / <code>host-ref</code>, and <code>@dispatch</code> / <code>@command</code> cover <code>fire-event</code> / <code>target-ref</code> / <code>form-ref</code> / <code>command-name</code>. Keep <code>&lt;event-handler&gt;</code> for markup without a sheet.</p>\n</blockquote>\n<h2 id=\"md-installation\">Installation</h2>\n<p><include-content is-active template-ref=\"/views/install-section/install-section.html\"></include-content></p>\n<h2 id=\"md-usage\">Usage</h2>\n<p>Listen (default <code>click</code>), then either <code>fire-event</code> or <code>command-name</code>.</p>\n<include-content data-language=\"html\"><template>&lt;event-handler fire-event=\"cart-add\"&gt;\n Add to cart\n&lt;/event-handler&gt;</template></include-content>\n<p>Or, for example: a successful form submit re-fetches the related data by invoking the provider&#39;s <code>--fetch</code> command.</p>\n<include-content data-language=\"html\"><template>&lt;event-handler listen-for=\"super-form-success\" target-ref=\"#fetch-todos\" command-name=\"--fetch\"&gt;\n &lt;super-form&gt;\n &lt;form&gt;\n &lt;!-- form to create a new todo --&gt;\n &lt;/form&gt;\n &lt;/super-form&gt;\n&lt;/event-handler&gt;</template></include-content>\n<p>In a Nucleus Stack application, this will be one of the most heavily used elements. It is the primary method of linking a functional cause and effect.</p>\n<h3 id=\"md-api-reference\">API Reference</h3>\n<p><include-content is-active template-ref=\"/views/api-reference/api-reference.html\"></include-content></p>\n<h3 id=\"md-examples\">Examples</h3>\n<h4 id=\"md-fire-a-custom-event\">Fire a custom event</h4>\n<p>Click dispatches <code>cart-add</code> event with <code>{ sku: &quot;sku-1&quot; }</code> in <code>event.detail</code>.</p>\n<p><include-content data-demo=\"fire-event\"></include-content></p>\n<h4 id=\"md-open-a-dialog\">Open a dialog</h4>\n<p><code>command-name</code> invokes the HTML Command API — here the built-in <code>show-modal</code> opens the sibling <code>&lt;dialog&gt;</code>. Custom commands (<code>command-name=&quot;--close&quot;</code>) reach any element that handles them, such as <code>&lt;content-drawer&gt;</code>, through a relative <code>target-ref</code>.</p>\n<p><include-content data-demo=\"command-dialog\"></include-content></p>\n<h4 id=\"md-close-on-enter\">Close on Enter</h4>\n<p><code>keycode-filter</code> gates keyboard handling — here Enter, focused in the\n<code>input</code>, invokes the drawer&#39;s <code>--close</code> command (local / bubbling events only).</p>\n<p><include-content data-demo=\"enter-close\"></include-content></p>\n<h4 id=\"md-escape-to-dismiss-global\">Escape to dismiss (global)</h4>\n<p><code>host-ref=&quot;window&quot;</code> listens for <code>keydown</code> on the window — Escape closes\nthe dialog even when focus is outside it.</p>\n<p><include-content data-demo=\"escape-close\"></include-content></p>\n<h4 id=\"md-cancel-a-link-click\">Cancel a link click</h4>\n<p><code>prevent-default</code> cancels the native action — here the wrapped <code>&lt;a&gt;</code> never navigates.\nUse <code>stop-propagation</code> / <code>stop-immediate-propagation</code> the same way when you need to stop bubbling.</p>\n<p><include-content data-demo=\"prevent-default\"></include-content></p>\n<h4 id=\"md-retarget\">Retarget</h4>\n<p><code>target-ref</code> aims the outgoing event at another element — useful when the target sits outside of the bubble path.</p>\n<p><include-content data-demo=\"retarget\"></include-content></p>\n<h4 id=\"md-debounced-input-form-data\">Debounced input / form data</h4>\n<p>Inherited <code>delay-ms</code> + <code>is-debounced</code> coalesce noisy <code>input</code> into one <code>search-query</code> event. Demo below is debounced every 200ms.</p>\n<p>Also demonstrated is form conversion into JSON: <code>text</code> -&gt; <code>string</code>, <code>checkbox</code> -&gt; <code>boolean</code>, etc. Including the data structure: <code>detail.strict</code> -&gt; <code>{detail: {strict}}</code>.</p>\n<p><include-content data-demo=\"debounce\"></include-content></p>\n"
25
+ },
26
+ "installation": {
27
+ "name": "@excom/event-handler",
28
+ "shortName": "event-handler",
29
+ "version": "0.1.0",
30
+ "description": "<event-handler> custom element",
31
+ "packageType": "kit-element",
32
+ "cdn": "<script src=\"https://unpkg.com/@excom/kit-utils/dist/index.umd.min.js\"></script>\n<script src=\"https://unpkg.com/@excom/neutron/dist/index.umd.min.js\"></script>\n<script src=\"https://unpkg.com/@excom/event-handler@0.1.0/dist/index.umd.min.js\"></script>\n<link rel=\"stylesheet\" href=\"https://unpkg.com/@excom/event-handler@0.1.0/dist/index.css\">",
33
+ "install": {
34
+ "npm": "npm install @excom/event-handler"
35
+ },
36
+ "imports": {
37
+ "js": "import \"@excom/event-handler\";",
38
+ "css": "@import \"@excom/event-handler\";",
39
+ "html": "<!-- import path to `node_modules` will depend on your build setup -->\n<script type=\"module\" src=\"/node_modules/@excom/event-handler\"></script>\n<link rel=\"stylesheet\" href=\"/node_modules/@excom/event-handler\">"
40
+ },
41
+ "peerDependencies": []
42
+ },
43
+ "elementApis": [
44
+ {
45
+ "tag": "event-handler",
46
+ "summary": "Rewire clicks / inputs into events or commands.",
47
+ "kind": "class",
48
+ "attributes": [
49
+ {
50
+ "name": "command-name",
51
+ "type": "tokenlist",
52
+ "description": "Space-separated commands to invoke on the target: custom <code>--verb</code> commands (<code>--submit</code>, <code>--close</code>) dispatch a <code>command</code> event, as a <code>&lt;button command commandfor&gt;</code> would; built-in verbs (<code>show-modal</code>, <code>close</code>, <code>toggle-popover</code>) run through the platform.",
53
+ "fieldName": "commandName",
54
+ "surface": "option",
55
+ "values": "<command>…"
56
+ },
57
+ {
58
+ "name": "delay-ms",
59
+ "type": "number",
60
+ "description": "Delay handling by this many milliseconds.",
61
+ "fieldName": "delayMs",
62
+ "surface": "option",
63
+ "inheritedFrom": "@excom/listenable-element"
64
+ },
65
+ {
66
+ "name": "fire-event",
67
+ "type": "tokenlist",
68
+ "description": "Space-separated event names to dispatch on the target. Each name gets the same merged <code>detail</code>. Ignored when <code>mutate-target</code> is set. Pair with <code>detail-*</code> attributes for static detail fields.",
69
+ "fieldName": "fireEvent",
70
+ "surface": "option",
71
+ "values": "<EventName>…"
72
+ },
73
+ {
74
+ "name": "form-ref",
75
+ "type": "string",
76
+ "description": "<code>&lt;form&gt;</code> whose fields merge into event <code>detail</code>, or become attributes when <code>mutate-target</code> is set.",
77
+ "fieldName": "formRef",
78
+ "surface": "option",
79
+ "values": "<CSS Selector>"
80
+ },
81
+ {
82
+ "name": "host-ref",
83
+ "type": "string",
84
+ "description": "Listen on another element / <code>window</code> / <code>document</code> — e.g. Escape to dismiss a dialog from a global <code>keydown</code>. Defaults to <code>:scope</code>. Used with <code>listen-for</code>. Not compatible with <code>listen-for-lifecycle</code>. The selector MUST resolve when <code>host-ref</code> is set — it will not wait for a match to appear.",
85
+ "fieldName": "hostRef",
86
+ "surface": "option",
87
+ "values": "<CSS Selector> | \"window\" | \"document\" | \"html\" | \"body\" | \"head\"",
88
+ "inheritedFrom": "@excom/listenable-element"
89
+ },
90
+ {
91
+ "name": "is-debounced",
92
+ "type": "boolean",
93
+ "description": "With <code>delay-ms</code>, coalesce bursts into one trailing call (debounce).",
94
+ "fieldName": "isDebounced",
95
+ "surface": "option",
96
+ "inheritedFrom": "@excom/listenable-element"
97
+ },
98
+ {
99
+ "name": "keycode-filter",
100
+ "type": "tokenlist",
101
+ "description": "Space-separated key filters (OR). Join modifiers with <code>+</code> (AND, any order): <code>shift+k tab</code> → Shift+K or Tab. Modifiers: <code>shift</code>, <code>alt</code>, <code>ctrl</code>/<code>control</code>, <code>meta</code>. Case-insensitive.",
102
+ "fieldName": "keycodeFilter",
103
+ "surface": "option",
104
+ "values": "<key | mod+key>…",
105
+ "inheritedFrom": "@excom/listenable-element"
106
+ },
107
+ {
108
+ "name": "listen-for",
109
+ "type": "tokenlist",
110
+ "description": "Space-separated event names to listen for. Defaults to <code>click</code> when unset (and no lifecycle list is set).",
111
+ "fieldName": "listenFor",
112
+ "surface": "option",
113
+ "values": "<EventName>…",
114
+ "inheritedFrom": "@excom/listenable-element"
115
+ },
116
+ {
117
+ "name": "listen-for-lifecycle",
118
+ "type": "tokenlist",
119
+ "description": "Space-separated element lifecycles to handle.",
120
+ "fieldName": "listenForLifecycle",
121
+ "surface": "option",
122
+ "values": "\"connected\" | \"disconnected\" | \"adopted\"",
123
+ "inheritedFrom": "@excom/listenable-element"
124
+ },
125
+ {
126
+ "name": "listen-once",
127
+ "type": "boolean",
128
+ "description": "Handle each distinct event name / lifecycle at most once.",
129
+ "fieldName": "listenOnce",
130
+ "surface": "option",
131
+ "inheritedFrom": "@excom/listenable-element"
132
+ },
133
+ {
134
+ "name": "mutate-target",
135
+ "type": "boolean",
136
+ "description": "@deprecated Write attributes on the target instead of firing events (sources: <code>attr-*</code> on this element plus <code>form-ref</code> fields). Use a Quark <code>@on &lt;event&gt; { … }</code> block instead — it writes State from the event without one element mutating another. Kept for compatibility; will be removed in a future major.",
137
+ "fieldName": "mutateTarget",
138
+ "surface": "option"
139
+ },
140
+ {
141
+ "name": "not-bubbles",
142
+ "type": "boolean",
143
+ "description": "Outgoing events use <code>bubbles: false</code> (default: bubble).",
144
+ "fieldName": "notBubbles",
145
+ "surface": "option"
146
+ },
147
+ {
148
+ "name": "not-cancelable",
149
+ "type": "boolean",
150
+ "description": "Outgoing events use <code>cancelable: false</code> (default: cancelable).",
151
+ "fieldName": "notCancelable",
152
+ "surface": "option"
153
+ },
154
+ {
155
+ "name": "not-composed",
156
+ "type": "boolean",
157
+ "description": "Outgoing events use <code>composed: false</code> (default: composed / cross shadow roots).",
158
+ "fieldName": "notComposed",
159
+ "surface": "option"
160
+ },
161
+ {
162
+ "name": "pathname-filter",
163
+ "type": "tokenlist",
164
+ "description": "Only handle when <code>location.pathname</code> is one of these values — route-aware behaviors without a separate router element.",
165
+ "fieldName": "pathnameFilter",
166
+ "surface": "option",
167
+ "values": "<pathname>…",
168
+ "inheritedFrom": "@excom/listenable-element"
169
+ },
170
+ {
171
+ "name": "prevent-default",
172
+ "type": "boolean",
173
+ "description": "Call <code>preventDefault()</code> on matched events (ignored for lifecycles).",
174
+ "fieldName": "preventDefault",
175
+ "surface": "option",
176
+ "inheritedFrom": "@excom/listenable-element"
177
+ },
178
+ {
179
+ "name": "selector-filter",
180
+ "type": "string",
181
+ "description": "Only handle events whose <code>event.target</code> matches this CSS selector. Does not support <code>:scope</code> in the selector.",
182
+ "fieldName": "selectorFilter",
183
+ "surface": "option",
184
+ "values": "<CSS Selector>",
185
+ "inheritedFrom": "@excom/listenable-element"
186
+ },
187
+ {
188
+ "name": "stop-immediate-propagation",
189
+ "type": "boolean",
190
+ "description": "Call <code>stopImmediatePropagation()</code> on matched events (ignored for lifecycles).",
191
+ "fieldName": "stopImmediatePropagation",
192
+ "surface": "option",
193
+ "inheritedFrom": "@excom/listenable-element"
194
+ },
195
+ {
196
+ "name": "stop-propagation",
197
+ "type": "boolean",
198
+ "description": "Call <code>stopPropagation()</code> on matched events (ignored for lifecycles).",
199
+ "fieldName": "stopPropagation",
200
+ "surface": "option",
201
+ "inheritedFrom": "@excom/listenable-element"
202
+ },
203
+ {
204
+ "name": "target-ref",
205
+ "type": "string",
206
+ "description": "Where outgoing events / mutations / commands apply. Unset = this element. Supports <code>:scope</code> for relative targeting (e.g. <code>:scope ~ dialog</code>).",
207
+ "fieldName": "targetRef",
208
+ "surface": "option",
209
+ "values": "<CSS Selector>"
210
+ },
211
+ {
212
+ "name": "vibrate-ms",
213
+ "type": "number",
214
+ "description": "Vibrate on handle (<code>navigator.vibrate</code>). Empty / <code>0</code> uses a 20ms pulse.",
215
+ "fieldName": "vibrateMs",
216
+ "surface": "option",
217
+ "default": "\"20 (when attribute is present with no value)\"",
218
+ "inheritedFrom": "@excom/listenable-element"
219
+ }
220
+ ],
221
+ "events": [],
222
+ "slots": [],
223
+ "cssProperties": [],
224
+ "cssClasses": [
225
+ {
226
+ "name": "unstyled",
227
+ "description": "Skip the pointer cursor from the listenable mixin."
228
+ }
229
+ ],
230
+ "cssAliases": [
231
+ {
232
+ "name": ":--event-handler",
233
+ "selectors": [
234
+ "event-handler",
235
+ ".tag-event-handler"
236
+ ],
237
+ "kind": "element",
238
+ "description": ""
239
+ }
240
+ ],
241
+ "listens": [],
242
+ "commands": [],
243
+ "defaultActions": [],
244
+ "expectedChildren": [],
245
+ "provisions": []
246
+ }
247
+ ],
248
+ "exportedFiles": {}
249
+ }
@@ -0,0 +1,23 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`debounce view > emits search-query after the debounce window > complexity 1`] = `
4
+ {
5
+ "attributeRuns": 0,
6
+ "closest": 0,
7
+ "getVar": 0,
8
+ "importNode": 0,
9
+ "listenerRuns": 0,
10
+ "matches": 0,
11
+ "parentElement": 0,
12
+ "quarkRuns": 0,
13
+ "queryScopeCost": 0,
14
+ "querySelectorAll": 1,
15
+ "removeAttribute": 1,
16
+ "ruleRuns": 0,
17
+ "schedulePaint": 0,
18
+ "setAttribute": 1,
19
+ "setVar": 0,
20
+ "textContent": 1,
21
+ "variableRuns": 0,
22
+ }
23
+ `;
@@ -0,0 +1,23 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`fire-event view > writes cart-add detail to output > complexity 1`] = `
4
+ {
5
+ "attributeRuns": 0,
6
+ "closest": 0,
7
+ "getVar": 0,
8
+ "importNode": 0,
9
+ "listenerRuns": 0,
10
+ "matches": 0,
11
+ "parentElement": 0,
12
+ "quarkRuns": 0,
13
+ "queryScopeCost": 0,
14
+ "querySelectorAll": 0,
15
+ "removeAttribute": 0,
16
+ "ruleRuns": 0,
17
+ "schedulePaint": 0,
18
+ "setAttribute": 0,
19
+ "setVar": 0,
20
+ "textContent": 1,
21
+ "variableRuns": 0,
22
+ }
23
+ `;
@@ -0,0 +1,23 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`retarget view > pings the previous output > complexity 1`] = `
4
+ {
5
+ "attributeRuns": 0,
6
+ "closest": 0,
7
+ "getVar": 0,
8
+ "importNode": 0,
9
+ "listenerRuns": 0,
10
+ "matches": 0,
11
+ "parentElement": 0,
12
+ "quarkRuns": 0,
13
+ "queryScopeCost": 0,
14
+ "querySelectorAll": 0,
15
+ "removeAttribute": 1,
16
+ "ruleRuns": 0,
17
+ "schedulePaint": 0,
18
+ "setAttribute": 1,
19
+ "setVar": 0,
20
+ "textContent": 1,
21
+ "variableRuns": 0,
22
+ }
23
+ `;
@@ -0,0 +1,34 @@
1
+ import "../../index";
2
+ import {
3
+ afterEach,
4
+ describe,
5
+ expect,
6
+ it,
7
+ } from "@excom/heft-rig/profiles/default/config/test-utils";
8
+ import {
9
+ click,
10
+ flush,
11
+ mountView,
12
+ readDemo,
13
+ } from "@excom/quark/support/tests/view-helpers";
14
+
15
+ describe("command-dialog view", () => {
16
+ afterEach(() => {
17
+ document.body.innerHTML = "";
18
+ });
19
+
20
+ it("wires show-modal at the sibling dialog", async () => {
21
+ const { root } = await mountView(readDemo(import.meta.url, "command-dialog"));
22
+ const opener = root.querySelector("event-handler")!;
23
+ const dialog = root.querySelector("dialog")!;
24
+ expect(opener.getAttribute("command-name")).toBe("show-modal");
25
+ expect(opener.getAttribute("target-ref")).toBe(":scope + dialog");
26
+ expect(dialog.open).toBe(false);
27
+
28
+ click(opener);
29
+ await flush();
30
+ // happy-dom has no Command Invokers API (`button.command` /
31
+ // `commandForElement`), which is how event-handler invokes commands.
32
+ expect(dialog.open).toBe(false);
33
+ });
34
+ });
@@ -0,0 +1,43 @@
1
+ import "@excom/quark-sheet";
2
+ import "../../index";
3
+ import {
4
+ afterEach,
5
+ beforeEach,
6
+ describe,
7
+ expect,
8
+ it,
9
+ wait,
10
+ } from "@excom/heft-rig/profiles/default/config/test-utils";
11
+ import {
12
+ expectComplexity,
13
+ flush,
14
+ installDemoModules,
15
+ measureComplexity,
16
+ mountView,
17
+ readDemo,
18
+ restoreDemoModules,
19
+ } from "@excom/quark/support/tests/view-helpers";
20
+
21
+ describe("debounce view", () => {
22
+ beforeEach(() => installDemoModules());
23
+ afterEach(() => {
24
+ document.body.innerHTML = "";
25
+ restoreDemoModules();
26
+ });
27
+
28
+ it("emits search-query after the debounce window", async () => {
29
+ const { root, quark } = await mountView(readDemo(import.meta.url, "debounce"));
30
+ const input = root.querySelector<HTMLInputElement>('input[name="detail.q"]')!;
31
+ input.value = "oak";
32
+ const meter = measureComplexity(quark!);
33
+ input.dispatchEvent(new Event("input", { bubbles: true }));
34
+ await flush();
35
+ expect(root.querySelector("output")?.textContent).toBe("Waiting…");
36
+ await wait(220);
37
+ await flush();
38
+ const budget = meter.take();
39
+ meter.stop();
40
+ expect(root.querySelector("output")?.textContent).toMatch(/oak/);
41
+ expectComplexity(budget);
42
+ });
43
+ });
@@ -0,0 +1,35 @@
1
+ import "@excom/content-drawer";
2
+ import "../../index";
3
+ import {
4
+ afterEach,
5
+ describe,
6
+ expect,
7
+ it,
8
+ } from "@excom/heft-rig/profiles/default/config/test-utils";
9
+ import {
10
+ click,
11
+ flush,
12
+ mountView,
13
+ readDemo,
14
+ } from "@excom/quark/support/tests/view-helpers";
15
+
16
+ describe("enter-close view", () => {
17
+ afterEach(() => {
18
+ document.body.innerHTML = "";
19
+ });
20
+
21
+ it("opens the drawer and closes on Enter", async () => {
22
+ const { root } = await mountView(readDemo(import.meta.url, "enter-close"));
23
+ const drawer = root.querySelector("content-drawer")!;
24
+ expect(drawer.hasAttribute("is-open")).toBe(true);
25
+ const input = root.querySelector("input")!;
26
+ input.dispatchEvent(
27
+ new KeyboardEvent("keydown", { bubbles: true, key: "Enter", keyCode: 13 }),
28
+ );
29
+ await flush();
30
+ expect(drawer.hasAttribute("is-open")).toBe(false);
31
+ click(root.querySelector("event-handler"));
32
+ await flush();
33
+ expect(drawer.hasAttribute("is-open")).toBe(true);
34
+ });
35
+ });
@@ -0,0 +1,44 @@
1
+ import "../../index";
2
+ import {
3
+ afterEach,
4
+ describe,
5
+ expect,
6
+ it,
7
+ } from "@excom/heft-rig/profiles/default/config/test-utils";
8
+ import {
9
+ click,
10
+ flush,
11
+ mountView,
12
+ readDemo,
13
+ } from "@excom/quark/support/tests/view-helpers";
14
+
15
+ describe("escape-close view", () => {
16
+ afterEach(() => {
17
+ document.body.innerHTML = "";
18
+ });
19
+
20
+ it("wires show-modal and Escape-to-close commands", async () => {
21
+ const { root } = await mountView(readDemo(import.meta.url, "escape-close"));
22
+ const [opener, closer] = [...root.querySelectorAll("event-handler")];
23
+ const dialog = root.querySelector("dialog")!;
24
+ expect(opener.getAttribute("command-name")).toBe("show-modal");
25
+ expect(closer.getAttribute("command-name")).toBe("close");
26
+ expect(closer.getAttribute("host-ref")).toBe("window");
27
+ expect(closer.getAttribute("keycode-filter")).toBe("escape");
28
+
29
+ click(opener);
30
+ await flush();
31
+ // Same Command Invokers gap as command-dialog: click and Escape
32
+ // don't move `dialog.open` under happy-dom.
33
+ expect(dialog.open).toBe(false);
34
+ window.dispatchEvent(
35
+ new KeyboardEvent("keydown", {
36
+ bubbles: true,
37
+ key: "Escape",
38
+ keyCode: 27,
39
+ }),
40
+ );
41
+ await flush();
42
+ expect(dialog.open).toBe(false);
43
+ });
44
+ });
@@ -0,0 +1,49 @@
1
+ import { vi } from "vitest";
2
+ import {
3
+ afterEach,
4
+ describe,
5
+ expect,
6
+ fixture,
7
+ it,
8
+ } from "@excom/heft-rig/profiles/default/config/test-utils";
9
+
10
+ /*
11
+ * `mutate-target` guards a form that yields no data. `formToJson` always
12
+ * returns an object for a real form, so stub the helper to reach the guard.
13
+ */
14
+ vi.mock("@excom/kit-utils", async (importOriginal) => {
15
+ const actual =
16
+ await importOriginal<typeof import("@excom/kit-utils")>();
17
+ return {
18
+ ...actual,
19
+ formToJson: vi.fn(() => null),
20
+ };
21
+ });
22
+
23
+ import "../../index";
24
+
25
+ describe("event-handler mutate-target with an empty form result", () => {
26
+ afterEach(() => {
27
+ document.body.innerHTML = "";
28
+ vi.restoreAllMocks();
29
+ });
30
+
31
+ it("still writes attr-* attributes when the form yields nothing", () => {
32
+ fixture<HTMLDivElement>(
33
+ `<div id="mutate-empty-form"></div>
34
+ <event-handler
35
+ mutate-target
36
+ target-ref="#mutate-empty-form"
37
+ listen-for="my-event"
38
+ form-ref="form"
39
+ attr-data-state="open">
40
+ <form><input name="data-ignored" value="x" type="text"></form>
41
+ </event-handler>`,
42
+ );
43
+ const eventHandler = document.querySelector("event-handler")!;
44
+ eventHandler.dispatchEvent(new CustomEvent("my-event"));
45
+ const div = document.querySelector("#mutate-empty-form")!;
46
+ expect(div.getAttribute("data-state")).toBe("open");
47
+ expect(div.hasAttribute("data-ignored")).toBe(false);
48
+ });
49
+ });