stimeo-ui 0.2.1 → 0.4.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 (219) hide show
  1. package/CHANGELOG.md +160 -0
  2. package/dist/cable/index.d.ts +16 -16
  3. package/dist/cable/index.js.map +1 -1
  4. package/dist/controllers/accordion_controller.js +10 -0
  5. package/dist/controllers/accordion_controller.js.map +1 -1
  6. package/dist/controllers/alert_dialog_controller.js +32 -5
  7. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  8. package/dist/controllers/announcer_controller.d.ts +32 -6
  9. package/dist/controllers/announcer_controller.js +255 -20
  10. package/dist/controllers/announcer_controller.js.map +1 -1
  11. package/dist/controllers/breadcrumb_controller.d.ts +97 -10
  12. package/dist/controllers/breadcrumb_controller.js +225 -13
  13. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  14. package/dist/controllers/calendar_controller.d.ts +11 -1
  15. package/dist/controllers/calendar_controller.js +89 -22
  16. package/dist/controllers/calendar_controller.js.map +1 -1
  17. package/dist/controllers/carousel_controller.d.ts +16 -1
  18. package/dist/controllers/carousel_controller.js +47 -6
  19. package/dist/controllers/carousel_controller.js.map +1 -1
  20. package/dist/controllers/collapsible_controller.js +2 -2
  21. package/dist/controllers/collapsible_controller.js.map +1 -1
  22. package/dist/controllers/color_picker_controller.d.ts +14 -2
  23. package/dist/controllers/color_picker_controller.js +92 -7
  24. package/dist/controllers/color_picker_controller.js.map +1 -1
  25. package/dist/controllers/combobox_controller.d.ts +14 -1
  26. package/dist/controllers/combobox_controller.js +162 -23
  27. package/dist/controllers/combobox_controller.js.map +1 -1
  28. package/dist/controllers/command_palette_controller.d.ts +2 -0
  29. package/dist/controllers/command_palette_controller.js +226 -22
  30. package/dist/controllers/command_palette_controller.js.map +1 -1
  31. package/dist/controllers/confirm_controller.js +32 -5
  32. package/dist/controllers/confirm_controller.js.map +1 -1
  33. package/dist/controllers/context_menu_controller.d.ts +7 -6
  34. package/dist/controllers/context_menu_controller.js +32 -10
  35. package/dist/controllers/context_menu_controller.js.map +1 -1
  36. package/dist/controllers/count_up_controller.js.map +1 -1
  37. package/dist/controllers/countdown_controller.d.ts +19 -5
  38. package/dist/controllers/countdown_controller.js +112 -12
  39. package/dist/controllers/countdown_controller.js.map +1 -1
  40. package/dist/controllers/data_grid_controller.d.ts +19 -1
  41. package/dist/controllers/data_grid_controller.js +82 -4
  42. package/dist/controllers/data_grid_controller.js.map +1 -1
  43. package/dist/controllers/date_range_picker_controller.d.ts +5 -1
  44. package/dist/controllers/date_range_picker_controller.js +77 -3
  45. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  46. package/dist/controllers/dialog_controller.js +32 -5
  47. package/dist/controllers/dialog_controller.js.map +1 -1
  48. package/dist/controllers/dismissible_controller.js.map +1 -1
  49. package/dist/controllers/drawer_controller.js +32 -5
  50. package/dist/controllers/drawer_controller.js.map +1 -1
  51. package/dist/controllers/dropdown_controller.d.ts +1 -2
  52. package/dist/controllers/dropdown_controller.js.map +1 -1
  53. package/dist/controllers/editable_controller.js +1 -0
  54. package/dist/controllers/editable_controller.js.map +1 -1
  55. package/dist/controllers/empty_state_controller.d.ts +9 -4
  56. package/dist/controllers/empty_state_controller.js +24 -10
  57. package/dist/controllers/empty_state_controller.js.map +1 -1
  58. package/dist/controllers/focus_controller.d.ts +1 -1
  59. package/dist/controllers/focus_controller.js +32 -5
  60. package/dist/controllers/focus_controller.js.map +1 -1
  61. package/dist/controllers/form_field_controller.js.map +1 -1
  62. package/dist/controllers/form_validation_controller.js +1 -1
  63. package/dist/controllers/form_validation_controller.js.map +1 -1
  64. package/dist/controllers/frame_loading_controller.d.ts +14 -3
  65. package/dist/controllers/frame_loading_controller.js +177 -15
  66. package/dist/controllers/frame_loading_controller.js.map +1 -1
  67. package/dist/controllers/highlight_controller.js.map +1 -1
  68. package/dist/controllers/hover_card_controller.d.ts +1 -1
  69. package/dist/controllers/hover_card_controller.js.map +1 -1
  70. package/dist/controllers/intersection_controller.js +36 -11
  71. package/dist/controllers/intersection_controller.js.map +1 -1
  72. package/dist/controllers/lazy_frame_controller.js +31 -10
  73. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  74. package/dist/controllers/listbox_controller.d.ts +9 -2
  75. package/dist/controllers/listbox_controller.js +257 -53
  76. package/dist/controllers/listbox_controller.js.map +1 -1
  77. package/dist/controllers/local_time_controller.d.ts +19 -3
  78. package/dist/controllers/local_time_controller.js +102 -8
  79. package/dist/controllers/local_time_controller.js.map +1 -1
  80. package/dist/controllers/menu_controller.d.ts +29 -11
  81. package/dist/controllers/menu_controller.js +104 -17
  82. package/dist/controllers/menu_controller.js.map +1 -1
  83. package/dist/controllers/menubar_controller.d.ts +91 -11
  84. package/dist/controllers/menubar_controller.js +415 -63
  85. package/dist/controllers/menubar_controller.js.map +1 -1
  86. package/dist/controllers/meter_controller.d.ts +22 -2
  87. package/dist/controllers/meter_controller.js +145 -26
  88. package/dist/controllers/meter_controller.js.map +1 -1
  89. package/dist/controllers/multi_select_controller.d.ts +31 -5
  90. package/dist/controllers/multi_select_controller.js +312 -29
  91. package/dist/controllers/multi_select_controller.js.map +1 -1
  92. package/dist/controllers/navigation_menu_controller.d.ts +65 -18
  93. package/dist/controllers/navigation_menu_controller.js +154 -27
  94. package/dist/controllers/navigation_menu_controller.js.map +1 -1
  95. package/dist/controllers/nested_form_controller.d.ts +4 -5
  96. package/dist/controllers/nested_form_controller.js.map +1 -1
  97. package/dist/controllers/network_status_controller.d.ts +13 -1
  98. package/dist/controllers/network_status_controller.js +28 -8
  99. package/dist/controllers/network_status_controller.js.map +1 -1
  100. package/dist/controllers/number_input_controller.d.ts +1 -1
  101. package/dist/controllers/number_input_controller.js +7 -0
  102. package/dist/controllers/number_input_controller.js.map +1 -1
  103. package/dist/controllers/otp_controller.js +18 -1
  104. package/dist/controllers/otp_controller.js.map +1 -1
  105. package/dist/controllers/overflow_indicator_controller.d.ts +3 -2
  106. package/dist/controllers/overflow_indicator_controller.js +81 -13
  107. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  108. package/dist/controllers/overflow_menu_controller.d.ts +78 -19
  109. package/dist/controllers/overflow_menu_controller.js +408 -57
  110. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  111. package/dist/controllers/pagination_controller.d.ts +36 -5
  112. package/dist/controllers/pagination_controller.js +163 -32
  113. package/dist/controllers/pagination_controller.js.map +1 -1
  114. package/dist/controllers/persist_controller.js +6 -6
  115. package/dist/controllers/persist_controller.js.map +1 -1
  116. package/dist/controllers/pointer_drag_controller.d.ts +2 -3
  117. package/dist/controllers/pointer_drag_controller.js +9 -1
  118. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  119. package/dist/controllers/popover_controller.d.ts +1 -2
  120. package/dist/controllers/popover_controller.js +2 -2
  121. package/dist/controllers/popover_controller.js.map +1 -1
  122. package/dist/controllers/portal_controller.d.ts +4 -4
  123. package/dist/controllers/portal_controller.js.map +1 -1
  124. package/dist/controllers/progress_controller.d.ts +16 -1
  125. package/dist/controllers/progress_controller.js +116 -9
  126. package/dist/controllers/progress_controller.js.map +1 -1
  127. package/dist/controllers/radio_group_controller.js +22 -3
  128. package/dist/controllers/radio_group_controller.js.map +1 -1
  129. package/dist/controllers/range_slider_controller.d.ts +18 -5
  130. package/dist/controllers/range_slider_controller.js +100 -9
  131. package/dist/controllers/range_slider_controller.js.map +1 -1
  132. package/dist/controllers/rating_controller.d.ts +4 -1
  133. package/dist/controllers/rating_controller.js +69 -2
  134. package/dist/controllers/rating_controller.js.map +1 -1
  135. package/dist/controllers/read_more_controller.js +63 -19
  136. package/dist/controllers/read_more_controller.js.map +1 -1
  137. package/dist/controllers/reading_progress_controller.js.map +1 -1
  138. package/dist/controllers/relative_time_controller.d.ts +13 -0
  139. package/dist/controllers/relative_time_controller.js +133 -12
  140. package/dist/controllers/relative_time_controller.js.map +1 -1
  141. package/dist/controllers/resizable_controller.d.ts +3 -1
  142. package/dist/controllers/resizable_controller.js +65 -1
  143. package/dist/controllers/resizable_controller.js.map +1 -1
  144. package/dist/controllers/roving_controller.js +17 -2
  145. package/dist/controllers/roving_controller.js.map +1 -1
  146. package/dist/controllers/scroll_area_controller.js +86 -12
  147. package/dist/controllers/scroll_area_controller.js.map +1 -1
  148. package/dist/controllers/scroll_restore_controller.js +1 -1
  149. package/dist/controllers/scroll_restore_controller.js.map +1 -1
  150. package/dist/controllers/scroll_visibility_controller.d.ts +6 -3
  151. package/dist/controllers/scroll_visibility_controller.js +33 -3
  152. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  153. package/dist/controllers/scrollspy_controller.d.ts +64 -15
  154. package/dist/controllers/scrollspy_controller.js +346 -73
  155. package/dist/controllers/scrollspy_controller.js.map +1 -1
  156. package/dist/controllers/separator_controller.d.ts +20 -5
  157. package/dist/controllers/separator_controller.js +9 -0
  158. package/dist/controllers/separator_controller.js.map +1 -1
  159. package/dist/controllers/sidebar_controller.d.ts +2 -12
  160. package/dist/controllers/sidebar_controller.js +37 -8
  161. package/dist/controllers/sidebar_controller.js.map +1 -1
  162. package/dist/controllers/skeleton_controller.d.ts +7 -2
  163. package/dist/controllers/skeleton_controller.js +73 -20
  164. package/dist/controllers/skeleton_controller.js.map +1 -1
  165. package/dist/controllers/slider_controller.d.ts +11 -1
  166. package/dist/controllers/slider_controller.js +49 -8
  167. package/dist/controllers/slider_controller.js.map +1 -1
  168. package/dist/controllers/sortable_controller.d.ts +3 -1
  169. package/dist/controllers/sortable_controller.js +34 -3
  170. package/dist/controllers/sortable_controller.js.map +1 -1
  171. package/dist/controllers/spinner_controller.d.ts +37 -4
  172. package/dist/controllers/spinner_controller.js +228 -27
  173. package/dist/controllers/spinner_controller.js.map +1 -1
  174. package/dist/controllers/step_indicator_controller.d.ts +12 -1
  175. package/dist/controllers/step_indicator_controller.js +82 -5
  176. package/dist/controllers/step_indicator_controller.js.map +1 -1
  177. package/dist/controllers/stick_to_bottom_controller.d.ts +31 -8
  178. package/dist/controllers/stick_to_bottom_controller.js +61 -10
  179. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  180. package/dist/controllers/sticky_observer_controller.js +32 -11
  181. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  182. package/dist/controllers/submit_once_controller.d.ts +2 -2
  183. package/dist/controllers/submit_once_controller.js.map +1 -1
  184. package/dist/controllers/switch_controller.js +1 -0
  185. package/dist/controllers/switch_controller.js.map +1 -1
  186. package/dist/controllers/tabs_controller.d.ts +11 -3
  187. package/dist/controllers/tabs_controller.js +26 -3
  188. package/dist/controllers/tabs_controller.js.map +1 -1
  189. package/dist/controllers/tags_input_controller.js +22 -2
  190. package/dist/controllers/tags_input_controller.js.map +1 -1
  191. package/dist/controllers/theme_controller.d.ts +1 -1
  192. package/dist/controllers/theme_controller.js +22 -3
  193. package/dist/controllers/theme_controller.js.map +1 -1
  194. package/dist/controllers/time_picker_controller.js +20 -1
  195. package/dist/controllers/time_picker_controller.js.map +1 -1
  196. package/dist/controllers/toast_controller.d.ts +4 -5
  197. package/dist/controllers/toast_controller.js +4 -5
  198. package/dist/controllers/toast_controller.js.map +1 -1
  199. package/dist/controllers/toggle_group_controller.js +23 -2
  200. package/dist/controllers/toggle_group_controller.js.map +1 -1
  201. package/dist/controllers/toolbar_controller.d.ts +53 -13
  202. package/dist/controllers/toolbar_controller.js +230 -31
  203. package/dist/controllers/toolbar_controller.js.map +1 -1
  204. package/dist/controllers/tooltip_controller.d.ts +1 -1
  205. package/dist/controllers/tooltip_controller.js.map +1 -1
  206. package/dist/controllers/transition_controller.js.map +1 -1
  207. package/dist/controllers/tree_view_controller.d.ts +87 -6
  208. package/dist/controllers/tree_view_controller.js +467 -51
  209. package/dist/controllers/tree_view_controller.js.map +1 -1
  210. package/dist/index.js +4507 -907
  211. package/dist/index.js.map +1 -1
  212. package/dist/inspector/cli.d.ts +375 -39
  213. package/dist/inspector/cli.js +1009 -86
  214. package/dist/inspector/cli.js.map +1 -1
  215. package/dist/inspector/cli_bin.js +1200 -120
  216. package/dist/inspector/cli_bin.js.map +1 -1
  217. package/dist/inspector/examples.json +28 -28
  218. package/dist/inspector/manifest.json +1060 -132
  219. package/package.json +5 -6
@@ -2,6 +2,23 @@ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  // src/controllers/network_status_controller.ts
4
4
 
5
+ // src/utils/announce.ts
6
+ function announce(message, options = {}) {
7
+ const text = message.trim();
8
+ if (text.length === 0) return;
9
+ window.dispatchEvent(
10
+ new CustomEvent("stimeo--announcer:announce", {
11
+ detail: { message: text, assertive: options.assertive === true }
12
+ })
13
+ );
14
+ }
15
+ function fillTemplate(template, values) {
16
+ return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
17
+ const replacement = values[name];
18
+ return replacement === void 0 ? match : String(replacement);
19
+ });
20
+ }
21
+
5
22
  // src/utils/safe_timeout.ts
6
23
  var TimerRegistry = class {
7
24
  /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
@@ -59,20 +76,17 @@ var SafeTimeout = class extends TimerRegistry {
59
76
  var NetworkStatusController = class extends Controller {
60
77
  static targets = ["offline", "online"];
61
78
  static values = {
79
+ announceText: { type: String, default: "" },
80
+ announceOnlineText: { type: String, default: "" },
62
81
  onlineAutoHide: { type: Number, default: 0 }
63
82
  };
64
83
  static events = ["change"];
65
84
  #timers = new SafeTimeout();
66
85
  /** Last known connectivity; guards against duplicate-state re-announcements. */
67
86
  #online = true;
68
- /** Banner text captured from the markup so transitions can re-write it. */
69
- #offlineMessage = "";
70
- #onlineMessage = "";
71
87
  #handleOnline = () => this.#update(true);
72
88
  #handleOffline = () => this.#update(false);
73
89
  connect() {
74
- this.#offlineMessage = this.hasOfflineTarget ? (this.offlineTarget.textContent ?? "").trim() : "";
75
- this.#onlineMessage = this.hasOnlineTarget ? (this.onlineTarget.textContent ?? "").trim() : "";
76
90
  if (this.hasOfflineTarget) this.offlineTarget.hidden = true;
77
91
  if (this.hasOnlineTarget) this.onlineTarget.hidden = true;
78
92
  this.#online = navigator.onLine;
@@ -86,7 +100,12 @@ var NetworkStatusController = class extends Controller {
86
100
  window.removeEventListener("offline", this.#handleOffline);
87
101
  this.#timers.clearAll();
88
102
  }
89
- /** Applies a connectivity transition, guarded against duplicate states. */
103
+ /**
104
+ * Applies a connectivity transition, guarded against duplicate states.
105
+ *
106
+ * The event goes out last, so a listener reading `data-state` or a banner's
107
+ * visibility sees the state the transition landed on rather than the previous one.
108
+ */
90
109
  #update(online) {
91
110
  if (online === this.#online) return;
92
111
  this.#online = online;
@@ -96,6 +115,9 @@ var NetworkStatusController = class extends Controller {
96
115
  } else {
97
116
  this.#showOffline();
98
117
  }
118
+ announce(fillTemplate(online ? this.announceOnlineTextValue : this.announceTextValue, {}), {
119
+ assertive: !online
120
+ });
99
121
  this.dispatch("change", { detail: { online } });
100
122
  }
101
123
  /** Shows the offline banner and hides the recovery banner. */
@@ -103,7 +125,6 @@ var NetworkStatusController = class extends Controller {
103
125
  this.#timers.clearAll();
104
126
  if (this.hasOnlineTarget) this.onlineTarget.hidden = true;
105
127
  if (this.hasOfflineTarget) {
106
- this.offlineTarget.textContent = this.#offlineMessage;
107
128
  this.offlineTarget.hidden = false;
108
129
  }
109
130
  }
@@ -111,7 +132,6 @@ var NetworkStatusController = class extends Controller {
111
132
  #showOnline() {
112
133
  if (this.hasOfflineTarget) this.offlineTarget.hidden = true;
113
134
  if (!this.hasOnlineTarget) return;
114
- this.onlineTarget.textContent = this.#onlineMessage;
115
135
  this.onlineTarget.hidden = false;
116
136
  if (this.onlineAutoHideValue > 0) {
117
137
  this.#timers.set(() => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/safe_timeout.ts","../../src/controllers/network_status_controller.ts"],"names":[],"mappings":"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","file":"network_status_controller.js","sourcesContent":["/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * Headless online/offline banner behavior built on the live-region practice (no\n * dedicated APG pattern).\n *\n * Markup contract (identifier: `stimeo--network-status`):\n * <div data-controller=\"stimeo--network-status\">\n * <div role=\"alert\" hidden data-stimeo--network-status-target=\"offline\">\n * You are offline.\n * </div>\n * <div role=\"status\" hidden data-stimeo--network-status-target=\"online\">\n * Back online.\n * </div>\n * </div>\n *\n * Reads `navigator.onLine` on connect and subscribes to the `window`\n * `online`/`offline` events, toggling the matching banner. The offline banner is\n * `role=\"alert\"` (assertive) because losing connectivity is urgent; the recovery\n * banner is `role=\"status\"` (polite).\n *\n * @remarks\n * Behavior only. `navigator.onLine` is the browser's *guess* — it does not\n * guarantee server reachability, which stays the consumer's job. To make the\n * announcement reliable across assistive tech (merely un-hiding a static banner\n * is flaky), the controller re-writes the banner's text on each transition,\n * guarded so an unchanged state never re-announces. The event listeners and the\n * auto-hide timer are removed/cleared on `disconnect()` (Turbo included).\n */\nexport class NetworkStatusController extends Controller<HTMLElement> {\n static override targets = [\"offline\", \"online\"];\n static override values = {\n onlineAutoHide: { type: Number, default: 0 },\n };\n static events = [\"change\"] as const;\n\n declare readonly offlineTarget: HTMLElement;\n declare readonly onlineTarget: HTMLElement;\n declare readonly hasOfflineTarget: boolean;\n declare readonly hasOnlineTarget: boolean;\n\n declare onlineAutoHideValue: number;\n\n readonly #timers = new SafeTimeout();\n\n /** Last known connectivity; guards against duplicate-state re-announcements. */\n #online = true;\n /** Banner text captured from the markup so transitions can re-write it. */\n #offlineMessage = \"\";\n #onlineMessage = \"\";\n\n readonly #handleOnline = (): void => this.#update(true);\n readonly #handleOffline = (): void => this.#update(false);\n\n override connect(): void {\n this.#offlineMessage = this.hasOfflineTarget\n ? (this.offlineTarget.textContent ?? \"\").trim()\n : \"\";\n this.#onlineMessage = this.hasOnlineTarget ? (this.onlineTarget.textContent ?? \"\").trim() : \"\";\n\n // Normalize initial visibility so a missing `hidden` in the markup cannot\n // strand a stale banner (e.g. an offline notice showing while online).\n if (this.hasOfflineTarget) this.offlineTarget.hidden = true;\n if (this.hasOnlineTarget) this.onlineTarget.hidden = true;\n\n this.#online = navigator.onLine;\n // On connect, surface only the offline state; do not flash a \"back online\"\n // banner just because the page loaded while connected.\n this.element.setAttribute(\"data-state\", this.#online ? \"online\" : \"offline\");\n if (!this.#online) this.#showOffline();\n\n window.addEventListener(\"online\", this.#handleOnline);\n window.addEventListener(\"offline\", this.#handleOffline);\n }\n\n override disconnect(): void {\n window.removeEventListener(\"online\", this.#handleOnline);\n window.removeEventListener(\"offline\", this.#handleOffline);\n this.#timers.clearAll();\n }\n\n /** Applies a connectivity transition, guarded against duplicate states. */\n #update(online: boolean): void {\n if (online === this.#online) return;\n this.#online = online;\n this.element.setAttribute(\"data-state\", online ? \"online\" : \"offline\");\n if (online) {\n this.#showOnline();\n } else {\n this.#showOffline();\n }\n this.dispatch(\"change\", { detail: { online } });\n }\n\n /** Shows the offline banner and hides the recovery banner. */\n #showOffline(): void {\n this.#timers.clearAll();\n if (this.hasOnlineTarget) this.onlineTarget.hidden = true;\n if (this.hasOfflineTarget) {\n this.offlineTarget.textContent = this.#offlineMessage;\n this.offlineTarget.hidden = false;\n }\n }\n\n /** Shows the recovery banner, optionally auto-hiding it after `onlineAutoHide`. */\n #showOnline(): void {\n if (this.hasOfflineTarget) this.offlineTarget.hidden = true;\n if (!this.hasOnlineTarget) return;\n this.onlineTarget.textContent = this.#onlineMessage;\n this.onlineTarget.hidden = false;\n if (this.onlineAutoHideValue > 0) {\n this.#timers.set(() => {\n this.onlineTarget.hidden = true;\n }, this.onlineAutoHideValue);\n }\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/announce.ts","../../src/utils/safe_timeout.ts","../../src/controllers/network_status_controller.ts"],"names":[],"mappings":"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","file":"network_status_controller.js","sourcesContent":["/**\n * Sends one message to the page's shared `stimeo--announcer`.\n *\n * A component that has to reach assistive tech does not carry a live region of its\n * own: a region only announces what changes *after* assistive tech already knows\n * about it, which a region that appears (or is un-hidden) with its message cannot\n * satisfy. The one region that can is the announcer sitting in the page from the\n * start, so state changes are handed to it as an event and it does the reading.\n *\n * The event goes to `window` because the announcer is usually a sibling high in the\n * document rather than an ancestor of the component dispatching it.\n *\n * Wording comes from the consumer — the library ships no English strings — so an\n * empty message is silently dropped and nothing is announced.\n *\n * @example\n * ```ts\n * announce(this.announceTextValue, { assertive: false });\n * ```\n */\nexport function announce(message: string, options: { assertive?: boolean } = {}): void {\n const text = message.trim();\n if (text.length === 0) return;\n window.dispatchEvent(\n new CustomEvent(\"stimeo--announcer:announce\", {\n detail: { message: text, assertive: options.assertive === true },\n }),\n );\n}\n\n/**\n * Fills `{name}` placeholders in an announcement template from `values`.\n *\n * The same substitution the value-text templates use, so a consumer writes\n * `\"{percent}% complete\"` in one attribute and gets the same rules everywhere. A\n * placeholder with no matching entry is left as authored rather than blanked, which\n * keeps a typo visible instead of silently swallowing the word.\n */\nexport function fillTemplate(template: string, values: Record<string, string | number>): string {\n return template.replace(/\\{([a-zA-Z][a-zA-Z0-9]*)\\}/g, (match, name: string) => {\n const replacement = values[name];\n return replacement === undefined ? match : String(replacement);\n });\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { announce, fillTemplate } from \"../utils/announce\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * Headless online/offline banner behavior built on the live-region practice (no\n * dedicated APG pattern).\n *\n * Markup contract (identifier: `stimeo--network-status`):\n * <div data-controller=\"stimeo--network-status\">\n * <div role=\"alert\" hidden data-stimeo--network-status-target=\"offline\">\n * You are offline.\n * </div>\n * <div role=\"status\" hidden data-stimeo--network-status-target=\"online\">\n * Back online.\n * </div>\n * </div>\n *\n * Reads `navigator.onLine` on connect and subscribes to the `window`\n * `online`/`offline` events, toggling the matching banner. The offline banner is\n * `role=\"alert\"` (assertive) because losing connectivity is urgent; the recovery\n * banner is `role=\"status\"` (polite).\n *\n * @remarks\n * Behavior only. `navigator.onLine` is the browser's *guess* — it does not\n * guarantee server reachability, which stays the consumer's job. To make the\n * announcement reliable across assistive tech (merely un-hiding a static banner\n * is flaky), the controller reveals the banner and *then* re-writes its text on\n * each transition — a region revealed after its content changed is outside the\n * accessibility tree while the text changes, so the write has to come second —\n * guarded so an unchanged state never re-announces. The event listeners and the\n * auto-hide timer are removed/cleared on `disconnect()` (Turbo included).\n */\nexport class NetworkStatusController extends Controller<HTMLElement> {\n static override targets = [\"offline\", \"online\"];\n static override values = {\n announceText: { type: String, default: \"\" },\n announceOnlineText: { type: String, default: \"\" },\n onlineAutoHide: { type: Number, default: 0 },\n };\n static events = [\"change\"] as const;\n\n declare readonly offlineTarget: HTMLElement;\n declare readonly onlineTarget: HTMLElement;\n declare readonly hasOfflineTarget: boolean;\n declare readonly hasOnlineTarget: boolean;\n\n declare onlineAutoHideValue: number;\n declare announceTextValue: string;\n declare announceOnlineTextValue: string;\n\n readonly #timers = new SafeTimeout();\n\n /** Last known connectivity; guards against duplicate-state re-announcements. */\n #online = true;\n\n readonly #handleOnline = (): void => this.#update(true);\n readonly #handleOffline = (): void => this.#update(false);\n\n override connect(): void {\n // Normalize initial visibility so a missing `hidden` in the markup cannot\n // strand a stale banner (e.g. an offline notice showing while online).\n if (this.hasOfflineTarget) this.offlineTarget.hidden = true;\n if (this.hasOnlineTarget) this.onlineTarget.hidden = true;\n\n this.#online = navigator.onLine;\n // On connect, surface only the offline state; do not flash a \"back online\"\n // banner just because the page loaded while connected.\n this.element.setAttribute(\"data-state\", this.#online ? \"online\" : \"offline\");\n if (!this.#online) this.#showOffline();\n\n window.addEventListener(\"online\", this.#handleOnline);\n window.addEventListener(\"offline\", this.#handleOffline);\n }\n\n override disconnect(): void {\n window.removeEventListener(\"online\", this.#handleOnline);\n window.removeEventListener(\"offline\", this.#handleOffline);\n this.#timers.clearAll();\n }\n\n /**\n * Applies a connectivity transition, guarded against duplicate states.\n *\n * The event goes out last, so a listener reading `data-state` or a banner's\n * visibility sees the state the transition landed on rather than the previous one.\n */\n #update(online: boolean): void {\n if (online === this.#online) return;\n this.#online = online;\n this.element.setAttribute(\"data-state\", online ? \"online\" : \"offline\");\n if (online) {\n this.#showOnline();\n } else {\n this.#showOffline();\n }\n // The banner is the visual half; reading it out is the announcer's job, because\n // a region that is only revealed at the moment of the change is not reliably read.\n announce(fillTemplate(online ? this.announceOnlineTextValue : this.announceTextValue, {}), {\n assertive: !online,\n });\n this.dispatch(\"change\", { detail: { online } });\n }\n\n /** Shows the offline banner and hides the recovery banner. */\n #showOffline(): void {\n this.#timers.clearAll();\n if (this.hasOnlineTarget) this.onlineTarget.hidden = true;\n if (this.hasOfflineTarget) {\n // Reveal before writing: a hidden region is out of the accessibility tree,\n // so a text change made while it is still hidden is never observed.\n this.offlineTarget.hidden = false;\n }\n }\n\n /** Shows the recovery banner, optionally auto-hiding it after `onlineAutoHide`. */\n #showOnline(): void {\n if (this.hasOfflineTarget) this.offlineTarget.hidden = true;\n if (!this.hasOnlineTarget) return;\n // Reveal before writing, same as the offline banner.\n this.onlineTarget.hidden = false;\n if (this.onlineAutoHideValue > 0) {\n this.#timers.set(() => {\n this.onlineTarget.hidden = true;\n }, this.onlineAutoHideValue);\n }\n }\n}\n"]}
@@ -23,7 +23,7 @@ import { Controller } from '@hotwired/stimulus';
23
23
  * Implements the WAI-ARIA APG **Spinbutton** pattern. The arrow/step logic is
24
24
  * fully owned by the controller (not delegated to the browser's native number
25
25
  * stepping) so the behavior is identical for a native `<input type="number">` and
26
- * a custom `role="spinbutton"` host, and deterministic under happy-dom.
26
+ * a custom `role="spinbutton"` host.
27
27
  *
28
28
  * @remarks
29
29
  * Behavior only — the consumer styles the field and buttons. The input is the
@@ -2,6 +2,12 @@ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  // src/controllers/number_input_controller.ts
4
4
 
5
+ // src/utils/arrow_step.ts
6
+ function isReservedArrowChord(event, allow = []) {
7
+ if (!event.key.startsWith("Arrow")) return false;
8
+ return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
9
+ }
10
+
5
11
  // src/utils/safe_timeout.ts
6
12
  var TimerRegistry = class {
7
13
  /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
@@ -148,6 +154,7 @@ var NumberInputController = class _NumberInputController extends Controller {
148
154
  }
149
155
  /** Keyboard stepping per the APG spinbutton model. */
150
156
  onKeydown(event) {
157
+ if (isReservedArrowChord(event)) return;
151
158
  const page = this.pageStepValue > 0 ? this.pageStepValue : this.stepValue * 10;
152
159
  let next = null;
153
160
  switch (event.key) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/safe_timeout.ts","../../src/controllers/number_input_controller.ts"],"names":[],"mappings":"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","file":"number_input_controller.js","sourcesContent":["/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { SafeInterval, SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * Headless, accessible number / spin-button behavior.\n *\n * Markup contract (identifier: `stimeo--number-input`):\n * <div data-controller=\"stimeo--number-input\"\n * data-stimeo--number-input-min-value=\"0\"\n * data-stimeo--number-input-max-value=\"100\"\n * data-stimeo--number-input-step-value=\"1\">\n * <button type=\"button\" aria-label=\"Decrease\" tabindex=\"-1\"\n * data-stimeo--number-input-target=\"decrement\"\n * data-action=\"click->stimeo--number-input#decrement\">−</button>\n * <input type=\"number\" min=\"0\" max=\"100\" step=\"1\" value=\"0\"\n * data-stimeo--number-input-target=\"input\"\n * data-action=\"change->stimeo--number-input#onInput\n * keydown->stimeo--number-input#onKeydown\" />\n * <button type=\"button\" aria-label=\"Increase\" tabindex=\"-1\"\n * data-stimeo--number-input-target=\"increment\"\n * data-action=\"click->stimeo--number-input#increment\">+</button>\n * </div>\n *\n * Implements the WAI-ARIA APG **Spinbutton** pattern. The arrow/step logic is\n * fully owned by the controller (not delegated to the browser's native number\n * stepping) so the behavior is identical for a native `<input type=\"number\">` and\n * a custom `role=\"spinbutton\"` host, and deterministic under happy-dom.\n *\n * @remarks\n * Behavior only — the consumer styles the field and buttons. The input is the\n * sole Tab stop; the buttons are `tabindex=\"-1\"` and keep focus on the input\n * (pointerdown is suppressed), so stepping never moves focus away.\n *\n * Behavior provided:\n * - `ArrowUp`/`ArrowDown` step by `step`; `PageUp`/`PageDown` by `pageStep`\n * (default `step × 10`); `Home`/`End` jump to a finite `min`/`max`.\n * - Increment/decrement buttons step too, and are `disabled` at the bounds (focus\n * is returned to the input before a focused button is disabled).\n * - **Press-and-hold auto-repeat**: holding a step button steps once, then after a\n * short delay repeats until release, the bound is reached, or the element\n * disconnects. The `click` binding stays the single-step path (a quick click, a\n * synthesized/programmatic click, or assistive activation), so a normal click\n * never double-steps — the trailing click after a hold is swallowed.\n * - Typed input is clamped and snapped to the step grid on `change`;\n * `stimeo--number-input:change` is dispatched on every committed change.\n */\nexport class NumberInputController extends Controller<HTMLElement> {\n static override targets = [\"input\", \"increment\", \"decrement\"];\n static override values = {\n min: { type: Number, default: Number.NEGATIVE_INFINITY },\n max: { type: Number, default: Number.POSITIVE_INFINITY },\n step: { type: Number, default: 1 },\n pageStep: { type: Number, default: 0 },\n };\n static actions = [\"decrement\", \"increment\", \"onInput\", \"onKeydown\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly inputTarget: HTMLInputElement;\n declare readonly hasInputTarget: boolean;\n declare readonly incrementTarget: HTMLButtonElement;\n declare readonly hasIncrementTarget: boolean;\n declare readonly decrementTarget: HTMLButtonElement;\n declare readonly hasDecrementTarget: boolean;\n declare minValue: number;\n declare maxValue: number;\n declare stepValue: number;\n declare pageStepValue: number;\n\n /** Delay (ms) a button must be held before auto-repeat starts. */\n static readonly #HOLD_DELAY_MS = 400;\n /** Interval (ms) between auto-repeat steps once a hold has started. */\n static readonly #HOLD_REPEAT_MS = 80;\n /**\n * Window (ms) after a hold ends during which the trailing synthetic `click` is\n * swallowed. A safety net for the (rare) case where that click never arrives\n * (e.g. the pointer was released off the button): the flag self-clears instead\n * of poisoning the next legitimate click.\n */\n static readonly #SUPPRESS_RESET_MS = 250;\n\n /** Aborts the pointer listeners on disconnect so none outlive the element. */\n #guards: AbortController | null = null;\n /** Timers for the hold delay and the suppress-reset safety net. */\n readonly #holdTimeouts = new SafeTimeout();\n /** The running auto-repeat interval (one at a time). */\n readonly #holdIntervals = new SafeInterval();\n /** True while a hold is armed/running, making `#stopHold` idempotent. */\n #holdActive = false;\n /** True once a hold actually produced a repeated step (vs. a quick click). */\n #repeatedDuringHold = false;\n /** True when the next `click` is the trailing one after a hold and must be ignored. */\n #suppressNextClick = false;\n\n /** Normalizes any initial value and wires the focus/hold pointer guards. */\n override connect(): void {\n if (!this.hasInputTarget) return;\n if (this.inputTarget.value.trim() !== \"\") {\n this.#write(this.#normalize(this.#currentValue()));\n } else {\n this.#updateButtons(this.#currentValue());\n }\n this.#guards = new AbortController();\n const { signal } = this.#guards;\n if (this.hasIncrementTarget) this.#wireButton(this.incrementTarget, this.stepValue, signal);\n if (this.hasDecrementTarget) this.#wireButton(this.decrementTarget, -this.stepValue, signal);\n // A pointer released or focus lost anywhere stops a running hold, even when\n // the button stopped receiving its own pointerup (e.g. it became disabled at\n // the bound, or the pointer was released off it).\n window.addEventListener(\"pointerup\", this.#stopHold, { signal });\n window.addEventListener(\"pointercancel\", this.#stopHold, { signal });\n window.addEventListener(\"blur\", this.#stopHold, { signal });\n }\n\n /** Releases the pointer guards and tears down every pending timer and hold state. */\n override disconnect(): void {\n this.#guards?.abort();\n this.#guards = null;\n this.#holdActive = false;\n this.#repeatedDuringHold = false;\n this.#suppressNextClick = false;\n this.#holdTimeouts.clearAll();\n this.#holdIntervals.clearAll();\n }\n\n /** Increases by one step. Bound via `data-action` (click). */\n increment(): void {\n if (this.#consumeSuppressedClick()) return;\n this.#commit(this.#currentValue() + this.stepValue);\n this.inputTarget.focus();\n }\n\n /** Decreases by one step. Bound via `data-action` (click). */\n decrement(): void {\n if (this.#consumeSuppressedClick()) return;\n this.#commit(this.#currentValue() - this.stepValue);\n this.inputTarget.focus();\n }\n\n /** Clamps and snaps a typed value. Bound via `data-action` (change). */\n onInput(): void {\n if (this.inputTarget.value.trim() === \"\") return;\n this.#commit(this.#currentValue());\n }\n\n /** Keyboard stepping per the APG spinbutton model. */\n onKeydown(event: KeyboardEvent): void {\n const page = this.pageStepValue > 0 ? this.pageStepValue : this.stepValue * 10;\n let next: number | null = null;\n switch (event.key) {\n case \"ArrowUp\":\n next = this.#currentValue() + this.stepValue;\n break;\n case \"ArrowDown\":\n next = this.#currentValue() - this.stepValue;\n break;\n case \"PageUp\":\n next = this.#currentValue() + page;\n break;\n case \"PageDown\":\n next = this.#currentValue() - page;\n break;\n case \"Home\":\n if (!Number.isFinite(this.minValue)) return;\n next = this.minValue;\n break;\n case \"End\":\n if (!Number.isFinite(this.maxValue)) return;\n next = this.maxValue;\n break;\n default:\n return;\n }\n event.preventDefault();\n this.#commit(next);\n }\n\n /**\n * Wires a step button: `pointerdown` keeps focus on the input and arms the\n * hold; leaving the button while held stops it (the global listeners cover\n * release/cancel/blur).\n */\n #wireButton(button: HTMLButtonElement, delta: number, signal: AbortSignal): void {\n button.addEventListener(\"pointerdown\", (event) => this.#armHold(event, button, delta), {\n signal,\n });\n button.addEventListener(\"pointerleave\", this.#stopHold, { signal });\n }\n\n /** Starts a hold: focus the input, then schedule the first repeat after a delay. */\n #armHold(event: Event, button: HTMLButtonElement, delta: number): void {\n // Ignore secondary buttons (right/middle) when the event exposes one.\n const pointerButton = (event as PointerEvent).button;\n if (typeof pointerButton === \"number\" && pointerButton !== 0) return;\n if (button.disabled) return;\n event.preventDefault(); // keep focus on the input rather than the button\n this.inputTarget.focus();\n\n this.#stopHold();\n this.#holdActive = true;\n this.#repeatedDuringHold = false;\n this.#suppressNextClick = false;\n this.#holdTimeouts.set(() => {\n if (!this.#commit(this.#currentValue() + delta)) {\n this.#stopHold();\n return;\n }\n this.#repeatedDuringHold = true;\n this.#holdIntervals.set(() => {\n if (!this.#commit(this.#currentValue() + delta)) this.#stopHold();\n }, NumberInputController.#HOLD_REPEAT_MS);\n }, NumberInputController.#HOLD_DELAY_MS);\n }\n\n /**\n * Stops the current hold (idempotent). When the hold actually repeated, the\n * trailing synthetic `click` must be ignored, so it is suppressed until the\n * click consumes it or a short safety-net timeout clears it.\n */\n readonly #stopHold = (): void => {\n if (!this.#holdActive) return;\n this.#holdActive = false;\n this.#holdTimeouts.clearAll();\n this.#holdIntervals.clearAll();\n if (this.#repeatedDuringHold) {\n this.#suppressNextClick = true;\n this.#holdTimeouts.set(() => {\n this.#suppressNextClick = false;\n }, NumberInputController.#SUPPRESS_RESET_MS);\n }\n };\n\n /** Consumes a pending trailing-click suppression; returns true if the click was swallowed. */\n #consumeSuppressedClick(): boolean {\n if (!this.#suppressNextClick) return false;\n this.#suppressNextClick = false;\n this.#holdTimeouts.clearAll();\n return true;\n }\n\n /**\n * Normalizes `raw`, reflects it, and dispatches `change` only when the\n * displayed value actually changes.\n *\n * @returns Whether the value changed (drives the auto-repeat's bound stop).\n */\n #commit(raw: number): boolean {\n const value = this.#normalize(raw);\n const changed = this.inputTarget.value !== String(value);\n this.#write(value);\n if (changed) this.dispatch(\"change\", { detail: { value } });\n return changed;\n }\n\n /** Reflects `value` on the input (and ARIA for non-native hosts) and the buttons. */\n #write(value: number): void {\n this.inputTarget.value = String(value);\n if (this.inputTarget.getAttribute(\"role\") === \"spinbutton\") {\n this.inputTarget.setAttribute(\"aria-valuenow\", String(value));\n if (Number.isFinite(this.minValue)) {\n this.inputTarget.setAttribute(\"aria-valuemin\", String(this.minValue));\n }\n if (Number.isFinite(this.maxValue)) {\n this.inputTarget.setAttribute(\"aria-valuemax\", String(this.maxValue));\n }\n }\n this.#updateButtons(value);\n }\n\n /** Disables a step button at its bound, returning focus to the input first. */\n #updateButtons(value: number): void {\n if (this.hasIncrementTarget) this.#toggleButton(this.incrementTarget, value < this.maxValue);\n if (this.hasDecrementTarget) this.#toggleButton(this.decrementTarget, value > this.minValue);\n }\n\n /**\n * Enables or disables a step button at its bound, never disabling one while it\n * holds focus. Owns only the `disabled` it sets itself via a marker\n * (`data-number-input-disabled`, like `conditional-fields`/`submit-once`), so an\n * author-disabled button (e.g. the whole control disabled) is never re-enabled.\n */\n #toggleButton(button: HTMLButtonElement, enabled: boolean): void {\n if (enabled) {\n if (button.hasAttribute(\"data-number-input-disabled\")) {\n button.disabled = false;\n button.removeAttribute(\"data-number-input-disabled\");\n }\n return;\n }\n if (button.disabled) return; // already disabled (possibly by the author) — leave it\n if (document.activeElement === button) this.inputTarget.focus();\n button.disabled = true;\n button.setAttribute(\"data-number-input-disabled\", \"\");\n }\n\n /** The current numeric value, falling back to a finite min (else 0) when blank. */\n #currentValue(): number {\n const parsed = Number(this.inputTarget.value);\n if (Number.isFinite(parsed) && this.inputTarget.value.trim() !== \"\") return parsed;\n return Number.isFinite(this.minValue) ? this.minValue : 0;\n }\n\n /** Clamps to `[min, max]` and snaps to the step grid anchored at a finite min (else 0). */\n #normalize(raw: number): number {\n const clamped = Math.min(this.maxValue, Math.max(this.minValue, raw));\n if (this.stepValue <= 0) return clamped;\n const base = Number.isFinite(this.minValue) ? this.minValue : 0;\n const stepped = Math.round((clamped - base) / this.stepValue) * this.stepValue + base;\n return Math.min(this.maxValue, Math.max(this.minValue, stepped));\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/arrow_step.ts","../../src/utils/safe_timeout.ts","../../src/controllers/number_input_controller.ts"],"names":[],"mappings":"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","file":"number_input_controller.js","sourcesContent":["import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord } from \"../utils/arrow_step\";\nimport { SafeInterval, SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * Headless, accessible number / spin-button behavior.\n *\n * Markup contract (identifier: `stimeo--number-input`):\n * <div data-controller=\"stimeo--number-input\"\n * data-stimeo--number-input-min-value=\"0\"\n * data-stimeo--number-input-max-value=\"100\"\n * data-stimeo--number-input-step-value=\"1\">\n * <button type=\"button\" aria-label=\"Decrease\" tabindex=\"-1\"\n * data-stimeo--number-input-target=\"decrement\"\n * data-action=\"click->stimeo--number-input#decrement\">−</button>\n * <input type=\"number\" min=\"0\" max=\"100\" step=\"1\" value=\"0\"\n * data-stimeo--number-input-target=\"input\"\n * data-action=\"change->stimeo--number-input#onInput\n * keydown->stimeo--number-input#onKeydown\" />\n * <button type=\"button\" aria-label=\"Increase\" tabindex=\"-1\"\n * data-stimeo--number-input-target=\"increment\"\n * data-action=\"click->stimeo--number-input#increment\">+</button>\n * </div>\n *\n * Implements the WAI-ARIA APG **Spinbutton** pattern. The arrow/step logic is\n * fully owned by the controller (not delegated to the browser's native number\n * stepping) so the behavior is identical for a native `<input type=\"number\">` and\n * a custom `role=\"spinbutton\"` host.\n *\n * @remarks\n * Behavior only — the consumer styles the field and buttons. The input is the\n * sole Tab stop; the buttons are `tabindex=\"-1\"` and keep focus on the input\n * (pointerdown is suppressed), so stepping never moves focus away.\n *\n * Behavior provided:\n * - `ArrowUp`/`ArrowDown` step by `step`; `PageUp`/`PageDown` by `pageStep`\n * (default `step × 10`); `Home`/`End` jump to a finite `min`/`max`.\n * - Increment/decrement buttons step too, and are `disabled` at the bounds (focus\n * is returned to the input before a focused button is disabled).\n * - **Press-and-hold auto-repeat**: holding a step button steps once, then after a\n * short delay repeats until release, the bound is reached, or the element\n * disconnects. The `click` binding stays the single-step path (a quick click, a\n * synthesized/programmatic click, or assistive activation), so a normal click\n * never double-steps — the trailing click after a hold is swallowed.\n * - Typed input is clamped and snapped to the step grid on `change`;\n * `stimeo--number-input:change` is dispatched on every committed change.\n */\nexport class NumberInputController extends Controller<HTMLElement> {\n static override targets = [\"input\", \"increment\", \"decrement\"];\n static override values = {\n min: { type: Number, default: Number.NEGATIVE_INFINITY },\n max: { type: Number, default: Number.POSITIVE_INFINITY },\n step: { type: Number, default: 1 },\n pageStep: { type: Number, default: 0 },\n };\n static actions = [\"decrement\", \"increment\", \"onInput\", \"onKeydown\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly inputTarget: HTMLInputElement;\n declare readonly hasInputTarget: boolean;\n declare readonly incrementTarget: HTMLButtonElement;\n declare readonly hasIncrementTarget: boolean;\n declare readonly decrementTarget: HTMLButtonElement;\n declare readonly hasDecrementTarget: boolean;\n declare minValue: number;\n declare maxValue: number;\n declare stepValue: number;\n declare pageStepValue: number;\n\n /** Delay (ms) a button must be held before auto-repeat starts. */\n static readonly #HOLD_DELAY_MS = 400;\n /** Interval (ms) between auto-repeat steps once a hold has started. */\n static readonly #HOLD_REPEAT_MS = 80;\n /**\n * Window (ms) after a hold ends during which the trailing synthetic `click` is\n * swallowed. A safety net for the (rare) case where that click never arrives\n * (e.g. the pointer was released off the button): the flag self-clears instead\n * of poisoning the next legitimate click.\n */\n static readonly #SUPPRESS_RESET_MS = 250;\n\n /** Aborts the pointer listeners on disconnect so none outlive the element. */\n #guards: AbortController | null = null;\n /** Timers for the hold delay and the suppress-reset safety net. */\n readonly #holdTimeouts = new SafeTimeout();\n /** The running auto-repeat interval (one at a time). */\n readonly #holdIntervals = new SafeInterval();\n /** True while a hold is armed/running, making `#stopHold` idempotent. */\n #holdActive = false;\n /** True once a hold actually produced a repeated step (vs. a quick click). */\n #repeatedDuringHold = false;\n /** True when the next `click` is the trailing one after a hold and must be ignored. */\n #suppressNextClick = false;\n\n /** Normalizes any initial value and wires the focus/hold pointer guards. */\n override connect(): void {\n if (!this.hasInputTarget) return;\n if (this.inputTarget.value.trim() !== \"\") {\n this.#write(this.#normalize(this.#currentValue()));\n } else {\n this.#updateButtons(this.#currentValue());\n }\n this.#guards = new AbortController();\n const { signal } = this.#guards;\n if (this.hasIncrementTarget) this.#wireButton(this.incrementTarget, this.stepValue, signal);\n if (this.hasDecrementTarget) this.#wireButton(this.decrementTarget, -this.stepValue, signal);\n // A pointer released or focus lost anywhere stops a running hold, even when\n // the button stopped receiving its own pointerup (e.g. it became disabled at\n // the bound, or the pointer was released off it).\n window.addEventListener(\"pointerup\", this.#stopHold, { signal });\n window.addEventListener(\"pointercancel\", this.#stopHold, { signal });\n window.addEventListener(\"blur\", this.#stopHold, { signal });\n }\n\n /** Releases the pointer guards and tears down every pending timer and hold state. */\n override disconnect(): void {\n this.#guards?.abort();\n this.#guards = null;\n this.#holdActive = false;\n this.#repeatedDuringHold = false;\n this.#suppressNextClick = false;\n this.#holdTimeouts.clearAll();\n this.#holdIntervals.clearAll();\n }\n\n /** Increases by one step. Bound via `data-action` (click). */\n increment(): void {\n if (this.#consumeSuppressedClick()) return;\n this.#commit(this.#currentValue() + this.stepValue);\n this.inputTarget.focus();\n }\n\n /** Decreases by one step. Bound via `data-action` (click). */\n decrement(): void {\n if (this.#consumeSuppressedClick()) return;\n this.#commit(this.#currentValue() - this.stepValue);\n this.inputTarget.focus();\n }\n\n /** Clamps and snaps a typed value. Bound via `data-action` (change). */\n onInput(): void {\n if (this.inputTarget.value.trim() === \"\") return;\n this.#commit(this.#currentValue());\n }\n\n /** Keyboard stepping per the APG spinbutton model. */\n onKeydown(event: KeyboardEvent): void {\n if (isReservedArrowChord(event)) return;\n const page = this.pageStepValue > 0 ? this.pageStepValue : this.stepValue * 10;\n let next: number | null = null;\n switch (event.key) {\n case \"ArrowUp\":\n next = this.#currentValue() + this.stepValue;\n break;\n case \"ArrowDown\":\n next = this.#currentValue() - this.stepValue;\n break;\n case \"PageUp\":\n next = this.#currentValue() + page;\n break;\n case \"PageDown\":\n next = this.#currentValue() - page;\n break;\n case \"Home\":\n if (!Number.isFinite(this.minValue)) return;\n next = this.minValue;\n break;\n case \"End\":\n if (!Number.isFinite(this.maxValue)) return;\n next = this.maxValue;\n break;\n default:\n return;\n }\n event.preventDefault();\n this.#commit(next);\n }\n\n /**\n * Wires a step button: `pointerdown` keeps focus on the input and arms the\n * hold; leaving the button while held stops it (the global listeners cover\n * release/cancel/blur).\n */\n #wireButton(button: HTMLButtonElement, delta: number, signal: AbortSignal): void {\n button.addEventListener(\"pointerdown\", (event) => this.#armHold(event, button, delta), {\n signal,\n });\n button.addEventListener(\"pointerleave\", this.#stopHold, { signal });\n }\n\n /** Starts a hold: focus the input, then schedule the first repeat after a delay. */\n #armHold(event: Event, button: HTMLButtonElement, delta: number): void {\n // Ignore secondary buttons (right/middle) when the event exposes one.\n const pointerButton = (event as PointerEvent).button;\n if (typeof pointerButton === \"number\" && pointerButton !== 0) return;\n if (button.disabled) return;\n event.preventDefault(); // keep focus on the input rather than the button\n this.inputTarget.focus();\n\n this.#stopHold();\n this.#holdActive = true;\n this.#repeatedDuringHold = false;\n this.#suppressNextClick = false;\n this.#holdTimeouts.set(() => {\n if (!this.#commit(this.#currentValue() + delta)) {\n this.#stopHold();\n return;\n }\n this.#repeatedDuringHold = true;\n this.#holdIntervals.set(() => {\n if (!this.#commit(this.#currentValue() + delta)) this.#stopHold();\n }, NumberInputController.#HOLD_REPEAT_MS);\n }, NumberInputController.#HOLD_DELAY_MS);\n }\n\n /**\n * Stops the current hold (idempotent). When the hold actually repeated, the\n * trailing synthetic `click` must be ignored, so it is suppressed until the\n * click consumes it or a short safety-net timeout clears it.\n */\n readonly #stopHold = (): void => {\n if (!this.#holdActive) return;\n this.#holdActive = false;\n this.#holdTimeouts.clearAll();\n this.#holdIntervals.clearAll();\n if (this.#repeatedDuringHold) {\n this.#suppressNextClick = true;\n this.#holdTimeouts.set(() => {\n this.#suppressNextClick = false;\n }, NumberInputController.#SUPPRESS_RESET_MS);\n }\n };\n\n /** Consumes a pending trailing-click suppression; returns true if the click was swallowed. */\n #consumeSuppressedClick(): boolean {\n if (!this.#suppressNextClick) return false;\n this.#suppressNextClick = false;\n this.#holdTimeouts.clearAll();\n return true;\n }\n\n /**\n * Normalizes `raw`, reflects it, and dispatches `change` only when the\n * displayed value actually changes.\n *\n * @returns Whether the value changed (drives the auto-repeat's bound stop).\n */\n #commit(raw: number): boolean {\n const value = this.#normalize(raw);\n const changed = this.inputTarget.value !== String(value);\n this.#write(value);\n if (changed) this.dispatch(\"change\", { detail: { value } });\n return changed;\n }\n\n /** Reflects `value` on the input (and ARIA for non-native hosts) and the buttons. */\n #write(value: number): void {\n this.inputTarget.value = String(value);\n if (this.inputTarget.getAttribute(\"role\") === \"spinbutton\") {\n this.inputTarget.setAttribute(\"aria-valuenow\", String(value));\n if (Number.isFinite(this.minValue)) {\n this.inputTarget.setAttribute(\"aria-valuemin\", String(this.minValue));\n }\n if (Number.isFinite(this.maxValue)) {\n this.inputTarget.setAttribute(\"aria-valuemax\", String(this.maxValue));\n }\n }\n this.#updateButtons(value);\n }\n\n /** Disables a step button at its bound, returning focus to the input first. */\n #updateButtons(value: number): void {\n if (this.hasIncrementTarget) this.#toggleButton(this.incrementTarget, value < this.maxValue);\n if (this.hasDecrementTarget) this.#toggleButton(this.decrementTarget, value > this.minValue);\n }\n\n /**\n * Enables or disables a step button at its bound, never disabling one while it\n * holds focus. Owns only the `disabled` it sets itself via a marker\n * (`data-number-input-disabled`, like `conditional-fields`/`submit-once`), so an\n * author-disabled button (e.g. the whole control disabled) is never re-enabled.\n */\n #toggleButton(button: HTMLButtonElement, enabled: boolean): void {\n if (enabled) {\n if (button.hasAttribute(\"data-number-input-disabled\")) {\n button.disabled = false;\n button.removeAttribute(\"data-number-input-disabled\");\n }\n return;\n }\n if (button.disabled) return; // already disabled (possibly by the author) — leave it\n if (document.activeElement === button) this.inputTarget.focus();\n button.disabled = true;\n button.setAttribute(\"data-number-input-disabled\", \"\");\n }\n\n /** The current numeric value, falling back to a finite min (else 0) when blank. */\n #currentValue(): number {\n const parsed = Number(this.inputTarget.value);\n if (Number.isFinite(parsed) && this.inputTarget.value.trim() !== \"\") return parsed;\n return Number.isFinite(this.minValue) ? this.minValue : 0;\n }\n\n /** Clamps to `[min, max]` and snaps to the step grid anchored at a finite min (else 0). */\n #normalize(raw: number): number {\n const clamped = Math.min(this.maxValue, Math.max(this.minValue, raw));\n if (this.stepValue <= 0) return clamped;\n const base = Number.isFinite(this.minValue) ? this.minValue : 0;\n const stepped = Math.round((clamped - base) / this.stepValue) * this.stepValue + base;\n return Math.min(this.maxValue, Math.max(this.minValue, stepped));\n }\n}\n"]}
@@ -2,6 +2,22 @@ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  // src/controllers/otp_controller.ts
4
4
 
5
+ // src/utils/logical_scroll.ts
6
+ function isRtl(element) {
7
+ return window.getComputedStyle(element).direction === "rtl";
8
+ }
9
+
10
+ // src/utils/arrow_step.ts
11
+ function logicalArrowKey(key, element) {
12
+ if (key !== "ArrowRight" && key !== "ArrowLeft") return key;
13
+ if (!isRtl(element)) return key;
14
+ return key === "ArrowRight" ? "ArrowLeft" : "ArrowRight";
15
+ }
16
+ function isReservedArrowChord(event, allow = []) {
17
+ if (!event.key.startsWith("Arrow")) return false;
18
+ return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
19
+ }
20
+
5
21
  // src/utils/composition_tracker.ts
6
22
  var CompositionTracker = class {
7
23
  #observedTargets = /* @__PURE__ */ new Set();
@@ -99,12 +115,13 @@ var OtpController = class extends Controller {
99
115
  }
100
116
  /** Handles Backspace retreating, arrows, and home/end navigation. */
101
117
  onKeydown(event) {
118
+ if (isReservedArrowChord(event)) return;
102
119
  const input = event.currentTarget;
103
120
  if (!input) return;
104
121
  const index = this.fieldTargets.indexOf(input);
105
122
  if (index === -1) return;
106
123
  if (this.#composition.isComposing(event)) return;
107
- switch (event.key) {
124
+ switch (logicalArrowKey(event.key, this.element)) {
108
125
  case "Backspace":
109
126
  if (!input.value) {
110
127
  if (index > 0) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/composition_tracker.ts","../../src/controllers/otp_controller.ts"],"names":[],"mappings":"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","file":"otp_controller.js","sourcesContent":["/** Minimal event shape carrying the platform's per-event composition signal. */\nexport interface CompositionSignal {\n readonly isComposing?: boolean;\n}\n\n/** Hooks that keep component-specific work outside {@link CompositionTracker}. */\nexport interface CompositionTrackerOptions {\n /** Runs after lifecycle state is set for a `compositionstart` event. */\n readonly onStart?: (event: Event) => void;\n /** Runs after lifecycle state is cleared for a `compositionend` event. */\n readonly onEnd?: (event: Event) => void;\n}\n\n/**\n * Owns IME composition listeners and transient state for one or more event targets.\n *\n * Some browsers omit `KeyboardEvent.isComposing` on the keydown that confirms a\n * conversion. Tracking `compositionstart` through `compositionend` supplies the\n * missing lifecycle signal without relying on deprecated numeric key codes.\n * Component policy stays with the consumer: filtering, validation, and submission\n * belong in the optional {@link CompositionTrackerOptions.onEnd} callback.\n *\n * @example\n * ```ts\n * #composition = new CompositionTracker({ onEnd: () => this.filter() });\n *\n * connect(): void {\n * this.#composition.observe(this.inputTarget);\n * }\n *\n * disconnect(): void {\n * this.#composition.disconnect();\n * }\n *\n * onKeydown(event: KeyboardEvent): void {\n * if (this.#composition.isComposing(event)) return;\n * }\n * ```\n */\nexport class CompositionTracker {\n readonly #observedTargets = new Set<EventTarget>();\n readonly #activeTargets = new Set<EventTarget>();\n readonly #onStart: ((event: Event) => void) | undefined;\n readonly #onEnd: ((event: Event) => void) | undefined;\n\n constructor(options: CompositionTrackerOptions = {}) {\n this.#onStart = options.onStart;\n this.#onEnd = options.onEnd;\n }\n\n /** Starts lifecycle tracking for `target`; repeated calls are idempotent. */\n observe(target: EventTarget): void {\n if (this.#observedTargets.has(target)) return;\n target.addEventListener(\"compositionstart\", this.#handleStart);\n target.addEventListener(\"compositionend\", this.#handleEnd);\n this.#observedTargets.add(target);\n }\n\n /** Stops tracking one target and clears any active composition it owned. */\n unobserve(target: EventTarget): void {\n if (!this.#observedTargets.delete(target)) return;\n target.removeEventListener(\"compositionstart\", this.#handleStart);\n target.removeEventListener(\"compositionend\", this.#handleEnd);\n this.#activeTargets.delete(target);\n }\n\n /** Releases every listener and clears state so reconnect starts cleanly. */\n disconnect(): void {\n for (const target of this.#observedTargets) {\n target.removeEventListener(\"compositionstart\", this.#handleStart);\n target.removeEventListener(\"compositionend\", this.#handleEnd);\n }\n this.#observedTargets.clear();\n this.#activeTargets.clear();\n }\n\n /** True when lifecycle tracking or the current event reports composition. */\n isComposing(event?: CompositionSignal): boolean {\n return this.#activeTargets.size > 0 || event?.isComposing === true;\n }\n\n readonly #handleStart = (event: Event): void => {\n if (event.currentTarget) this.#activeTargets.add(event.currentTarget);\n this.#onStart?.(event);\n };\n\n readonly #handleEnd = (event: Event): void => {\n if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);\n this.#onEnd?.(event);\n };\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { CompositionTracker } from \"../utils/composition_tracker\";\n\n/**\n * Headless, accessible One-Time Password / PIN input logic.\n *\n * Markup contract (identifier: `stimeo--otp`):\n * <div data-controller=\"stimeo--otp\"\n * data-stimeo--otp-length-value=\"6\"\n * data-stimeo--otp-pattern-value=\"[0-9]\"\n * role=\"group\" aria-label=\"One-time passcode\">\n * <input data-stimeo--otp-target=\"field\" inputmode=\"numeric\" maxlength=\"1\"\n * data-action=\"input->stimeo--otp#onInput\n * keydown->stimeo--otp#onKeydown\n * paste->stimeo--otp#onPaste\" />\n * <input type=\"hidden\" data-stimeo--otp-target=\"value\" />\n * </div>\n *\n * Implements highly polished UX conventions for single-digit field groups:\n * - Dynamic autofocus advancement upon valid inputs.\n * - Auto-selection `select()` upon gaining focus to enable frictionless digit overwrites.\n * - Retroactive Backspace key focus stepping to clear preceding values safely.\n * - Clipboard intercepting paste division across fields with trailing focus positioning.\n * - Hidden form sync and complete/change event dispatching.\n *\n * @remarks\n * Behavior only. The controller binds events dynamically to pre-rendered inputs,\n * updates `data-filled` and hidden value bindings, and emits Stimulus custom events.\n */\nexport class OtpController extends Controller<HTMLElement> {\n static override targets = [\"field\", \"value\", \"error\"];\n static override values = {\n length: { type: Number, default: 6 },\n pattern: { type: String, default: \"[0-9]\" },\n };\n static actions = [\"onInput\", \"onKeydown\", \"onPaste\"] as const;\n static events = [\"change\", \"complete\", \"invalid\"] as const;\n\n declare readonly fieldTargets: HTMLInputElement[];\n declare readonly valueTarget: HTMLInputElement;\n declare readonly errorTarget: HTMLElement;\n declare readonly hasValueTarget: boolean;\n declare readonly hasErrorTarget: boolean;\n\n declare lengthValue: number;\n declare patternValue: string;\n\n /** Owns IME lifecycle state across every digit field. */\n readonly #composition = new CompositionTracker({\n onEnd: (event) => {\n const input = event.currentTarget as HTMLInputElement | null;\n if (input) this.#handleInputValidation(input);\n },\n });\n\n override connect(): void {\n // Dynamically bind auto-selection and IME tracking logic to all connected inputs\n for (const field of this.fieldTargets) {\n field.addEventListener(\"focus\", this.#onFieldFocus);\n this.#composition.observe(field);\n }\n }\n\n override disconnect(): void {\n for (const field of this.fieldTargets) {\n field.removeEventListener(\"focus\", this.#onFieldFocus);\n }\n this.#composition.disconnect();\n }\n\n /**\n * Stimulus lifecycle callback when a new field target enters the DOM.\n * Ensures new additions are also wired with overwriting support.\n */\n fieldTargetConnected(element: HTMLInputElement): void {\n element.addEventListener(\"focus\", this.#onFieldFocus);\n this.#composition.observe(element);\n }\n\n /** Removes focus listeners when fields are dropped. */\n fieldTargetDisconnected(element: HTMLInputElement): void {\n element.removeEventListener(\"focus\", this.#onFieldFocus);\n this.#composition.unobserve(element);\n }\n\n /** Handles keystroke inputs and advances focus to the next field. */\n onInput(event: Event): void {\n const input = event.currentTarget as HTMLInputElement | null;\n if (!input) return;\n\n // Guard during active composition to prevent premature focus switching\n if (this.#composition.isComposing(event as InputEvent)) return;\n\n this.#handleInputValidation(input);\n }\n\n /** Handles Backspace retreating, arrows, and home/end navigation. */\n onKeydown(event: KeyboardEvent): void {\n const input = event.currentTarget as HTMLInputElement | null;\n if (!input) return;\n\n const index = this.fieldTargets.indexOf(input);\n if (index === -1) return;\n\n // Do not trigger keydown actions during composition\n if (this.#composition.isComposing(event)) return;\n\n switch (event.key) {\n case \"Backspace\":\n if (!input.value) {\n // Empty field: step backward, wipe previous digit, and focus it\n if (index > 0) {\n event.preventDefault();\n const prevField = this.fieldTargets[index - 1];\n if (prevField) {\n prevField.value = \"\";\n prevField.removeAttribute(\"data-filled\");\n prevField.focus();\n this.#clearError();\n this.#syncAndDispatch();\n }\n }\n } else {\n // Filled field: clear current value\n input.value = \"\";\n input.removeAttribute(\"data-filled\");\n this.#clearError();\n this.#syncAndDispatch();\n }\n break;\n\n case \"ArrowLeft\":\n if (index > 0) {\n event.preventDefault();\n this.fieldTargets[index - 1]?.focus();\n }\n break;\n\n case \"ArrowRight\":\n if (index < this.lengthValue - 1) {\n event.preventDefault();\n this.fieldTargets[index + 1]?.focus();\n }\n break;\n\n case \"Home\":\n event.preventDefault();\n this.fieldTargets[0]?.focus();\n break;\n\n case \"End\":\n event.preventDefault();\n this.fieldTargets[this.lengthValue - 1]?.focus();\n break;\n\n default:\n break;\n }\n }\n\n /** Divides pasted string characters across available input fields. */\n onPaste(event: ClipboardEvent): void {\n const input = event.currentTarget as HTMLInputElement | null;\n if (!input) return;\n\n const startIndex = this.fieldTargets.indexOf(input);\n if (startIndex === -1) return;\n\n event.preventDefault();\n\n const rawText = event.clipboardData?.getData(\"text\") || \"\";\n const text = this.#normalizeValue(rawText);\n const regex = new RegExp(`^${this.patternValue}$`);\n const validChars = Array.from(text).filter((char) => regex.test(char));\n\n if (validChars.length === 0) {\n this.#showError();\n return;\n }\n\n const limit = Math.min(validChars.length, this.lengthValue - startIndex);\n let lastFocusedIndex = startIndex;\n\n for (let i = 0; i < limit; i++) {\n const fieldIndex = startIndex + i;\n const field = this.fieldTargets[fieldIndex];\n const char = validChars[i];\n\n if (field && char) {\n field.value = char;\n field.setAttribute(\"data-filled\", \"true\");\n lastFocusedIndex = fieldIndex;\n }\n }\n\n this.#clearError();\n\n // Set trailing focus position to the last filled box (or next empty if one exists)\n const focusTargetIndex =\n lastFocusedIndex < this.lengthValue - 1 ? lastFocusedIndex + 1 : lastFocusedIndex;\n this.fieldTargets[focusTargetIndex]?.focus();\n\n this.#syncAndDispatch();\n }\n\n readonly #onFieldFocus = (event: FocusEvent): void => {\n const input = event.currentTarget as HTMLInputElement | null;\n if (input) {\n // Auto-selection enables effortless character overwrites\n input.select();\n }\n };\n\n #handleInputValidation(input: HTMLInputElement): void {\n const index = this.fieldTargets.indexOf(input);\n if (index === -1) return;\n\n const rawValue = input.value;\n const normalized = this.#normalizeValue(rawValue);\n const regex = new RegExp(`^${this.patternValue}$`);\n\n if (normalized && regex.test(normalized)) {\n input.value = normalized;\n input.setAttribute(\"data-filled\", \"true\");\n this.#clearError();\n\n // Step focus forward\n if (index < this.lengthValue - 1) {\n const nextField = this.fieldTargets[index + 1];\n nextField?.focus();\n }\n } else if (normalized) {\n input.value = \"\";\n input.removeAttribute(\"data-filled\");\n this.#showError();\n } else {\n input.removeAttribute(\"data-filled\");\n }\n\n this.#syncAndDispatch();\n }\n\n #normalizeValue(val: string): string {\n // Replaces full-width numbers (0-9) with standard half-width ones (0-9)\n return val.replace(/[0-9]/g, (s) => String.fromCharCode(s.charCodeAt(0) - 0xfee0));\n }\n\n #showError(): void {\n if (this.hasErrorTarget) {\n this.errorTarget.removeAttribute(\"hidden\");\n }\n // Behavior only: emit a neutral payload and let the consumer compose/localize\n // the user-facing message. The pattern is reported so consumers can word it.\n this.dispatch(\"invalid\", { detail: { pattern: this.patternValue } });\n }\n\n #clearError(): void {\n if (this.hasErrorTarget) {\n this.errorTarget.setAttribute(\"hidden\", \"true\");\n }\n }\n\n #syncAndDispatch(): void {\n const fields = this.fieldTargets;\n const combinedValue = fields.map((f) => f.value).join(\"\");\n\n if (this.hasValueTarget) {\n this.valueTarget.value = combinedValue;\n }\n\n this.dispatch(\"change\", { detail: { value: combinedValue } });\n\n // Completed state when all fields contain a digit\n const isCompleted =\n fields.every((f) => f.value.length > 0) && combinedValue.length === this.lengthValue;\n if (isCompleted) {\n this.dispatch(\"complete\", { detail: { value: combinedValue } });\n }\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/logical_scroll.ts","../../src/utils/arrow_step.ts","../../src/utils/composition_tracker.ts","../../src/controllers/otp_controller.ts"],"names":[],"mappings":"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","file":"otp_controller.js","sourcesContent":["/** Normalized scroll position and maximum distance on one logical axis. */\nexport interface LogicalScrollMetrics {\n position: number;\n max: number;\n}\n\n/**\n * Whether horizontal scrolling on `element` follows right-to-left inline flow.\n *\n * Resolved from the **computed** `direction`, so the authoring contract is the\n * usual `dir=\"rtl\"` (or a stylesheet) on the element or any ancestor.\n *\n * Scope: horizontal writing modes. A vertical writing mode (`writing-mode:\n * vertical-rl`) also inverts the horizontal axis, which this check does not\n * model — vertical writing modes are out of scope for the scroll utilities\n * (their consumers describe axes as horizontal/vertical, not inline/block).\n */\nexport function isRtl(element: Element): boolean {\n return window.getComputedStyle(element).direction === \"rtl\";\n}\n\n/**\n * Returns scroll distance from the logical start edge.\n *\n * CSSOM View exposes standards-mode RTL horizontal offsets as `0` at the inline\n * start (right) and increasingly negative values toward the inline end (left).\n * The normalized position is always clamped to `[0, max]`, which also absorbs\n * Safari's elastic overscroll values.\n */\nexport function logicalScrollMetrics(\n element: HTMLElement,\n horizontal: boolean,\n): LogicalScrollMetrics {\n const max = Math.max(\n 0,\n horizontal\n ? element.scrollWidth - element.clientWidth\n : element.scrollHeight - element.clientHeight,\n );\n const raw = horizontal ? element.scrollLeft : element.scrollTop;\n const position = horizontal && isRtl(element) ? -raw : raw;\n return { position: Math.min(max, Math.max(0, position)), max };\n}\n\n/**\n * Converts a logical start/end delta to the physical value accepted by\n * `Element.scrollBy`.\n */\nexport function physicalScrollDelta(\n element: HTMLElement,\n horizontal: boolean,\n logicalDelta: number,\n): number {\n return horizontal && isRtl(element) ? -logicalDelta : logicalDelta;\n}\n","import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n","/** Minimal event shape carrying the platform's per-event composition signal. */\nexport interface CompositionSignal {\n readonly isComposing?: boolean;\n}\n\n/** Hooks that keep component-specific work outside {@link CompositionTracker}. */\nexport interface CompositionTrackerOptions {\n /** Runs after lifecycle state is set for a `compositionstart` event. */\n readonly onStart?: (event: Event) => void;\n /** Runs after lifecycle state is cleared for a `compositionend` event. */\n readonly onEnd?: (event: Event) => void;\n}\n\n/**\n * Owns IME composition listeners and transient state for one or more event targets.\n *\n * Some browsers omit `KeyboardEvent.isComposing` on the keydown that confirms a\n * conversion. Tracking `compositionstart` through `compositionend` supplies the\n * missing lifecycle signal without relying on deprecated numeric key codes.\n * Component policy stays with the consumer: filtering, validation, and submission\n * belong in the optional {@link CompositionTrackerOptions.onEnd} callback.\n *\n * @example\n * ```ts\n * #composition = new CompositionTracker({ onEnd: () => this.filter() });\n *\n * connect(): void {\n * this.#composition.observe(this.inputTarget);\n * }\n *\n * disconnect(): void {\n * this.#composition.disconnect();\n * }\n *\n * onKeydown(event: KeyboardEvent): void {\n * if (this.#composition.isComposing(event)) return;\n * }\n * ```\n */\nexport class CompositionTracker {\n readonly #observedTargets = new Set<EventTarget>();\n readonly #activeTargets = new Set<EventTarget>();\n readonly #onStart: ((event: Event) => void) | undefined;\n readonly #onEnd: ((event: Event) => void) | undefined;\n\n constructor(options: CompositionTrackerOptions = {}) {\n this.#onStart = options.onStart;\n this.#onEnd = options.onEnd;\n }\n\n /** Starts lifecycle tracking for `target`; repeated calls are idempotent. */\n observe(target: EventTarget): void {\n if (this.#observedTargets.has(target)) return;\n target.addEventListener(\"compositionstart\", this.#handleStart);\n target.addEventListener(\"compositionend\", this.#handleEnd);\n this.#observedTargets.add(target);\n }\n\n /** Stops tracking one target and clears any active composition it owned. */\n unobserve(target: EventTarget): void {\n if (!this.#observedTargets.delete(target)) return;\n target.removeEventListener(\"compositionstart\", this.#handleStart);\n target.removeEventListener(\"compositionend\", this.#handleEnd);\n this.#activeTargets.delete(target);\n }\n\n /** Releases every listener and clears state so reconnect starts cleanly. */\n disconnect(): void {\n for (const target of this.#observedTargets) {\n target.removeEventListener(\"compositionstart\", this.#handleStart);\n target.removeEventListener(\"compositionend\", this.#handleEnd);\n }\n this.#observedTargets.clear();\n this.#activeTargets.clear();\n }\n\n /** True when lifecycle tracking or the current event reports composition. */\n isComposing(event?: CompositionSignal): boolean {\n return this.#activeTargets.size > 0 || event?.isComposing === true;\n }\n\n readonly #handleStart = (event: Event): void => {\n if (event.currentTarget) this.#activeTargets.add(event.currentTarget);\n this.#onStart?.(event);\n };\n\n readonly #handleEnd = (event: Event): void => {\n if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);\n this.#onEnd?.(event);\n };\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord, logicalArrowKey } from \"../utils/arrow_step\";\nimport { CompositionTracker } from \"../utils/composition_tracker\";\n\n/**\n * Headless, accessible One-Time Password / PIN input logic.\n *\n * Markup contract (identifier: `stimeo--otp`):\n * <div data-controller=\"stimeo--otp\"\n * data-stimeo--otp-length-value=\"6\"\n * data-stimeo--otp-pattern-value=\"[0-9]\"\n * role=\"group\" aria-label=\"One-time passcode\">\n * <input data-stimeo--otp-target=\"field\" inputmode=\"numeric\" maxlength=\"1\"\n * data-action=\"input->stimeo--otp#onInput\n * keydown->stimeo--otp#onKeydown\n * paste->stimeo--otp#onPaste\" />\n * <input type=\"hidden\" data-stimeo--otp-target=\"value\" />\n * </div>\n *\n * Implements highly polished UX conventions for single-digit field groups:\n * - Dynamic autofocus advancement upon valid inputs.\n * - Auto-selection `select()` upon gaining focus to enable frictionless digit overwrites.\n * - Retroactive Backspace key focus stepping to clear preceding values safely.\n * - Clipboard intercepting paste division across fields with trailing focus positioning.\n * - Hidden form sync and complete/change event dispatching.\n *\n * @remarks\n * Behavior only. The controller binds events dynamically to pre-rendered inputs,\n * updates `data-filled` and hidden value bindings, and emits Stimulus custom events.\n */\nexport class OtpController extends Controller<HTMLElement> {\n static override targets = [\"field\", \"value\", \"error\"];\n static override values = {\n length: { type: Number, default: 6 },\n pattern: { type: String, default: \"[0-9]\" },\n };\n static actions = [\"onInput\", \"onKeydown\", \"onPaste\"] as const;\n static events = [\"change\", \"complete\", \"invalid\"] as const;\n\n declare readonly fieldTargets: HTMLInputElement[];\n declare readonly valueTarget: HTMLInputElement;\n declare readonly errorTarget: HTMLElement;\n declare readonly hasValueTarget: boolean;\n declare readonly hasErrorTarget: boolean;\n\n declare lengthValue: number;\n declare patternValue: string;\n\n /** Owns IME lifecycle state across every digit field. */\n readonly #composition = new CompositionTracker({\n onEnd: (event) => {\n const input = event.currentTarget as HTMLInputElement | null;\n if (input) this.#handleInputValidation(input);\n },\n });\n\n override connect(): void {\n // Dynamically bind auto-selection and IME tracking logic to all connected inputs\n for (const field of this.fieldTargets) {\n field.addEventListener(\"focus\", this.#onFieldFocus);\n this.#composition.observe(field);\n }\n }\n\n override disconnect(): void {\n for (const field of this.fieldTargets) {\n field.removeEventListener(\"focus\", this.#onFieldFocus);\n }\n this.#composition.disconnect();\n }\n\n /**\n * Stimulus lifecycle callback when a new field target enters the DOM.\n * Ensures new additions are also wired with overwriting support.\n */\n fieldTargetConnected(element: HTMLInputElement): void {\n element.addEventListener(\"focus\", this.#onFieldFocus);\n this.#composition.observe(element);\n }\n\n /** Removes focus listeners when fields are dropped. */\n fieldTargetDisconnected(element: HTMLInputElement): void {\n element.removeEventListener(\"focus\", this.#onFieldFocus);\n this.#composition.unobserve(element);\n }\n\n /** Handles keystroke inputs and advances focus to the next field. */\n onInput(event: Event): void {\n const input = event.currentTarget as HTMLInputElement | null;\n if (!input) return;\n\n // Guard during active composition to prevent premature focus switching\n if (this.#composition.isComposing(event as InputEvent)) return;\n\n this.#handleInputValidation(input);\n }\n\n /** Handles Backspace retreating, arrows, and home/end navigation. */\n onKeydown(event: KeyboardEvent): void {\n if (isReservedArrowChord(event)) return;\n const input = event.currentTarget as HTMLInputElement | null;\n if (!input) return;\n\n const index = this.fieldTargets.indexOf(input);\n if (index === -1) return;\n\n // Do not trigger keydown actions during composition\n if (this.#composition.isComposing(event)) return;\n // Logical, not physical. The key is normalised rather than the\n // delta negated: these two branches are not mirror images — their bounds\n // guards differ — so swapping the key keeps each guard with its own direction.\n\n switch (logicalArrowKey(event.key, this.element)) {\n case \"Backspace\":\n if (!input.value) {\n // Empty field: step backward, wipe previous digit, and focus it\n if (index > 0) {\n event.preventDefault();\n const prevField = this.fieldTargets[index - 1];\n if (prevField) {\n prevField.value = \"\";\n prevField.removeAttribute(\"data-filled\");\n prevField.focus();\n this.#clearError();\n this.#syncAndDispatch();\n }\n }\n } else {\n // Filled field: clear current value\n input.value = \"\";\n input.removeAttribute(\"data-filled\");\n this.#clearError();\n this.#syncAndDispatch();\n }\n break;\n\n case \"ArrowLeft\":\n if (index > 0) {\n event.preventDefault();\n this.fieldTargets[index - 1]?.focus();\n }\n break;\n\n case \"ArrowRight\":\n if (index < this.lengthValue - 1) {\n event.preventDefault();\n this.fieldTargets[index + 1]?.focus();\n }\n break;\n\n case \"Home\":\n event.preventDefault();\n this.fieldTargets[0]?.focus();\n break;\n\n case \"End\":\n event.preventDefault();\n this.fieldTargets[this.lengthValue - 1]?.focus();\n break;\n\n default:\n break;\n }\n }\n\n /** Divides pasted string characters across available input fields. */\n onPaste(event: ClipboardEvent): void {\n const input = event.currentTarget as HTMLInputElement | null;\n if (!input) return;\n\n const startIndex = this.fieldTargets.indexOf(input);\n if (startIndex === -1) return;\n\n event.preventDefault();\n\n const rawText = event.clipboardData?.getData(\"text\") || \"\";\n const text = this.#normalizeValue(rawText);\n const regex = new RegExp(`^${this.patternValue}$`);\n const validChars = Array.from(text).filter((char) => regex.test(char));\n\n if (validChars.length === 0) {\n this.#showError();\n return;\n }\n\n const limit = Math.min(validChars.length, this.lengthValue - startIndex);\n let lastFocusedIndex = startIndex;\n\n for (let i = 0; i < limit; i++) {\n const fieldIndex = startIndex + i;\n const field = this.fieldTargets[fieldIndex];\n const char = validChars[i];\n\n if (field && char) {\n field.value = char;\n field.setAttribute(\"data-filled\", \"true\");\n lastFocusedIndex = fieldIndex;\n }\n }\n\n this.#clearError();\n\n // Set trailing focus position to the last filled box (or next empty if one exists)\n const focusTargetIndex =\n lastFocusedIndex < this.lengthValue - 1 ? lastFocusedIndex + 1 : lastFocusedIndex;\n this.fieldTargets[focusTargetIndex]?.focus();\n\n this.#syncAndDispatch();\n }\n\n readonly #onFieldFocus = (event: FocusEvent): void => {\n const input = event.currentTarget as HTMLInputElement | null;\n if (input) {\n // Auto-selection enables effortless character overwrites\n input.select();\n }\n };\n\n #handleInputValidation(input: HTMLInputElement): void {\n const index = this.fieldTargets.indexOf(input);\n if (index === -1) return;\n\n const rawValue = input.value;\n const normalized = this.#normalizeValue(rawValue);\n const regex = new RegExp(`^${this.patternValue}$`);\n\n if (normalized && regex.test(normalized)) {\n input.value = normalized;\n input.setAttribute(\"data-filled\", \"true\");\n this.#clearError();\n\n // Step focus forward\n if (index < this.lengthValue - 1) {\n const nextField = this.fieldTargets[index + 1];\n nextField?.focus();\n }\n } else if (normalized) {\n input.value = \"\";\n input.removeAttribute(\"data-filled\");\n this.#showError();\n } else {\n input.removeAttribute(\"data-filled\");\n }\n\n this.#syncAndDispatch();\n }\n\n #normalizeValue(val: string): string {\n // Replaces full-width numbers (0-9) with standard half-width ones (0-9)\n return val.replace(/[0-9]/g, (s) => String.fromCharCode(s.charCodeAt(0) - 0xfee0));\n }\n\n #showError(): void {\n if (this.hasErrorTarget) {\n this.errorTarget.removeAttribute(\"hidden\");\n }\n // Behavior only: emit a neutral payload and let the consumer compose/localize\n // the user-facing message. The pattern is reported so consumers can word it.\n this.dispatch(\"invalid\", { detail: { pattern: this.patternValue } });\n }\n\n #clearError(): void {\n if (this.hasErrorTarget) {\n this.errorTarget.setAttribute(\"hidden\", \"true\");\n }\n }\n\n #syncAndDispatch(): void {\n const fields = this.fieldTargets;\n const combinedValue = fields.map((f) => f.value).join(\"\");\n\n if (this.hasValueTarget) {\n this.valueTarget.value = combinedValue;\n }\n\n this.dispatch(\"change\", { detail: { value: combinedValue } });\n\n // Completed state when all fields contain a digit\n const isCompleted =\n fields.every((f) => f.value.length > 0) && combinedValue.length === this.lengthValue;\n if (isCompleted) {\n this.dispatch(\"complete\", { detail: { value: combinedValue } });\n }\n }\n}\n"]}
@@ -30,8 +30,9 @@ import { Controller } from '@hotwired/stimulus';
30
30
  * @remarks
31
31
  * Behavior only — shadows, arrows, and gradients are the consumer's CSS;
32
32
  * `data-overflow-*` carry no ARIA semantics. All observers/listeners are released
33
- * on `disconnect()` (Turbo navigation included). `scrollByPage` honors
34
- * `prefers-reduced-motion`.
33
+ * on `disconnect()` (Turbo navigation included). `scrollByPage` scrolls smoothly
34
+ * by default and forces an instant jump under `prefers-reduced-motion`, regardless
35
+ * of the consumer's CSS `scroll-behavior`.
35
36
  */
36
37
  declare class OverflowIndicatorController extends Controller<HTMLElement> {
37
38
  #private;