stimeo-ui 0.15.0 → 0.16.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 (267) hide show
  1. package/CHANGELOG.md +178 -0
  2. package/README.md +120 -0
  3. package/dist/cable/index.js +63 -15
  4. package/dist/cable/index.js.map +1 -1
  5. package/dist/controllers/accordion_controller.d.ts +30 -3
  6. package/dist/controllers/accordion_controller.js +98 -9
  7. package/dist/controllers/accordion_controller.js.map +1 -1
  8. package/dist/controllers/alert_dialog_controller.d.ts +8 -7
  9. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  10. package/dist/controllers/announcer_controller.d.ts +3 -2
  11. package/dist/controllers/announcer_controller.js +8 -1
  12. package/dist/controllers/announcer_controller.js.map +1 -1
  13. package/dist/controllers/auto_submit_controller.js +43 -2
  14. package/dist/controllers/auto_submit_controller.js.map +1 -1
  15. package/dist/controllers/avatar_controller.js +1 -1
  16. package/dist/controllers/avatar_controller.js.map +1 -1
  17. package/dist/controllers/breadcrumb_controller.d.ts +10 -7
  18. package/dist/controllers/breadcrumb_controller.js +9 -8
  19. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  20. package/dist/controllers/bulk_select_controller.d.ts +4 -4
  21. package/dist/controllers/bulk_select_controller.js +7 -6
  22. package/dist/controllers/bulk_select_controller.js.map +1 -1
  23. package/dist/controllers/calendar_controller.d.ts +85 -23
  24. package/dist/controllers/calendar_controller.js +323 -123
  25. package/dist/controllers/calendar_controller.js.map +1 -1
  26. package/dist/controllers/carousel_controller.d.ts +67 -22
  27. package/dist/controllers/carousel_controller.js +263 -38
  28. package/dist/controllers/carousel_controller.js.map +1 -1
  29. package/dist/controllers/character_counter_controller.d.ts +1 -1
  30. package/dist/controllers/character_counter_controller.js +40 -2
  31. package/dist/controllers/character_counter_controller.js.map +1 -1
  32. package/dist/controllers/checkbox_controller.js +54 -10
  33. package/dist/controllers/checkbox_controller.js.map +1 -1
  34. package/dist/controllers/clipboard_controller.d.ts +28 -2
  35. package/dist/controllers/clipboard_controller.js +63 -5
  36. package/dist/controllers/clipboard_controller.js.map +1 -1
  37. package/dist/controllers/collapsible_controller.d.ts +25 -1
  38. package/dist/controllers/collapsible_controller.js +99 -14
  39. package/dist/controllers/collapsible_controller.js.map +1 -1
  40. package/dist/controllers/color_picker_controller.d.ts +29 -8
  41. package/dist/controllers/color_picker_controller.js +80 -34
  42. package/dist/controllers/color_picker_controller.js.map +1 -1
  43. package/dist/controllers/combobox_controller.d.ts +10 -1
  44. package/dist/controllers/combobox_controller.js +106 -16
  45. package/dist/controllers/combobox_controller.js.map +1 -1
  46. package/dist/controllers/command_palette_controller.d.ts +3 -3
  47. package/dist/controllers/command_palette_controller.js +35 -3
  48. package/dist/controllers/command_palette_controller.js.map +1 -1
  49. package/dist/controllers/conditional_fields_controller.js +56 -11
  50. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  51. package/dist/controllers/confirm_controller.js +3 -0
  52. package/dist/controllers/confirm_controller.js.map +1 -1
  53. package/dist/controllers/context_menu_controller.d.ts +6 -0
  54. package/dist/controllers/context_menu_controller.js +32 -12
  55. package/dist/controllers/context_menu_controller.js.map +1 -1
  56. package/dist/controllers/countdown_controller.d.ts +30 -1
  57. package/dist/controllers/countdown_controller.js +129 -26
  58. package/dist/controllers/countdown_controller.js.map +1 -1
  59. package/dist/controllers/currency_input_controller.d.ts +74 -15
  60. package/dist/controllers/currency_input_controller.js +205 -58
  61. package/dist/controllers/currency_input_controller.js.map +1 -1
  62. package/dist/controllers/data_grid_controller.d.ts +63 -18
  63. package/dist/controllers/data_grid_controller.js +195 -29
  64. package/dist/controllers/data_grid_controller.js.map +1 -1
  65. package/dist/controllers/date_range_picker_controller.d.ts +34 -6
  66. package/dist/controllers/date_range_picker_controller.js +139 -27
  67. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  68. package/dist/controllers/dialog_controller.d.ts +10 -3
  69. package/dist/controllers/dialog_controller.js +35 -8
  70. package/dist/controllers/dialog_controller.js.map +1 -1
  71. package/dist/controllers/direct_upload_controller.js +22 -4
  72. package/dist/controllers/direct_upload_controller.js.map +1 -1
  73. package/dist/controllers/dirty_form_controller.d.ts +2 -2
  74. package/dist/controllers/dirty_form_controller.js +14 -1
  75. package/dist/controllers/dirty_form_controller.js.map +1 -1
  76. package/dist/controllers/dismissible_controller.js +1 -0
  77. package/dist/controllers/dismissible_controller.js.map +1 -1
  78. package/dist/controllers/drawer_controller.d.ts +18 -10
  79. package/dist/controllers/drawer_controller.js +54 -19
  80. package/dist/controllers/drawer_controller.js.map +1 -1
  81. package/dist/controllers/dropdown_controller.d.ts +9 -3
  82. package/dist/controllers/dropdown_controller.js +36 -9
  83. package/dist/controllers/dropdown_controller.js.map +1 -1
  84. package/dist/controllers/editable_controller.js +34 -0
  85. package/dist/controllers/editable_controller.js.map +1 -1
  86. package/dist/controllers/file_dropzone_controller.js +144 -51
  87. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  88. package/dist/controllers/filter_controller.d.ts +10 -4
  89. package/dist/controllers/filter_controller.js +20 -6
  90. package/dist/controllers/filter_controller.js.map +1 -1
  91. package/dist/controllers/flash_controller.d.ts +26 -6
  92. package/dist/controllers/flash_controller.js +305 -46
  93. package/dist/controllers/flash_controller.js.map +1 -1
  94. package/dist/controllers/focus_controller.js +1 -0
  95. package/dist/controllers/focus_controller.js.map +1 -1
  96. package/dist/controllers/form_field_controller.js +7 -5
  97. package/dist/controllers/form_field_controller.js.map +1 -1
  98. package/dist/controllers/form_validation_controller.js +19 -13
  99. package/dist/controllers/form_validation_controller.js.map +1 -1
  100. package/dist/controllers/frame_loading_controller.js +16 -2
  101. package/dist/controllers/frame_loading_controller.js.map +1 -1
  102. package/dist/controllers/highlight_controller.js +19 -2
  103. package/dist/controllers/highlight_controller.js.map +1 -1
  104. package/dist/controllers/hover_card_controller.d.ts +10 -2
  105. package/dist/controllers/hover_card_controller.js +40 -14
  106. package/dist/controllers/hover_card_controller.js.map +1 -1
  107. package/dist/controllers/idle_controller.d.ts +16 -3
  108. package/dist/controllers/idle_controller.js +90 -5
  109. package/dist/controllers/idle_controller.js.map +1 -1
  110. package/dist/controllers/input_mask_controller.d.ts +5 -2
  111. package/dist/controllers/input_mask_controller.js +65 -9
  112. package/dist/controllers/input_mask_controller.js.map +1 -1
  113. package/dist/controllers/intersection_controller.js +3 -0
  114. package/dist/controllers/intersection_controller.js.map +1 -1
  115. package/dist/controllers/lazy_frame_controller.js +11 -2
  116. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  117. package/dist/controllers/listbox_controller.d.ts +50 -8
  118. package/dist/controllers/listbox_controller.js +203 -45
  119. package/dist/controllers/listbox_controller.js.map +1 -1
  120. package/dist/controllers/masonry_controller.js +0 -2
  121. package/dist/controllers/masonry_controller.js.map +1 -1
  122. package/dist/controllers/menu_controller.d.ts +9 -3
  123. package/dist/controllers/menu_controller.js +45 -16
  124. package/dist/controllers/menu_controller.js.map +1 -1
  125. package/dist/controllers/menubar_controller.d.ts +11 -0
  126. package/dist/controllers/menubar_controller.js +58 -24
  127. package/dist/controllers/menubar_controller.js.map +1 -1
  128. package/dist/controllers/meter_controller.js +9 -5
  129. package/dist/controllers/meter_controller.js.map +1 -1
  130. package/dist/controllers/multi_select_controller.d.ts +18 -3
  131. package/dist/controllers/multi_select_controller.js +278 -104
  132. package/dist/controllers/multi_select_controller.js.map +1 -1
  133. package/dist/controllers/navigation_menu_controller.d.ts +11 -0
  134. package/dist/controllers/navigation_menu_controller.js +48 -15
  135. package/dist/controllers/navigation_menu_controller.js.map +1 -1
  136. package/dist/controllers/nested_form_controller.js +37 -8
  137. package/dist/controllers/nested_form_controller.js.map +1 -1
  138. package/dist/controllers/network_status_controller.js +9 -1
  139. package/dist/controllers/network_status_controller.js.map +1 -1
  140. package/dist/controllers/number_input_controller.d.ts +36 -8
  141. package/dist/controllers/number_input_controller.js +124 -21
  142. package/dist/controllers/number_input_controller.js.map +1 -1
  143. package/dist/controllers/optimistic_controller.js +14 -1
  144. package/dist/controllers/optimistic_controller.js.map +1 -1
  145. package/dist/controllers/otp_controller.d.ts +22 -7
  146. package/dist/controllers/otp_controller.js +198 -55
  147. package/dist/controllers/otp_controller.js.map +1 -1
  148. package/dist/controllers/overflow_indicator_controller.d.ts +8 -12
  149. package/dist/controllers/overflow_indicator_controller.js +84 -13
  150. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  151. package/dist/controllers/overflow_menu_controller.d.ts +26 -6
  152. package/dist/controllers/overflow_menu_controller.js +112 -41
  153. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  154. package/dist/controllers/pagination_controller.d.ts +19 -11
  155. package/dist/controllers/pagination_controller.js +74 -28
  156. package/dist/controllers/pagination_controller.js.map +1 -1
  157. package/dist/controllers/password_reveal_controller.d.ts +15 -1
  158. package/dist/controllers/password_reveal_controller.js +59 -2
  159. package/dist/controllers/password_reveal_controller.js.map +1 -1
  160. package/dist/controllers/persist_controller.js +30 -8
  161. package/dist/controllers/persist_controller.js.map +1 -1
  162. package/dist/controllers/pointer_drag_controller.js +131 -52
  163. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  164. package/dist/controllers/popover_controller.d.ts +9 -3
  165. package/dist/controllers/popover_controller.js +45 -11
  166. package/dist/controllers/popover_controller.js.map +1 -1
  167. package/dist/controllers/portal_controller.d.ts +1 -1
  168. package/dist/controllers/portal_controller.js +6 -2
  169. package/dist/controllers/portal_controller.js.map +1 -1
  170. package/dist/controllers/preview_guard_controller.js +16 -1
  171. package/dist/controllers/preview_guard_controller.js.map +1 -1
  172. package/dist/controllers/progress_controller.js +8 -4
  173. package/dist/controllers/progress_controller.js.map +1 -1
  174. package/dist/controllers/radio_group_controller.d.ts +6 -4
  175. package/dist/controllers/radio_group_controller.js +42 -17
  176. package/dist/controllers/radio_group_controller.js.map +1 -1
  177. package/dist/controllers/range_slider_controller.d.ts +49 -1
  178. package/dist/controllers/range_slider_controller.js +88 -42
  179. package/dist/controllers/range_slider_controller.js.map +1 -1
  180. package/dist/controllers/rating_controller.d.ts +14 -3
  181. package/dist/controllers/rating_controller.js +39 -15
  182. package/dist/controllers/rating_controller.js.map +1 -1
  183. package/dist/controllers/read_more_controller.d.ts +24 -2
  184. package/dist/controllers/read_more_controller.js +69 -2
  185. package/dist/controllers/read_more_controller.js.map +1 -1
  186. package/dist/controllers/resizable_controller.d.ts +16 -2
  187. package/dist/controllers/resizable_controller.js +82 -22
  188. package/dist/controllers/resizable_controller.js.map +1 -1
  189. package/dist/controllers/scroll_area_controller.js +5 -1
  190. package/dist/controllers/scroll_area_controller.js.map +1 -1
  191. package/dist/controllers/scroll_visibility_controller.js +0 -1
  192. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  193. package/dist/controllers/scrollspy_controller.d.ts +3 -2
  194. package/dist/controllers/scrollspy_controller.js +6 -0
  195. package/dist/controllers/scrollspy_controller.js.map +1 -1
  196. package/dist/controllers/separator_controller.d.ts +41 -14
  197. package/dist/controllers/separator_controller.js +66 -37
  198. package/dist/controllers/separator_controller.js.map +1 -1
  199. package/dist/controllers/sidebar_controller.d.ts +20 -3
  200. package/dist/controllers/sidebar_controller.js +77 -18
  201. package/dist/controllers/sidebar_controller.js.map +1 -1
  202. package/dist/controllers/skeleton_controller.js +6 -1
  203. package/dist/controllers/skeleton_controller.js.map +1 -1
  204. package/dist/controllers/slider_controller.d.ts +45 -7
  205. package/dist/controllers/slider_controller.js +82 -47
  206. package/dist/controllers/slider_controller.js.map +1 -1
  207. package/dist/controllers/smart_sticky_header_controller.js +11 -1
  208. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  209. package/dist/controllers/sortable_controller.js +17 -2
  210. package/dist/controllers/sortable_controller.js.map +1 -1
  211. package/dist/controllers/spinner_controller.js +10 -2
  212. package/dist/controllers/spinner_controller.js.map +1 -1
  213. package/dist/controllers/step_indicator_controller.d.ts +19 -17
  214. package/dist/controllers/step_indicator_controller.js +18 -17
  215. package/dist/controllers/step_indicator_controller.js.map +1 -1
  216. package/dist/controllers/stepper_controller.d.ts +34 -9
  217. package/dist/controllers/stepper_controller.js +101 -19
  218. package/dist/controllers/stepper_controller.js.map +1 -1
  219. package/dist/controllers/stick_to_bottom_controller.d.ts +19 -0
  220. package/dist/controllers/stick_to_bottom_controller.js +75 -5
  221. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  222. package/dist/controllers/submit_once_controller.d.ts +3 -2
  223. package/dist/controllers/submit_once_controller.js +16 -3
  224. package/dist/controllers/submit_once_controller.js.map +1 -1
  225. package/dist/controllers/switch_controller.d.ts +29 -7
  226. package/dist/controllers/switch_controller.js +101 -10
  227. package/dist/controllers/switch_controller.js.map +1 -1
  228. package/dist/controllers/tabs_controller.d.ts +12 -0
  229. package/dist/controllers/tabs_controller.js +21 -2
  230. package/dist/controllers/tabs_controller.js.map +1 -1
  231. package/dist/controllers/tags_input_controller.d.ts +15 -3
  232. package/dist/controllers/tags_input_controller.js +209 -59
  233. package/dist/controllers/tags_input_controller.js.map +1 -1
  234. package/dist/controllers/theme_controller.d.ts +20 -3
  235. package/dist/controllers/theme_controller.js +64 -14
  236. package/dist/controllers/theme_controller.js.map +1 -1
  237. package/dist/controllers/time_picker_controller.js +23 -8
  238. package/dist/controllers/time_picker_controller.js.map +1 -1
  239. package/dist/controllers/toast_controller.d.ts +52 -15
  240. package/dist/controllers/toast_controller.js +337 -54
  241. package/dist/controllers/toast_controller.js.map +1 -1
  242. package/dist/controllers/toggle_group_controller.d.ts +49 -7
  243. package/dist/controllers/toggle_group_controller.js +159 -23
  244. package/dist/controllers/toggle_group_controller.js.map +1 -1
  245. package/dist/controllers/toolbar_controller.js +32 -0
  246. package/dist/controllers/toolbar_controller.js.map +1 -1
  247. package/dist/controllers/tooltip_controller.d.ts +8 -0
  248. package/dist/controllers/tooltip_controller.js +39 -13
  249. package/dist/controllers/tooltip_controller.js.map +1 -1
  250. package/dist/controllers/transition_controller.js +4 -0
  251. package/dist/controllers/transition_controller.js.map +1 -1
  252. package/dist/controllers/tree_view_controller.d.ts +39 -8
  253. package/dist/controllers/tree_view_controller.js +169 -16
  254. package/dist/controllers/tree_view_controller.js.map +1 -1
  255. package/dist/index.d.ts +28 -1
  256. package/dist/index.js +4588 -1535
  257. package/dist/index.js.map +1 -1
  258. package/dist/inspector/cli.d.ts +72 -6
  259. package/dist/inspector/cli.js +262 -51
  260. package/dist/inspector/cli.js.map +1 -1
  261. package/dist/inspector/cli_bin.js +309 -51
  262. package/dist/inspector/cli_bin.js.map +1 -1
  263. package/dist/inspector/examples.json +40 -40
  264. package/dist/inspector/manifest.json +525 -59
  265. package/dist/positioning/index.js +2 -0
  266. package/dist/positioning/index.js.map +1 -1
  267. package/package.json +2 -2
@@ -20,6 +20,39 @@ function canTakeFocus(element) {
20
20
  return !inheritsFieldsetDisabled(element);
21
21
  }
22
22
 
23
+ // src/utils/microtask_coalescer.ts
24
+ var MicrotaskCoalescer = class {
25
+ #run;
26
+ #queued = false;
27
+ #active = false;
28
+ #generation = 0;
29
+ /** @param run - the single reconciliation pass, invoked at most once per batch. */
30
+ constructor(run) {
31
+ this.#run = run;
32
+ }
33
+ /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
34
+ activate() {
35
+ this.#active = true;
36
+ }
37
+ /** Closes the window and drops any pending pass; call from `disconnect()`. */
38
+ cancel() {
39
+ this.#active = false;
40
+ this.#queued = false;
41
+ this.#generation += 1;
42
+ }
43
+ /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
44
+ schedule() {
45
+ if (!this.#active || this.#queued) return;
46
+ this.#queued = true;
47
+ const generation = this.#generation;
48
+ queueMicrotask(() => {
49
+ if (generation !== this.#generation || !this.#queued || !this.#active) return;
50
+ this.#queued = false;
51
+ this.#run();
52
+ });
53
+ }
54
+ };
55
+
23
56
  // src/utils/before_cache_reset.ts
24
57
  var BeforeCacheReset = class _BeforeCacheReset {
25
58
  /** Every subscribed instance, iterated by the one shared document listener. */
@@ -81,53 +114,57 @@ var TabindexLoan = class {
81
114
  };
82
115
 
83
116
  // src/controllers/pagination_controller.ts
84
- var PaginationController = class _PaginationController extends Controller {
117
+ var PaginationController = class extends Controller {
85
118
  static targets = ["page", "prev", "next"];
86
119
  static values = {
87
120
  page: { type: Number, default: 1 },
88
121
  total: { type: Number, default: 1 }
89
122
  };
90
123
  static actions = ["next", "prev", "select"];
91
- static events = ["change"];
92
- /** Marks a `disabled` this controller applied at a boundary (ownership flag). */
93
- static #BOUNDARY_ATTR = "data-stimeo--pagination-boundary-disabled";
94
- #isConnected = false;
124
+ static events = ["change", "reconcile"];
125
+ /** Marks a `disabled` this controller applied at a boundary, in its own namespace. */
126
+ get #boundaryAttribute() {
127
+ return `data-${this.identifier}-boundary-disabled`;
128
+ }
95
129
  /** The `tabindex` this instance lends the root for the focus fallback. */
96
130
  #tabindex = new TabindexLoan();
97
- /** Normalizes out-of-range initial values and renders the initial state. */
131
+ /**
132
+ * Collapses the Value and target callbacks of one mutation into one pass, and
133
+ * refuses the ones Stimulus delivers before `connect()`, which renders itself.
134
+ */
135
+ #repaint = new MicrotaskCoalescer(() => this.#reconcilePage());
136
+ /** The page shown last, which the next move of the current page is measured from. */
137
+ #shown = 1;
138
+ /** Renders the initial state from the clamped `page` and `total`. */
98
139
  connect() {
99
- this.#isConnected = true;
100
- this.#normalizeAndRender();
140
+ this.#repaint.activate();
141
+ this.#shown = this.#page;
142
+ this.#render();
101
143
  }
102
- /** Reverts the one attribute the controller adds outside its state hooks. */
144
+ /** Drops a pending pass and reverts the one attribute added outside the state hooks. */
103
145
  disconnect() {
104
- this.#isConnected = false;
146
+ this.#repaint.cancel();
105
147
  this.#tabindex.returnAll();
106
148
  }
107
149
  /** Re-renders when application code (or a Turbo morph) changes `page` at runtime. */
108
150
  pageValueChanged() {
109
- if (!this.#isConnected) return;
110
- this.#normalizeAndRender();
151
+ this.#repaint.schedule();
111
152
  }
112
153
  /** Re-renders when application code (or a Turbo morph) changes `total` at runtime. */
113
154
  totalValueChanged() {
114
- if (!this.#isConnected) return;
115
- this.#normalizeAndRender();
155
+ this.#repaint.schedule();
116
156
  }
117
157
  /** Syncs a page button appended/replaced at runtime (the consumer owns the list). */
118
158
  pageTargetConnected() {
119
- if (!this.#isConnected) return;
120
- this.#render();
159
+ this.#repaint.schedule();
121
160
  }
122
161
  /** Syncs a `prev` button appended/replaced at runtime. */
123
162
  prevTargetConnected() {
124
- if (!this.#isConnected) return;
125
- this.#render();
163
+ this.#repaint.schedule();
126
164
  }
127
165
  /** Syncs a `next` button appended/replaced at runtime. */
128
166
  nextTargetConnected() {
129
- if (!this.#isConnected) return;
130
- this.#render();
167
+ this.#repaint.schedule();
131
168
  }
132
169
  /** Makes the clicked page button (its `data-page`) current. */
133
170
  select(event) {
@@ -153,22 +190,31 @@ var PaginationController = class _PaginationController extends Controller {
153
190
  const target = this.#clamp(page);
154
191
  if (target === previous) return;
155
192
  this.pageValue = target;
193
+ this.#shown = target;
156
194
  this.#render();
157
195
  this.dispatch("change", {
158
196
  detail: { page: target, total: this.#total, previous }
159
197
  });
160
198
  }
161
- /** Writes the normalized Values back (only when they differ) and renders. */
162
- #normalizeAndRender() {
163
- const total = this.#total;
164
- if (!Object.is(total, this.totalValue)) this.totalValue = total;
199
+ /**
200
+ * Renders one settled batch of Value and target changes, and reports a current
201
+ * page that moved from the one shown before as `reconcile`.
202
+ */
203
+ #reconcilePage() {
204
+ const previous = this.#shown;
165
205
  const page = this.#page;
166
- if (!Object.is(page, this.pageValue)) this.pageValue = page;
206
+ this.#shown = page;
167
207
  this.#render();
208
+ if (page !== previous) {
209
+ this.dispatch("reconcile", { detail: { page, total: this.#total, previous } });
210
+ }
168
211
  }
169
212
  /**
170
213
  * Syncs `aria-current` on the page buttons and the prev/next `disabled` state.
171
214
  *
215
+ * It reads `page` and `total` through their clamps and never writes either Value,
216
+ * so a declaration outside the range stays in the attributes as the page wrote it.
217
+ *
172
218
  * @stimeoRenderRoot
173
219
  */
174
220
  #render() {
@@ -201,7 +247,7 @@ var PaginationController = class _PaginationController extends Controller {
201
247
  #release(button, atBoundary) {
202
248
  if (!button || atBoundary || !this.#owns(button)) return;
203
249
  button.disabled = false;
204
- button.removeAttribute(_PaginationController.#BOUNDARY_ATTR);
250
+ button.removeAttribute(this.#boundaryAttribute);
205
251
  }
206
252
  /**
207
253
  * Disables a boundary button, first moving focus off it when it is the active
@@ -212,7 +258,7 @@ var PaginationController = class _PaginationController extends Controller {
212
258
  if (button.disabled && !this.#owns(button)) return;
213
259
  if (button === document.activeElement) this.#moveFocusAwayFrom(opposite);
214
260
  button.disabled = true;
215
- button.setAttribute(_PaginationController.#BOUNDARY_ATTR, "");
261
+ button.setAttribute(this.#boundaryAttribute, "");
216
262
  }
217
263
  /**
218
264
  * Moves focus to `opposite` (already resolved to `null` when it will stay
@@ -249,7 +295,7 @@ var PaginationController = class _PaginationController extends Controller {
249
295
  }
250
296
  /** Whether the button's current `disabled` was applied by boundary control. */
251
297
  #owns(button) {
252
- return button.hasAttribute(_PaginationController.#BOUNDARY_ATTR);
298
+ return button.hasAttribute(this.#boundaryAttribute);
253
299
  }
254
300
  };
255
301
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/focus_candidate.ts","../../src/utils/before_cache_reset.ts","../../src/utils/tabindex_loan.ts","../../src/controllers/pagination_controller.ts"],"names":[],"mappings":"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","file":"pagination_controller.js","sourcesContent":["/**\n * Whether a disabled `<fieldset>` ancestor actually reaches `control`.\n *\n * HTML exempts the contents of a fieldset's **first direct-child `<legend>`**, and\n * the exemption is per fieldset — a control legal in one legend can still be\n * disabled by a second, outer fieldset, so the walk continues upward.\n *\n * Exported on its own for callers that need the disabled-inheritance rule without\n * the rest of {@link canTakeFocus} — a control's availability check, or a `hidden`\n * walk bounded at a widget root, which is a different rule to compose with.\n */\nexport function inheritsFieldsetDisabled(control: HTMLElement): boolean {\n let fieldset: Element | null = control.closest(\"fieldset[disabled]\");\n while (fieldset) {\n const legend = Array.from(fieldset.children).find((child) => child.tagName === \"LEGEND\");\n if (!legend?.contains(control)) return true;\n fieldset = fieldset.parentElement?.closest(\"fieldset[disabled]\") ?? null;\n }\n return false;\n}\n\n/**\n * Whether an element can actually take focus, checked **before** `focus()` runs.\n *\n * A controller that must move focus off a control it is about to disable or hide\n * picks a destination and calls `focus()` on it. If that destination cannot take\n * focus, the call **fails silently**: `hidden` and natively `disabled` elements\n * swallow it, so the caret stays in the subtree that is disappearing and lands on\n * `<body>` a frame later — the exact outcome the rescue exists to prevent, minus\n * any signal that it happened.\n *\n * Testing after the fact is the obvious alternative and is deliberately not the\n * rule. Reading `document.activeElement` back only works in a real browser, and\n * looping over candidates that way performs a real focus move per failure —\n * observable to assistive technology. Checking first costs nothing and catches\n * the cases that actually occur.\n *\n * **`aria-disabled` is not disqualifying.** It is the attribute an author uses\n * for a control that must stay *discoverable*, and the roving contract keeps\n * such items reachable. Only the four conditions that make the platform refuse\n * focus are checked: `hidden` / `inert` (an ancestor's counts),\n * `input[type=\"hidden\"]`, the native `disabled` property, and `disabled`\n * inherited from an ancestor `fieldset`. CSS-only invisibility is handled by\n * {@link isRenderedForFocus} when a consumer needs sequential-focus semantics.\n *\n * Reading `:disabled` instead of walking the fieldset chain would be shorter, but\n * that pseudo-class is not evaluated consistently outside real browsers and this\n * has to be right headlessly too. happy-dom in particular focuses a `<button>`\n * inside a disabled fieldset where a real engine refuses, so the inheritance is\n * spelled out rather than delegated.\n *\n * What a consumer does when nothing survives is its own call: some fall back to\n * their landmark, while a widget whose caret already sits somewhere legitimate\n * refuses the move outright rather than relocating it.\n *\n * @example\n * ```ts\n * const target = candidates.find(canTakeFocus);\n * if (target) target.focus();\n * else {\n * this.#tabindex.lend(this.element); // nothing left: fall back to the landmark\n * this.element.focus();\n * }\n * ```\n *\n * @param element - the candidate destination\n */\nexport function canTakeFocus(element: HTMLElement): boolean {\n if (element.closest(\"[hidden], [inert]\")) return false;\n if (element instanceof HTMLInputElement && element.type === \"hidden\") return false;\n if (!(\"disabled\" in element)) return true;\n if ((element as HTMLElement & { disabled: boolean }).disabled) return false;\n return !inheritsFieldsetDisabled(element);\n}\n\n/** Elements whose semantics or authored attributes can place them in sequential focus order. */\nexport const TAB_STOP_CANDIDATE_SELECTOR = [\n \"a[href]\",\n \"area[href]\",\n \"button\",\n \"input\",\n \"select\",\n \"textarea\",\n \"summary\",\n \"iframe\",\n \"audio[controls]\",\n \"video[controls]\",\n \"[tabindex]\",\n \"[contenteditable]\",\n].join(\",\");\n\n/** Optional browser visibility API used to exclude CSS-hidden candidates. */\ninterface VisibilityCheckable {\n checkVisibility?: (options?: { visibilityProperty?: boolean }) => boolean;\n}\n\n/** Whether CSS visibility allows an otherwise eligible element to participate in focus order. */\nexport function isRenderedForFocus(element: HTMLElement): boolean {\n const check = (element as HTMLElement & VisibilityCheckable).checkVisibility;\n return typeof check === \"function\" ? check.call(element, { visibilityProperty: true }) : true;\n}\n\n/** Parses an authored `tabindex`; invalid syntax has no explicit focus-order meaning. */\nfunction authoredTabindex(element: HTMLElement): number | null {\n const value = element.getAttribute(\"tabindex\");\n if (value === null || !/^[+-]?\\d+$/.test(value.trim())) return null;\n return Number(value);\n}\n\n/** Whether the element's native semantics place it in sequential focus order. */\nfunction hasNativeTabStop(element: HTMLElement): boolean {\n if (element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement) {\n return element.hasAttribute(\"href\");\n }\n if (\n element instanceof HTMLButtonElement ||\n element instanceof HTMLSelectElement ||\n element instanceof HTMLTextAreaElement\n ) {\n return true;\n }\n if (element instanceof HTMLInputElement) return element.type !== \"hidden\";\n if (element instanceof HTMLIFrameElement) return true;\n if (element.tagName === \"AUDIO\" || element.tagName === \"VIDEO\") {\n return element.hasAttribute(\"controls\");\n }\n if (element instanceof HTMLElement && element.tagName === \"SUMMARY\") {\n const details = element.parentElement;\n return (\n details instanceof HTMLDetailsElement &&\n Array.from(details.children).find((child) => child.tagName === \"SUMMARY\") === element\n );\n }\n return false;\n}\n\n/** Whether an explicit `contenteditable` value creates an editable tab stop. */\nfunction hasEditableTabStop(element: HTMLElement): boolean {\n const value = element.getAttribute(\"contenteditable\")?.toLowerCase();\n return value === \"\" || value === \"true\" || value === \"plaintext-only\";\n}\n\n/**\n * Whether an element is a usable sequential Tab stop right now.\n *\n * Native semantics, authored `tabindex`, editable hosts, inherited disabled state,\n * HTML `hidden`/`inert`, and CSS visibility are evaluated together. `aria-disabled`\n * remains focusable because it communicates unavailability without removing the\n * control from discovery order.\n */\nexport function isTabStop(element: HTMLElement): boolean {\n if (!canTakeFocus(element) || !isRenderedForFocus(element)) return false;\n\n const tabindex = authoredTabindex(element);\n if (tabindex !== null) return tabindex >= 0;\n return hasNativeTabStop(element) || hasEditableTabStop(element);\n}\n\n/** Returns every usable sequential Tab stop below `root` in document order. */\nexport function tabStopsWithin(root: ParentNode): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)).filter(\n isTabStop,\n );\n}\n\n/** Returns the first usable sequential Tab stop below `root`, if one exists. */\nexport function firstTabStop(root: ParentNode): HTMLElement | null {\n for (const candidate of root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)) {\n if (isTabStop(candidate)) return candidate;\n }\n return null;\n}\n\n/** Whether `root` contains at least one usable sequential Tab stop. */\nexport function hasTabStop(root: ParentNode): boolean {\n return firstTabStop(root) !== null;\n}\n","/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way.\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","import { BeforeCacheReset } from \"./before_cache_reset\";\n\n/**\n * Shared bookkeeping for a `tabindex` a controller lends an element temporarily.\n *\n * A controller that must move focus somewhere the author never made focusable\n * (a landmark root, a scroll destination) reaches for the same trick: add a\n * `tabindex` just-in-time and hand it back once it is no longer needed. The\n * borrow is the easy half; the return is what the two conditions below are for.\n *\n * **Returning needs two conditions, not one.** Owning the borrow is not enough:\n * the attribute must also still hold the value this instance wrote. A consumer\n * that changed it afterwards — `tabindex=\"0\"` to make the root its own Tab stop\n * — owns it now, and removing it there silently discards authored markup. The\n * bookkeeping is dropped either way, since the loan is over regardless of who\n * ends up owning the value.\n *\n * **Never borrow over an existing value.** An element that already carries a\n * `tabindex` is the author's to control, so there is nothing to lend and nothing\n * to return.\n *\n * Every live loan also owns a shared `turbo:before-cache` subscription. The loan\n * is returned before Turbo can copy it into a snapshot, so consumers get cache\n * safety without duplicating a lifecycle hook; `returnAll()` removes the\n * subscription again as soon as no loan remains.\n *\n * The registry is keyed by element, so a controller borrowing on a single\n * element (`this.element`) and one borrowing across a changing set of targets\n * use the same API — the single-element case is a set of one. It holds no\n * opinion about *when* to borrow or where focus goes next; that stays in the\n * controller.\n *\n * **The API is deliberately two methods.** This file's own doc block is dropped\n * from `dist`, but every member comment is inlined into **each** consumer entry\n * (`tsup` builds with `splitting: false`), so rationale belongs here, only the\n * contract belongs on the members, and every method no consumer calls is still\n * paid for once per consumer entry.\n *\n * @example\n * ```ts\n * readonly #tabindex = new TabindexLoan();\n *\n * #rescueFocus() {\n * this.#tabindex.lend(this.element);\n * this.element.focus();\n * }\n *\n * disconnect() {\n * this.#tabindex.returnAll();\n * }\n * ```\n */\nexport class TabindexLoan<T extends HTMLElement = HTMLElement> {\n readonly #value: string;\n readonly #lent = new Set<T>();\n /** Returns live loans before Turbo can copy them into its page snapshot. */\n readonly #beforeCache = new BeforeCacheReset(() => this.returnAll());\n\n /**\n * @param value - the `tabindex` to lend. `\"-1\"` (the default) is\n * programmatically focusable but not a Tab stop; `\"0\"` is a real Tab stop,\n * which a scroll region with no focusable content of its own needs.\n */\n constructor(value: string = \"-1\") {\n this.#value = value;\n }\n\n /** Lends `element` the value; no-ops when it already carries a `tabindex`. */\n lend(element: T): void {\n if (element.hasAttribute(\"tabindex\")) return;\n element.setAttribute(\"tabindex\", this.#value);\n this.#lent.add(element);\n // Subscribe only while a real loan exists. Keeping this guarantee here means\n // every consumer is Turbo-safe without another lifecycle hook to remember.\n this.#beforeCache.activate();\n }\n\n /** Takes back every loan whose value is still the one that was lent. */\n returnAll(): void {\n for (const element of this.#lent) {\n if (element.getAttribute(\"tabindex\") === this.#value) element.removeAttribute(\"tabindex\");\n }\n this.#lent.clear();\n this.#beforeCache.deactivate();\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { canTakeFocus } from \"../utils/focus_candidate\";\nimport { TabindexLoan } from \"../utils/tabindex_loan\";\n\n/**\n * Headless, accessible pagination behavior.\n *\n * Markup contract (identifier: `stimeo--pagination`):\n * <nav data-controller=\"stimeo--pagination\" aria-label=\"Pagination\"\n * data-stimeo--pagination-page-value=\"1\"\n * data-stimeo--pagination-total-value=\"5\">\n * <button type=\"button\" data-stimeo--pagination-target=\"prev\"\n * data-action=\"click->stimeo--pagination#prev\">Prev</button>\n * <button type=\"button\" data-page=\"1\" aria-current=\"page\"\n * data-stimeo--pagination-target=\"page\"\n * data-action=\"click->stimeo--pagination#select\">1</button>\n * <!-- more page buttons -->\n * <button type=\"button\" data-stimeo--pagination-target=\"next\"\n * data-action=\"click->stimeo--pagination#next\">Next</button>\n * </nav>\n *\n * There is no dedicated APG pattern; this uses a navigation landmark plus\n * `aria-current=\"page\"`. The controller owns current-page state, the\n * `aria-current` sync, boundary disabling of prev/next, and the change event.\n * Generating/eliding the page buttons and fetching data stay with the consumer.\n *\n * `prev`/`next` must be real `<button>` elements: the boundary state is applied\n * through the native `disabled` property, which a `<div>` or `<a>` does not honor.\n *\n * `change` dispatches `{ page: number, total: number, previous: number }`.\n *\n * @remarks\n * Behavior only — each control is in the natural Tab order (no roving). When a\n * boundary disables the button that currently has focus, focus is moved first so\n * it is never lost to a `disabled` element.\n *\n * Behavior provided:\n * - `select` reads the clicked button's `data-page` and makes it current. The\n * value must parse as an integer; blank/fractional/non-numeric ones are ignored.\n * - `prev`/`next` step by one, clamped to `[1, total]`.\n * - The current page button gets `aria-current=\"page\"` (removed from the rest);\n * `prev` is `disabled` at page 1 and `next` at `total`.\n * - `page`/`total` are normalized (`total` to a finite integer >= 1, `page` into\n * `[1, total]`) on connect **and** on every runtime Value change, so JS-driven\n * updates re-render. Normalization never dispatches `change`: it is a display\n * correction, not a user navigation, and the initial render is consumer-owned.\n * - Page/boundary buttons swapped in at runtime re-render through the Stimulus\n * target callbacks, so a consumer-regenerated button list stays in sync.\n * - Every navigation dispatches `stimeo--pagination:change`, whose `detail.total`\n * is the same normalized total the boundary state is derived from.\n *\n * The boundary `disabled` is **owned**: the controller marks what it disabled\n * with `data-stimeo--pagination-boundary-disabled` and releases only that. A\n * button already disabled by the consumer is never marked when it overlaps a\n * boundary, so loading / permission state survives both entering and leaving the\n * boundary. The marker lives in the DOM rather than in a field so ownership\n * survives a Turbo cache restore, where a fresh instance would otherwise either\n * strand or steal the flag.\n */\nexport class PaginationController extends Controller<HTMLElement> {\n static override targets = [\"page\", \"prev\", \"next\"];\n static override values = {\n page: { type: Number, default: 1 },\n total: { type: Number, default: 1 },\n };\n static actions = [\"next\", \"prev\", \"select\"] as const;\n static events = [\"change\"] as const;\n\n /** Marks a `disabled` this controller applied at a boundary (ownership flag). */\n static readonly #BOUNDARY_ATTR = \"data-stimeo--pagination-boundary-disabled\";\n\n declare readonly pageTargets: HTMLElement[];\n declare readonly prevTarget: HTMLButtonElement;\n declare readonly nextTarget: HTMLButtonElement;\n declare readonly hasPrevTarget: boolean;\n declare readonly hasNextTarget: boolean;\n declare pageValue: number;\n declare totalValue: number;\n\n #isConnected = false;\n /** The `tabindex` this instance lends the root for the focus fallback. */\n readonly #tabindex = new TabindexLoan();\n\n /** Normalizes out-of-range initial values and renders the initial state. */\n override connect(): void {\n this.#isConnected = true;\n this.#normalizeAndRender();\n }\n\n /** Reverts the one attribute the controller adds outside its state hooks. */\n override disconnect(): void {\n this.#isConnected = false;\n this.#tabindex.returnAll();\n }\n\n /** Re-renders when application code (or a Turbo morph) changes `page` at runtime. */\n pageValueChanged(): void {\n if (!this.#isConnected) return;\n this.#normalizeAndRender();\n }\n\n /** Re-renders when application code (or a Turbo morph) changes `total` at runtime. */\n totalValueChanged(): void {\n if (!this.#isConnected) return;\n this.#normalizeAndRender();\n }\n\n /** Syncs a page button appended/replaced at runtime (the consumer owns the list). */\n pageTargetConnected(): void {\n if (!this.#isConnected) return;\n this.#render();\n }\n\n /** Syncs a `prev` button appended/replaced at runtime. */\n prevTargetConnected(): void {\n if (!this.#isConnected) return;\n this.#render();\n }\n\n /** Syncs a `next` button appended/replaced at runtime. */\n nextTargetConnected(): void {\n if (!this.#isConnected) return;\n this.#render();\n }\n\n /** Makes the clicked page button (its `data-page`) current. */\n select(event: Event): void {\n const button = event.currentTarget as HTMLElement;\n const raw = button.dataset.page;\n if (raw === undefined || raw.trim() === \"\") return;\n const page = Number(raw);\n // Integer-only, matching `stimeo--stepper`: `Number(\"\")` is 0 and `Number(\"2.7\")`\n // truncates, so a bare finite check would accept meaningless page numbers.\n if (!Number.isInteger(page)) return;\n this.#goto(page);\n }\n\n /** Steps to the previous page. */\n prev(): void {\n this.#goto(this.#page - 1);\n }\n\n /** Steps to the next page. */\n next(): void {\n this.#goto(this.#page + 1);\n }\n\n /** Moves to `page` (clamped to `[1, total]`), re-renders, and dispatches `change`. */\n #goto(page: number): void {\n if (!Number.isFinite(page)) return;\n const previous = this.#page;\n const target = this.#clamp(page);\n if (target === previous) return;\n this.pageValue = target;\n this.#render();\n this.dispatch(\"change\", {\n detail: { page: target, total: this.#total, previous },\n });\n }\n\n /** Writes the normalized Values back (only when they differ) and renders. */\n #normalizeAndRender(): void {\n // Stimulus setters rewrite the data attribute unconditionally, so guard the\n // no-op case to avoid waking consumer MutationObservers for nothing.\n const total = this.#total;\n if (!Object.is(total, this.totalValue)) this.totalValue = total;\n const page = this.#page;\n if (!Object.is(page, this.pageValue)) this.pageValue = page;\n this.#render();\n }\n\n /**\n * Syncs `aria-current` on the page buttons and the prev/next `disabled` state.\n *\n * @stimeoRenderRoot\n */\n #render(): void {\n const page = this.#page;\n for (const button of this.pageTargets) {\n if (Number(button.dataset.page) === page) {\n button.setAttribute(\"aria-current\", \"page\");\n } else {\n button.removeAttribute(\"aria-current\");\n }\n }\n\n const prev = this.hasPrevTarget ? this.prevTarget : null;\n const next = this.hasNextTarget ? this.nextTarget : null;\n const atStart = page <= 1;\n const atEnd = page >= this.#total;\n // Resolve both post-render states up front: a focus hand-off must target the\n // opposite button as it will be *after* this pass, not as it is now (moving\n // between the two boundaries of a 2-page set re-enables it in the same pass).\n const prevStaysDisabled = this.#disabledAfter(prev, atStart);\n const nextStaysDisabled = this.#disabledAfter(next, atEnd);\n // Release before disabling, so the hand-off can land on a button this same\n // pass re-enables (focusing a still-`disabled` button is a no-op).\n this.#release(prev, atStart);\n this.#release(next, atEnd);\n this.#disable(prev, atStart, nextStaysDisabled ? null : next);\n this.#disable(next, atEnd, prevStaysDisabled ? null : prev);\n }\n\n /** Whether a boundary button will still be `disabled` once this render applies. */\n #disabledAfter(button: HTMLButtonElement | null, atBoundary: boolean): boolean {\n if (!button) return true;\n if (atBoundary) return true;\n // Away from a boundary, only the controller's own `disabled` is released.\n return button.disabled && !this.#owns(button);\n }\n\n /** Releases the boundary `disabled` this controller owns, once away from it. */\n #release(button: HTMLButtonElement | null, atBoundary: boolean): void {\n if (!button || atBoundary || !this.#owns(button)) return;\n button.disabled = false;\n button.removeAttribute(PaginationController.#BOUNDARY_ATTR);\n }\n\n /**\n * Disables a boundary button, first moving focus off it when it is the active\n * element so disabling never strands focus.\n */\n #disable(\n button: HTMLButtonElement | null,\n atBoundary: boolean,\n opposite: HTMLButtonElement | null,\n ): void {\n if (!button || !atBoundary) return;\n if (button.disabled && !this.#owns(button)) return;\n if (button === document.activeElement) this.#moveFocusAwayFrom(opposite);\n button.disabled = true;\n button.setAttribute(PaginationController.#BOUNDARY_ATTR, \"\");\n }\n\n /**\n * Moves focus to `opposite` (already resolved to `null` when it will stay\n * disabled), else to the current page button, else to the landmark itself.\n */\n #moveFocusAwayFrom(opposite: HTMLButtonElement | null): void {\n const page = this.#page;\n const currentPage = this.pageTargets.find(\n (candidate) => Number(candidate.dataset.page) === page,\n );\n // Checked before the call, never after: `hidden` and natively disabled\n // elements swallow `focus()` silently, so an unchecked destination leaves the\n // caret in the subtree that is about to disable and drops it to <body> a frame\n // later — the outcome this rescue exists to prevent.\n // Narrowed to `HTMLElement`, not `HTMLButtonElement`: `pageTargets` is typed\n // `HTMLElement[]`, so a page control can legitimately be an `<a>`. The\n // destination is only ever focused, so the wider type is the honest one.\n const destination = [opposite, currentPage].find(\n (candidate): candidate is HTMLElement => candidate != null && canTakeFocus(candidate),\n );\n if (destination) {\n destination.focus();\n return;\n }\n // Nothing left to hand focus to (a lone boundary button, or every page button\n // disabled): keep it inside the pagination landmark instead of letting the\n // browser drop it to <body> when the button disables. The root is made\n // programmatically focusable just-in-time with `tabindex=\"-1\"`, which is not a\n // Tab stop; `disconnect()` removes it again when this controller added it.\n this.#tabindex.lend(this.element);\n this.element.focus();\n }\n\n /** Total pages, normalized to a finite integer >= 1. */\n get #total(): number {\n const total = this.totalValue;\n return Number.isFinite(total) ? Math.max(1, Math.trunc(total)) : 1;\n }\n\n /** The current page, normalized into `[1, total]`. */\n get #page(): number {\n return this.#clamp(this.pageValue);\n }\n\n /** Constrains `page` to `[1, total]`; a non-finite page falls back to page 1. */\n #clamp(page: number): number {\n if (!Number.isFinite(page)) return 1;\n return Math.min(this.#total, Math.max(1, Math.trunc(page)));\n }\n\n /** Whether the button's current `disabled` was applied by boundary control. */\n #owns(button: HTMLButtonElement): boolean {\n return button.hasAttribute(PaginationController.#BOUNDARY_ATTR);\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/focus_candidate.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/before_cache_reset.ts","../../src/utils/tabindex_loan.ts","../../src/controllers/pagination_controller.ts"],"names":[],"mappings":"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","file":"pagination_controller.js","sourcesContent":["/**\n * Whether a disabled `<fieldset>` ancestor actually reaches `control`.\n *\n * HTML exempts the contents of a fieldset's **first direct-child `<legend>`**, and\n * the exemption is per fieldset — a control legal in one legend can still be\n * disabled by a second, outer fieldset, so the walk continues upward.\n *\n * Exported on its own for callers that need the disabled-inheritance rule without\n * the rest of {@link canTakeFocus} — a control's availability check, or a `hidden`\n * walk bounded at a widget root, which is a different rule to compose with.\n */\nexport function inheritsFieldsetDisabled(control: HTMLElement): boolean {\n let fieldset: Element | null = control.closest(\"fieldset[disabled]\");\n while (fieldset) {\n const legend = Array.from(fieldset.children).find((child) => child.tagName === \"LEGEND\");\n if (!legend?.contains(control)) return true;\n fieldset = fieldset.parentElement?.closest(\"fieldset[disabled]\") ?? null;\n }\n return false;\n}\n\n/**\n * Whether an element can actually take focus, checked **before** `focus()` runs.\n *\n * A controller that must move focus off a control it is about to disable or hide\n * picks a destination and calls `focus()` on it. If that destination cannot take\n * focus, the call **fails silently**: `hidden` and natively `disabled` elements\n * swallow it, so the caret stays in the subtree that is disappearing and lands on\n * `<body>` a frame later — the exact outcome the rescue exists to prevent, minus\n * any signal that it happened.\n *\n * Testing after the fact is the obvious alternative and is deliberately not the\n * rule. Reading `document.activeElement` back only works in a real browser, and\n * looping over candidates that way performs a real focus move per failure —\n * observable to assistive technology. Checking first costs nothing and catches\n * the cases that actually occur.\n *\n * **`aria-disabled` is not disqualifying.** It is the attribute an author uses\n * for a control that must stay *discoverable*, and the roving contract keeps\n * such items reachable. Only the four conditions that make the platform refuse\n * focus are checked: `hidden` / `inert` (an ancestor's counts),\n * `input[type=\"hidden\"]`, the native `disabled` property, and `disabled`\n * inherited from an ancestor `fieldset`. CSS-only invisibility is handled by\n * {@link isRenderedForFocus} when a consumer needs sequential-focus semantics.\n *\n * Reading `:disabled` instead of walking the fieldset chain would be shorter, but\n * that pseudo-class is not evaluated consistently outside real browsers and this\n * has to be right headlessly too. happy-dom in particular focuses a `<button>`\n * inside a disabled fieldset where a real engine refuses, so the inheritance is\n * spelled out rather than delegated.\n *\n * What a consumer does when nothing survives is its own call: some fall back to\n * their landmark, while a widget whose caret already sits somewhere legitimate\n * refuses the move outright rather than relocating it.\n *\n * @example\n * ```ts\n * const target = candidates.find(canTakeFocus);\n * if (target) target.focus();\n * else {\n * this.#tabindex.lend(this.element); // nothing left: fall back to the landmark\n * this.element.focus();\n * }\n * ```\n *\n * @param element - the candidate destination\n */\nexport function canTakeFocus(element: HTMLElement): boolean {\n if (element.closest(\"[hidden], [inert]\")) return false;\n if (element instanceof HTMLInputElement && element.type === \"hidden\") return false;\n if (!(\"disabled\" in element)) return true;\n if ((element as HTMLElement & { disabled: boolean }).disabled) return false;\n return !inheritsFieldsetDisabled(element);\n}\n\n/** Elements whose semantics or authored attributes can place them in sequential focus order. */\nexport const TAB_STOP_CANDIDATE_SELECTOR = [\n \"a[href]\",\n \"area[href]\",\n \"button\",\n \"input\",\n \"select\",\n \"textarea\",\n \"summary\",\n \"iframe\",\n \"audio[controls]\",\n \"video[controls]\",\n \"[tabindex]\",\n \"[contenteditable]\",\n].join(\",\");\n\n/** Optional browser visibility API used to exclude CSS-hidden candidates. */\ninterface VisibilityCheckable {\n checkVisibility?: (options?: { visibilityProperty?: boolean }) => boolean;\n}\n\n/** Whether CSS visibility allows an otherwise eligible element to participate in focus order. */\nexport function isRenderedForFocus(element: HTMLElement): boolean {\n const check = (element as HTMLElement & VisibilityCheckable).checkVisibility;\n return typeof check === \"function\" ? check.call(element, { visibilityProperty: true }) : true;\n}\n\n/** Parses an authored `tabindex`; invalid syntax has no explicit focus-order meaning. */\nfunction authoredTabindex(element: HTMLElement): number | null {\n const value = element.getAttribute(\"tabindex\");\n if (value === null || !/^[+-]?\\d+$/.test(value.trim())) return null;\n return Number(value);\n}\n\n/** Whether the element's native semantics place it in sequential focus order. */\nfunction hasNativeTabStop(element: HTMLElement): boolean {\n if (element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement) {\n return element.hasAttribute(\"href\");\n }\n if (\n element instanceof HTMLButtonElement ||\n element instanceof HTMLSelectElement ||\n element instanceof HTMLTextAreaElement\n ) {\n return true;\n }\n if (element instanceof HTMLInputElement) return element.type !== \"hidden\";\n if (element instanceof HTMLIFrameElement) return true;\n if (element.tagName === \"AUDIO\" || element.tagName === \"VIDEO\") {\n return element.hasAttribute(\"controls\");\n }\n if (element instanceof HTMLElement && element.tagName === \"SUMMARY\") {\n const details = element.parentElement;\n return (\n details instanceof HTMLDetailsElement &&\n Array.from(details.children).find((child) => child.tagName === \"SUMMARY\") === element\n );\n }\n return false;\n}\n\n/** Whether an explicit `contenteditable` value creates an editable tab stop. */\nfunction hasEditableTabStop(element: HTMLElement): boolean {\n const value = element.getAttribute(\"contenteditable\")?.toLowerCase();\n return value === \"\" || value === \"true\" || value === \"plaintext-only\";\n}\n\n/**\n * Whether an element is a usable sequential Tab stop right now.\n *\n * Native semantics, authored `tabindex`, editable hosts, inherited disabled state,\n * HTML `hidden`/`inert`, and CSS visibility are evaluated together. `aria-disabled`\n * remains focusable because it communicates unavailability without removing the\n * control from discovery order.\n */\nexport function isTabStop(element: HTMLElement): boolean {\n if (!canTakeFocus(element) || !isRenderedForFocus(element)) return false;\n\n const tabindex = authoredTabindex(element);\n if (tabindex !== null) return tabindex >= 0;\n return hasNativeTabStop(element) || hasEditableTabStop(element);\n}\n\n/** Returns every usable sequential Tab stop below `root` in document order. */\nexport function tabStopsWithin(root: ParentNode): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)).filter(\n isTabStop,\n );\n}\n\n/** Returns the first usable sequential Tab stop below `root`, if one exists. */\nexport function firstTabStop(root: ParentNode): HTMLElement | null {\n for (const candidate of root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)) {\n if (isTabStop(candidate)) return candidate;\n }\n return null;\n}\n\n/** Whether `root` contains at least one usable sequential Tab stop. */\nexport function hasTabStop(root: ParentNode): boolean {\n return firstTabStop(root) !== null;\n}\n","/**\n * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way.\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","import { BeforeCacheReset } from \"./before_cache_reset\";\n\n/**\n * Shared bookkeeping for a `tabindex` a controller lends an element temporarily.\n *\n * A controller that must move focus somewhere the author never made focusable\n * (a landmark root, a scroll destination) reaches for the same trick: add a\n * `tabindex` just-in-time and hand it back once it is no longer needed. The\n * borrow is the easy half; the return is what the two conditions below are for.\n *\n * **Returning needs two conditions, not one.** Owning the borrow is not enough:\n * the attribute must also still hold the value this instance wrote. A consumer\n * that changed it afterwards — `tabindex=\"0\"` to make the root its own Tab stop\n * — owns it now, and removing it there silently discards authored markup. The\n * bookkeeping is dropped either way, since the loan is over regardless of who\n * ends up owning the value.\n *\n * **Never borrow over an existing value.** An element that already carries a\n * `tabindex` is the author's to control, so there is nothing to lend and nothing\n * to return.\n *\n * Every live loan also owns a shared `turbo:before-cache` subscription. The loan\n * is returned before Turbo can copy it into a snapshot, so consumers get cache\n * safety without duplicating a lifecycle hook; `returnAll()` removes the\n * subscription again as soon as no loan remains.\n *\n * The registry is keyed by element, so a controller borrowing on a single\n * element (`this.element`) and one borrowing across a changing set of targets\n * use the same API — the single-element case is a set of one. It holds no\n * opinion about *when* to borrow or where focus goes next; that stays in the\n * controller.\n *\n * **The API is deliberately two methods.** This file's own doc block is dropped\n * from `dist`, but every member comment is inlined into **each** consumer entry\n * (`tsup` builds with `splitting: false`), so rationale belongs here, only the\n * contract belongs on the members, and every method no consumer calls is still\n * paid for once per consumer entry.\n *\n * @example\n * ```ts\n * readonly #tabindex = new TabindexLoan();\n *\n * #rescueFocus() {\n * this.#tabindex.lend(this.element);\n * this.element.focus();\n * }\n *\n * disconnect() {\n * this.#tabindex.returnAll();\n * }\n * ```\n */\nexport class TabindexLoan<T extends HTMLElement = HTMLElement> {\n readonly #value: string;\n readonly #lent = new Set<T>();\n /** Returns live loans before Turbo can copy them into its page snapshot. */\n readonly #beforeCache = new BeforeCacheReset(() => this.returnAll());\n\n /**\n * @param value - the `tabindex` to lend. `\"-1\"` (the default) is\n * programmatically focusable but not a Tab stop; `\"0\"` is a real Tab stop,\n * which a scroll region with no focusable content of its own needs.\n */\n constructor(value: string = \"-1\") {\n this.#value = value;\n }\n\n /** Lends `element` the value; no-ops when it already carries a `tabindex`. */\n lend(element: T): void {\n if (element.hasAttribute(\"tabindex\")) return;\n element.setAttribute(\"tabindex\", this.#value);\n this.#lent.add(element);\n // Subscribe only while a real loan exists. Keeping this guarantee here means\n // every consumer is Turbo-safe without another lifecycle hook to remember.\n this.#beforeCache.activate();\n }\n\n /** Takes back every loan whose value is still the one that was lent. */\n returnAll(): void {\n for (const element of this.#lent) {\n if (element.getAttribute(\"tabindex\") === this.#value) element.removeAttribute(\"tabindex\");\n }\n this.#lent.clear();\n this.#beforeCache.deactivate();\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { canTakeFocus } from \"../utils/focus_candidate\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\nimport { TabindexLoan } from \"../utils/tabindex_loan\";\n\n/**\n * Headless, accessible pagination behavior.\n *\n * Markup contract (identifier: `stimeo--pagination`):\n * <nav data-controller=\"stimeo--pagination\" aria-label=\"Pagination\"\n * data-stimeo--pagination-page-value=\"1\"\n * data-stimeo--pagination-total-value=\"5\">\n * <button type=\"button\" data-stimeo--pagination-target=\"prev\"\n * data-action=\"click->stimeo--pagination#prev\">Prev</button>\n * <button type=\"button\" data-page=\"1\" aria-current=\"page\"\n * data-stimeo--pagination-target=\"page\"\n * data-action=\"click->stimeo--pagination#select\">1</button>\n * <!-- more page buttons -->\n * <button type=\"button\" data-stimeo--pagination-target=\"next\"\n * data-action=\"click->stimeo--pagination#next\">Next</button>\n * </nav>\n *\n * There is no dedicated APG pattern; this uses a navigation landmark plus\n * `aria-current=\"page\"`. The controller owns current-page state, the\n * `aria-current` sync, boundary disabling of prev/next, and the change event.\n * Generating/eliding the page buttons and fetching data stay with the consumer.\n *\n * `prev`/`next` must be real `<button>` elements: the boundary state is applied\n * through the native `disabled` property, which a `<div>` or `<a>` does not honor.\n *\n * `change` dispatches `{ page: number, total: number, previous: number }`, and\n * `reconcile` dispatches the same `{ page: number, total: number, previous: number }`\n * — `previous` is the page shown before — when a change the page made moves the\n * current page.\n *\n * @remarks\n * Behavior only — each control is in the natural Tab order (no roving). When a\n * boundary disables the button that currently has focus, focus is moved first so\n * it is never lost to a `disabled` element.\n *\n * Behavior provided:\n * - `select` reads the clicked button's `data-page` and makes it current. The\n * value must parse as an integer; blank/fractional/non-numeric ones are ignored.\n * - `prev`/`next` step by one, clamped to `[1, total]`.\n * - The current page button gets `aria-current=\"page\"` (removed from the rest);\n * `prev` is `disabled` at page 1 and `next` at `total`.\n * - `page`/`total` are read through a clamp (`total` as a finite integer >= 1,\n * `page` into `[1, total]`) on connect **and** on every runtime Value change, so\n * JS-driven updates re-render. The clamp is for display only: the Values keep\n * what the page declared, so a `page` beyond the current `total` is shown as soon\n * as a later `total` reaches it, and only a navigation writes `page`.\n * - Runtime Value changes and page/boundary buttons swapped in at runtime\n * re-render once per batch, so a consumer-regenerated button list stays in\n * sync. Re-rendering never dispatches `change`: it is not a user navigation.\n * Once connected, a batch that moves the current page away from the one shown\n * dispatches `stimeo--pagination:reconcile` once; the initial render reports\n * nothing.\n * - Every navigation dispatches `stimeo--pagination:change`, whose `detail.total`\n * is the same clamped total the boundary state is derived from.\n *\n * The boundary `disabled` is **owned**: the controller marks what it disabled\n * with `data-stimeo--pagination-boundary-disabled` and releases only that. A\n * button already disabled by the consumer is never marked when it overlaps a\n * boundary, so loading / permission state survives both entering and leaving the\n * boundary. The marker lives in the DOM rather than in a field so ownership\n * survives a Turbo cache restore, where a fresh instance would otherwise either\n * strand or steal the flag.\n */\nexport class PaginationController extends Controller<HTMLElement> {\n static override targets = [\"page\", \"prev\", \"next\"];\n static override values = {\n page: { type: Number, default: 1 },\n total: { type: Number, default: 1 },\n };\n static actions = [\"next\", \"prev\", \"select\"] as const;\n static events = [\"change\", \"reconcile\"] as const;\n\n /** Marks a `disabled` this controller applied at a boundary, in its own namespace. */\n get #boundaryAttribute(): string {\n return `data-${this.identifier}-boundary-disabled`;\n }\n\n declare readonly pageTargets: HTMLElement[];\n declare readonly prevTarget: HTMLButtonElement;\n declare readonly nextTarget: HTMLButtonElement;\n declare readonly hasPrevTarget: boolean;\n declare readonly hasNextTarget: boolean;\n declare pageValue: number;\n declare totalValue: number;\n\n /** The `tabindex` this instance lends the root for the focus fallback. */\n readonly #tabindex = new TabindexLoan();\n\n /**\n * Collapses the Value and target callbacks of one mutation into one pass, and\n * refuses the ones Stimulus delivers before `connect()`, which renders itself.\n */\n readonly #repaint = new MicrotaskCoalescer(() => this.#reconcilePage());\n\n /** The page shown last, which the next move of the current page is measured from. */\n #shown = 1;\n\n /** Renders the initial state from the clamped `page` and `total`. */\n override connect(): void {\n this.#repaint.activate();\n this.#shown = this.#page;\n this.#render();\n }\n\n /** Drops a pending pass and reverts the one attribute added outside the state hooks. */\n override disconnect(): void {\n this.#repaint.cancel();\n this.#tabindex.returnAll();\n }\n\n /** Re-renders when application code (or a Turbo morph) changes `page` at runtime. */\n pageValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /** Re-renders when application code (or a Turbo morph) changes `total` at runtime. */\n totalValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /** Syncs a page button appended/replaced at runtime (the consumer owns the list). */\n pageTargetConnected(): void {\n this.#repaint.schedule();\n }\n\n /** Syncs a `prev` button appended/replaced at runtime. */\n prevTargetConnected(): void {\n this.#repaint.schedule();\n }\n\n /** Syncs a `next` button appended/replaced at runtime. */\n nextTargetConnected(): void {\n this.#repaint.schedule();\n }\n\n /** Makes the clicked page button (its `data-page`) current. */\n select(event: Event): void {\n const button = event.currentTarget as HTMLElement;\n const raw = button.dataset.page;\n if (raw === undefined || raw.trim() === \"\") return;\n const page = Number(raw);\n // Integer-only, matching `stimeo--stepper`: `Number(\"\")` is 0 and `Number(\"2.7\")`\n // is a fraction, so a bare finite check would accept meaningless page numbers.\n if (!Number.isInteger(page)) return;\n this.#goto(page);\n }\n\n /** Steps to the previous page. */\n prev(): void {\n this.#goto(this.#page - 1);\n }\n\n /** Steps to the next page. */\n next(): void {\n this.#goto(this.#page + 1);\n }\n\n /** Moves to `page` (clamped to `[1, total]`), re-renders, and dispatches `change`. */\n #goto(page: number): void {\n if (!Number.isFinite(page)) return;\n const previous = this.#page;\n const target = this.#clamp(page);\n if (target === previous) return;\n this.pageValue = target;\n // Settled before the report, so the pass the Value write starts finds the page\n // already shown, and a listener that navigates on is measured from this page.\n this.#shown = target;\n this.#render();\n this.dispatch(\"change\", {\n detail: { page: target, total: this.#total, previous },\n });\n }\n\n /**\n * Renders one settled batch of Value and target changes, and reports a current\n * page that moved from the one shown before as `reconcile`.\n */\n #reconcilePage(): void {\n const previous = this.#shown;\n const page = this.#page;\n this.#shown = page;\n this.#render();\n if (page !== previous) {\n this.dispatch(\"reconcile\", { detail: { page, total: this.#total, previous } });\n }\n }\n\n /**\n * Syncs `aria-current` on the page buttons and the prev/next `disabled` state.\n *\n * It reads `page` and `total` through their clamps and never writes either Value,\n * so a declaration outside the range stays in the attributes as the page wrote it.\n *\n * @stimeoRenderRoot\n */\n #render(): void {\n const page = this.#page;\n for (const button of this.pageTargets) {\n if (Number(button.dataset.page) === page) {\n button.setAttribute(\"aria-current\", \"page\");\n } else {\n button.removeAttribute(\"aria-current\");\n }\n }\n\n const prev = this.hasPrevTarget ? this.prevTarget : null;\n const next = this.hasNextTarget ? this.nextTarget : null;\n const atStart = page <= 1;\n const atEnd = page >= this.#total;\n // Resolve both post-render states up front: a focus hand-off must target the\n // opposite button as it will be *after* this pass, not as it is now (moving\n // between the two boundaries of a 2-page set re-enables it in the same pass).\n const prevStaysDisabled = this.#disabledAfter(prev, atStart);\n const nextStaysDisabled = this.#disabledAfter(next, atEnd);\n // Release before disabling, so the hand-off can land on a button this same\n // pass re-enables (focusing a still-`disabled` button is a no-op).\n this.#release(prev, atStart);\n this.#release(next, atEnd);\n this.#disable(prev, atStart, nextStaysDisabled ? null : next);\n this.#disable(next, atEnd, prevStaysDisabled ? null : prev);\n }\n\n /** Whether a boundary button will still be `disabled` once this render applies. */\n #disabledAfter(button: HTMLButtonElement | null, atBoundary: boolean): boolean {\n if (!button) return true;\n if (atBoundary) return true;\n // Away from a boundary, only the controller's own `disabled` is released.\n return button.disabled && !this.#owns(button);\n }\n\n /** Releases the boundary `disabled` this controller owns, once away from it. */\n #release(button: HTMLButtonElement | null, atBoundary: boolean): void {\n if (!button || atBoundary || !this.#owns(button)) return;\n button.disabled = false;\n button.removeAttribute(this.#boundaryAttribute);\n }\n\n /**\n * Disables a boundary button, first moving focus off it when it is the active\n * element so disabling never strands focus.\n */\n #disable(\n button: HTMLButtonElement | null,\n atBoundary: boolean,\n opposite: HTMLButtonElement | null,\n ): void {\n if (!button || !atBoundary) return;\n if (button.disabled && !this.#owns(button)) return;\n if (button === document.activeElement) this.#moveFocusAwayFrom(opposite);\n button.disabled = true;\n button.setAttribute(this.#boundaryAttribute, \"\");\n }\n\n /**\n * Moves focus to `opposite` (already resolved to `null` when it will stay\n * disabled), else to the current page button, else to the landmark itself.\n */\n #moveFocusAwayFrom(opposite: HTMLButtonElement | null): void {\n const page = this.#page;\n const currentPage = this.pageTargets.find(\n (candidate) => Number(candidate.dataset.page) === page,\n );\n // Checked before the call, never after: `hidden` and natively disabled\n // elements swallow `focus()` silently, so an unchecked destination leaves the\n // caret in the subtree that is about to disable and drops it to <body> a frame\n // later — the outcome this rescue exists to prevent.\n // Narrowed to `HTMLElement`, not `HTMLButtonElement`: `pageTargets` is typed\n // `HTMLElement[]`, so a page control can legitimately be an `<a>`. The\n // destination is only ever focused, so the wider type is the honest one.\n const destination = [opposite, currentPage].find(\n (candidate): candidate is HTMLElement => candidate != null && canTakeFocus(candidate),\n );\n if (destination) {\n destination.focus();\n return;\n }\n // Nothing left to hand focus to (a lone boundary button, or every page button\n // disabled): keep it inside the pagination landmark instead of letting the\n // browser drop it to <body> when the button disables. The root is made\n // programmatically focusable just-in-time with `tabindex=\"-1\"`, which is not a\n // Tab stop; `disconnect()` removes it again when this controller added it.\n this.#tabindex.lend(this.element);\n this.element.focus();\n }\n\n /** Total pages, normalized to a finite integer >= 1. */\n get #total(): number {\n const total = this.totalValue;\n return Number.isFinite(total) ? Math.max(1, Math.trunc(total)) : 1;\n }\n\n /** The current page, normalized into `[1, total]`. */\n get #page(): number {\n return this.#clamp(this.pageValue);\n }\n\n /** Constrains `page` to `[1, total]`; a non-finite page falls back to page 1. */\n #clamp(page: number): number {\n if (!Number.isFinite(page)) return 1;\n return Math.min(this.#total, Math.max(1, Math.trunc(page)));\n }\n\n /** Whether the button's current `disabled` was applied by boundary control. */\n #owns(button: HTMLButtonElement): boolean {\n return button.hasAttribute(this.#boundaryAttribute);\n }\n}\n"]}
@@ -9,13 +9,21 @@ import { Controller } from '@hotwired/stimulus';
9
9
  * data-stimeo--password-reveal-target="input">
10
10
  * <button type="button" aria-pressed="false" aria-label="Show password"
11
11
  * data-stimeo--password-reveal-target="toggle"
12
- * data-action="click->stimeo--password-reveal#toggle"></button>
12
+ * data-action="click->stimeo--password-reveal#toggle">
13
+ * <span data-stimeo--password-reveal-target="offLabel">Show</span>
14
+ * <span data-stimeo--password-reveal-target="onLabel" hidden>Hide</span>
15
+ * </button>
13
16
  * </div>
14
17
  *
15
18
  * No dedicated APG pattern; this follows the toggle **Button** practice. The
16
19
  * accessible name stays state-independent ("Show password") while the pressed
17
20
  * state is conveyed by `aria-pressed`.
18
21
  *
22
+ * The button may carry an optional label pair — `onLabel` while the field is
23
+ * revealed, `offLabel` while it is masked. The name comes from `aria-label`, so the
24
+ * pair is a visual affordance: which half shows follows the state, and where only
25
+ * one of the two is declared inside the button the author's own visibility stands.
26
+ *
19
27
  * `toggle` dispatches `{ visible: boolean }`.
20
28
  *
21
29
  * @remarks
@@ -49,6 +57,8 @@ declare class PasswordRevealController extends Controller<HTMLElement> {
49
57
  static events: readonly ["toggle"];
50
58
  readonly inputTarget: HTMLInputElement;
51
59
  readonly toggleTarget: HTMLElement;
60
+ readonly onLabelTargets: HTMLElement[];
61
+ readonly offLabelTargets: HTMLElement[];
52
62
  readonly hasInputTarget: boolean;
53
63
  readonly hasToggleTarget: boolean;
54
64
  autoHideValue: number;
@@ -69,6 +79,10 @@ declare class PasswordRevealController extends Controller<HTMLElement> {
69
79
  inputTargetDisconnected(): void;
70
80
  /** Re-derives the pressed state for a button swapped in after connect. */
71
81
  toggleTargetConnected(): void;
82
+ /** Describes the state to a revealed-side label that arrives after connect. */
83
+ onLabelTargetConnected(): void;
84
+ /** Describes the state to a masked-side label that arrives after connect. */
85
+ offLabelTargetConnected(): void;
72
86
  /** Toggles the input between masked and revealed. Bound via `data-action`. */
73
87
  toggle(): void;
74
88
  }
@@ -84,10 +84,51 @@ var SafeTimeout = class extends TimerRegistry {
84
84
  }
85
85
  };
86
86
 
87
+ // src/utils/state_regions.ts
88
+ var StateRegions = class {
89
+ #whenTrue;
90
+ #whenFalse;
91
+ #taken = /* @__PURE__ */ new WeakSet();
92
+ constructor(options) {
93
+ this.#whenTrue = options.whenTrue;
94
+ this.#whenFalse = options.whenFalse ?? null;
95
+ }
96
+ /** Shows the regions inside `host` that belong to `isTrue` and hides the others. */
97
+ reflect(host, isTrue) {
98
+ const shown = this.#inside(host, this.#whenTrue());
99
+ if (!this.#whenFalse) {
100
+ for (const region of shown) this.#write(region, !isTrue);
101
+ return;
102
+ }
103
+ const hiddenSide = this.#inside(host, this.#whenFalse());
104
+ if (shown.length === 0 || hiddenSide.length === 0) {
105
+ for (const region of shown) this.#give(region);
106
+ for (const region of hiddenSide) this.#give(region);
107
+ return;
108
+ }
109
+ for (const region of shown) this.#write(region, !isTrue);
110
+ for (const region of hiddenSide) this.#write(region, isTrue);
111
+ }
112
+ /** Writes `hidden` where it moves, noting which regions it takes out of view. */
113
+ #write(region, hidden) {
114
+ if (hidden) this.#taken.add(region);
115
+ else this.#taken.delete(region);
116
+ if (region.hidden !== hidden) region.hidden = hidden;
117
+ }
118
+ /** Returns a region this instance hid; one it never hid keeps what it carries. */
119
+ #give(region) {
120
+ if (this.#taken.delete(region)) region.hidden = false;
121
+ }
122
+ /** The declared regions that sit within `host`, which may be the host itself. */
123
+ #inside(host, regions) {
124
+ return regions.filter((region) => host.contains(region));
125
+ }
126
+ };
127
+
87
128
  // src/controllers/password_reveal_controller.ts
88
129
  var MAX_DELAY = 2 ** 31 - 1;
89
130
  var PasswordRevealController = class extends Controller {
90
- static targets = ["input", "toggle"];
131
+ static targets = ["input", "toggle", "onLabel", "offLabel"];
91
132
  static values = {
92
133
  autoHide: { type: Number, default: 0 }
93
134
  };
@@ -105,6 +146,11 @@ var PasswordRevealController = class extends Controller {
105
146
  * document does not answer this — unloading the controller leaves it there.
106
147
  */
107
148
  #connected = false;
149
+ /** Owns `hidden` on the label pair the toggle button carries. */
150
+ #labels = new StateRegions({
151
+ whenTrue: () => this.onLabelTargets,
152
+ whenFalse: () => this.offLabelTargets
153
+ });
108
154
  /** Masks the field before Turbo copies the page into its snapshot. */
109
155
  #beforeCache = new BeforeCacheReset(() => this.#rewindToMasked());
110
156
  connect() {
@@ -144,6 +190,14 @@ var PasswordRevealController = class extends Controller {
144
190
  toggleTargetConnected() {
145
191
  this.#reflect(this.#isVisible);
146
192
  }
193
+ /** Describes the state to a revealed-side label that arrives after connect. */
194
+ onLabelTargetConnected() {
195
+ this.#reflect(this.#isVisible);
196
+ }
197
+ /** Describes the state to a masked-side label that arrives after connect. */
198
+ offLabelTargetConnected() {
199
+ this.#reflect(this.#isVisible);
200
+ }
147
201
  /** Toggles the input between masked and revealed. Bound via `data-action`. */
148
202
  toggle() {
149
203
  this.#setVisible(!this.#isVisible);
@@ -187,6 +241,8 @@ var PasswordRevealController = class extends Controller {
187
241
  * the opposite of what the declaration asked for. A value that is no delay at
188
242
  * all stays out of the positive range {@link PasswordRevealController.#arm}
189
243
  * requires, so it schedules nothing.
244
+ *
245
+ * @stimeoRuntimeOnly `autoHide` is the delay of the one re-masking timer it arms.
190
246
  */
191
247
  get #autoHideDelay() {
192
248
  return Math.min(this.autoHideValue, MAX_DELAY);
@@ -202,10 +258,11 @@ var PasswordRevealController = class extends Controller {
202
258
  this.inputTarget.type = "password";
203
259
  this.#reflect(false);
204
260
  }
205
- /** Reflects the visible state onto `aria-pressed` and `data-state`. */
261
+ /** Reflects the visible state onto `aria-pressed`, `data-state` and the labels. */
206
262
  #reflect(visible) {
207
263
  if (this.hasToggleTarget) {
208
264
  this.toggleTarget.setAttribute("aria-pressed", visible ? "true" : "false");
265
+ this.#labels.reflect(this.toggleTarget, visible);
209
266
  }
210
267
  this.element.setAttribute("data-state", visible ? "visible" : "hidden");
211
268
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/before_cache_reset.ts","../../src/utils/safe_timeout.ts","../../src/controllers/password_reveal_controller.ts"],"names":[],"mappings":"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","file":"password_reveal_controller.js","sourcesContent":["/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way.\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\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 * Largest delay a timer can hold: the platform stores it in a 32-bit signed\n * integer, and anything larger overflows to `1`, so a delay meant to be far in\n * the future fires almost immediately. A declared delay above this bound names\n * no delay at all, and a controller reading one falls back to its default.\n */\nexport const MAX_TIMER_DELAY_MS = 2_147_483_647;\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 { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/** The longest delay `setTimeout` can hold; past it a delay folds to zero. */\nconst MAX_DELAY = 2 ** 31 - 1;\n\n/**\n * Headless password show/hide (unmask) toggle behavior.\n *\n * Markup contract (identifier: `stimeo--password-reveal`):\n * <div data-controller=\"stimeo--password-reveal\">\n * <input type=\"password\" aria-label=\"Password\"\n * data-stimeo--password-reveal-target=\"input\">\n * <button type=\"button\" aria-pressed=\"false\" aria-label=\"Show password\"\n * data-stimeo--password-reveal-target=\"toggle\"\n * data-action=\"click->stimeo--password-reveal#toggle\"></button>\n * </div>\n *\n * No dedicated APG pattern; this follows the toggle **Button** practice. The\n * accessible name stays state-independent (\"Show password\") while the pressed\n * state is conveyed by `aria-pressed`.\n *\n * `toggle` dispatches `{ visible: boolean }`.\n *\n * @remarks\n * Behavior only — icon rendering is the consumer's, keyed off `aria-pressed` /\n * `data-state` (`hidden` / `visible`). Flipping `input.type` can drop focus and\n * the caret, so when (and only when) the input was the focused element its focus\n * and selection are restored afterward; when the toggle button holds focus\n * (keyboard use) focus is left on the button.\n *\n * The hooks are derived from the input's `type`, so they are re-derived whenever\n * a target enters or leaves — a field or button swapped in by a Turbo Stream\n * describes the state it actually has rather than the one the server rendered.\n *\n * An optional `autoHide` re-masks after a delay. The delay is armed on connect as\n * well, so a controller that reconnects onto an already revealed field still owes\n * the re-mask it promised; a value that is not a delay `setTimeout` can hold falls\n * back rather than inverting into an immediate one. Revealing is never carried\n * into a Turbo snapshot: the field is masked again before the page is cached, so\n * returning to it does not put a credential back on screen.\n */\nexport class PasswordRevealController extends Controller<HTMLElement> {\n static override targets = [\"input\", \"toggle\"];\n static override values = {\n autoHide: { type: Number, default: 0 },\n };\n static actions = [\"toggle\"] as const;\n static events = [\"toggle\"] as const;\n\n declare readonly inputTarget: HTMLInputElement;\n declare readonly toggleTarget: HTMLElement;\n declare readonly hasInputTarget: boolean;\n declare readonly hasToggleTarget: boolean;\n\n declare autoHideValue: number;\n\n /** Auto re-mask timer; torn down on disconnect. */\n #timers = new SafeTimeout();\n\n /**\n * Whether this controller is between `connect()` and `disconnect()`.\n *\n * Target callbacks outlive the controller: Stimulus stops the target observer\n * after `disconnect()`, so a field leaving after teardown still reaches\n * {@link PasswordRevealController.inputTargetDisconnected}. Arming from there\n * would put a timer back that nothing will clear. The element staying in the\n * document does not answer this — unloading the controller leaves it there.\n */\n #connected = false;\n\n /** Masks the field before Turbo copies the page into its snapshot. */\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindToMasked());\n\n override connect(): void {\n this.#connected = true;\n const visible = this.#isVisible;\n this.#reflect(visible);\n this.#beforeCache.activate();\n // Reconnecting onto an already revealed field inherits the promise the\n // declaration made: without re-arming, the re-mask would never arrive.\n this.#arm(visible);\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.#timers.clearAll();\n this.#beforeCache.deactivate();\n }\n\n /** Re-derives the hooks and the re-mask for a field swapped in after connect. */\n inputTargetConnected(): void {\n const visible = this.#connected && this.#isVisible;\n this.#reflect(visible);\n this.#arm(visible);\n }\n\n /**\n * Re-derives from whatever field is left rather than assuming none is. A swap\n * delivers this callback next to the arrival in either order, so a revealed\n * replacement that answered \"masked\" here would be described as hidden while\n * showing the password, and would carry no re-mask.\n *\n * Teardown is the one case with nothing to derive: target callbacks run after\n * `disconnect()`, so the field is still revealed and re-arming from it would\n * outlive the controller. A detached root is the signal to stand down.\n */\n inputTargetDisconnected(): void {\n const visible = this.#connected && this.element.isConnected && this.#isVisible;\n this.#reflect(visible);\n this.#arm(visible);\n }\n\n /** Re-derives the pressed state for a button swapped in after connect. */\n toggleTargetConnected(): void {\n this.#reflect(this.#isVisible);\n }\n\n /** Toggles the input between masked and revealed. Bound via `data-action`. */\n toggle(): void {\n this.#setVisible(!this.#isVisible);\n }\n\n /** Whether the input is currently revealed (`type=\"text\"`). */\n get #isVisible(): boolean {\n return this.hasInputTarget && this.inputTarget.type === \"text\";\n }\n\n /** Switches the masked/revealed state, preserving focus and caret. */\n #setVisible(visible: boolean): void {\n if (!this.hasInputTarget) return;\n const input = this.inputTarget;\n\n // Only the input's *own* focus is restored across the type change; if the\n // toggle button (keyboard) holds focus, it is left untouched.\n const restoreInputFocus = document.activeElement === input;\n const selectionStart = input.selectionStart;\n const selectionEnd = input.selectionEnd;\n\n input.type = visible ? \"text\" : \"password\";\n\n if (restoreInputFocus) {\n input.focus();\n // `selectionStart` / `selectionEnd` are `number | null` (null for input\n // types that don't expose a selection); only restore when both are present.\n if (selectionStart !== null && selectionEnd !== null) {\n try {\n input.setSelectionRange(selectionStart, selectionEnd);\n } catch {\n // Some input types reject selection access; focus alone is enough.\n }\n }\n }\n\n this.#reflect(visible);\n this.dispatch(\"toggle\", { detail: { visible } });\n this.#arm(visible);\n }\n\n /** Schedules the auto re-mask for a revealed field, replacing any pending one. */\n #arm(visible: boolean): void {\n this.#timers.clearAll();\n const delay = this.#autoHideDelay;\n if (visible && delay > 0) {\n this.#timers.set(() => this.#setVisible(false), delay);\n }\n }\n\n /**\n * The auto re-mask delay, held to what `setTimeout` can carry. Past that limit\n * a delay folds to zero, turning \"keep it showing\" into \"hide it at once\" —\n * the opposite of what the declaration asked for. A value that is no delay at\n * all stays out of the positive range {@link PasswordRevealController.#arm}\n * requires, so it schedules nothing.\n */\n get #autoHideDelay(): number {\n return Math.min(this.autoHideValue, MAX_DELAY);\n }\n\n /**\n * Returns the field to masked before Turbo copies the page. Silent and\n * focus-free: the page is about to be frozen, so there is no one to tell and\n * nowhere for focus to go.\n */\n #rewindToMasked(): void {\n this.#timers.clearAll();\n if (!this.hasInputTarget) return;\n this.inputTarget.type = \"password\";\n this.#reflect(false);\n }\n\n /** Reflects the visible state onto `aria-pressed` and `data-state`. */\n #reflect(visible: boolean): void {\n if (this.hasToggleTarget) {\n this.toggleTarget.setAttribute(\"aria-pressed\", visible ? \"true\" : \"false\");\n }\n this.element.setAttribute(\"data-state\", visible ? \"visible\" : \"hidden\");\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/before_cache_reset.ts","../../src/utils/safe_timeout.ts","../../src/utils/state_regions.ts","../../src/controllers/password_reveal_controller.ts"],"names":[],"mappings":"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","file":"password_reveal_controller.js","sourcesContent":["/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way.\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\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 * Largest delay a timer can hold: the platform stores it in a 32-bit signed\n * integer, and anything larger overflows to `1`, so a delay meant to be far in\n * the future fires almost immediately. A declared delay above this bound names\n * no delay at all, and a controller reading one falls back to its default.\n */\nexport const MAX_TIMER_DELAY_MS = 2_147_483_647;\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","/** What a consumer declares once about the regions one boolean state reveals. */\nexport interface StateRegionsOptions {\n /** Returns the declared regions shown while the state holds. */\n readonly whenTrue: () => readonly HTMLElement[];\n /**\n * Returns the declared regions shown while the state does not hold. Omitted by a\n * state whose other side has nothing of its own to show.\n */\n readonly whenFalse?: () => readonly HTMLElement[];\n}\n\n/**\n * Owns `hidden` on the regions a boolean state reveals.\n *\n * A region is a **declared** target, so the consumer decides whether one exists at\n * all; where it does, which side is shown is a pure function of the state and the\n * controller writes it unconditionally. An authored `hidden` is therefore not read\n * back — the first reflection after a connection settles it, and a restored DOM\n * cannot leave a region contradicting the state it belongs to.\n *\n * The regions arrive as the caller's own target arrays, which Stimulus has already\n * scoped to this instance: a nested controller registered under the same identifier\n * keeps its own regions. `host` narrows that set further, to the regions inside one\n * trigger or item, so a widget with several of them reflects each independently.\n *\n * **Two sides are a pair.** Where the state has a `whenFalse` side and only one half\n * sits inside `host`, the half that is there is left in view: hiding it would take\n * the only label with it, and the static check names the missing one instead. What\n * the author wrote on a lone half is theirs and stays — only a half this instance\n * took out of view is given back, which is what a pair losing one side at runtime\n * leaves behind.\n *\n * `hidden` moves only when it changes, so a consumer watching its own subtree for\n * attribute mutations sees one record per transition rather than one per reflection.\n *\n * @example\n * ```ts\n * readonly #labels = new StateRegions({\n * whenTrue: () => this.expandedLabelTargets,\n * whenFalse: () => this.collapsedLabelTargets,\n * });\n *\n * #reflect(): void {\n * this.#labels.reflect(this.triggerTarget, this.#expanded);\n * }\n * ```\n */\nexport class StateRegions {\n readonly #whenTrue: () => readonly HTMLElement[];\n readonly #whenFalse: (() => readonly HTMLElement[]) | null;\n readonly #taken = new WeakSet<HTMLElement>();\n\n constructor(options: StateRegionsOptions) {\n this.#whenTrue = options.whenTrue;\n this.#whenFalse = options.whenFalse ?? null;\n }\n\n /** Shows the regions inside `host` that belong to `isTrue` and hides the others. */\n reflect(host: Element, isTrue: boolean): void {\n const shown = this.#inside(host, this.#whenTrue());\n if (!this.#whenFalse) {\n for (const region of shown) this.#write(region, !isTrue);\n return;\n }\n const hiddenSide = this.#inside(host, this.#whenFalse());\n if (shown.length === 0 || hiddenSide.length === 0) {\n for (const region of shown) this.#give(region);\n for (const region of hiddenSide) this.#give(region);\n return;\n }\n for (const region of shown) this.#write(region, !isTrue);\n for (const region of hiddenSide) this.#write(region, isTrue);\n }\n\n /** Writes `hidden` where it moves, noting which regions it takes out of view. */\n #write(region: HTMLElement, hidden: boolean): void {\n if (hidden) this.#taken.add(region);\n else this.#taken.delete(region);\n if (region.hidden !== hidden) region.hidden = hidden;\n }\n\n /** Returns a region this instance hid; one it never hid keeps what it carries. */\n #give(region: HTMLElement): void {\n if (this.#taken.delete(region)) region.hidden = false;\n }\n\n /** The declared regions that sit within `host`, which may be the host itself. */\n #inside(host: Element, regions: readonly HTMLElement[]): HTMLElement[] {\n return regions.filter((region) => host.contains(region));\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\nimport { StateRegions } from \"../utils/state_regions\";\n\n/** The longest delay `setTimeout` can hold; past it a delay folds to zero. */\nconst MAX_DELAY = 2 ** 31 - 1;\n\n/**\n * Headless password show/hide (unmask) toggle behavior.\n *\n * Markup contract (identifier: `stimeo--password-reveal`):\n * <div data-controller=\"stimeo--password-reveal\">\n * <input type=\"password\" aria-label=\"Password\"\n * data-stimeo--password-reveal-target=\"input\">\n * <button type=\"button\" aria-pressed=\"false\" aria-label=\"Show password\"\n * data-stimeo--password-reveal-target=\"toggle\"\n * data-action=\"click->stimeo--password-reveal#toggle\">\n * <span data-stimeo--password-reveal-target=\"offLabel\">Show</span>\n * <span data-stimeo--password-reveal-target=\"onLabel\" hidden>Hide</span>\n * </button>\n * </div>\n *\n * No dedicated APG pattern; this follows the toggle **Button** practice. The\n * accessible name stays state-independent (\"Show password\") while the pressed\n * state is conveyed by `aria-pressed`.\n *\n * The button may carry an optional label pair — `onLabel` while the field is\n * revealed, `offLabel` while it is masked. The name comes from `aria-label`, so the\n * pair is a visual affordance: which half shows follows the state, and where only\n * one of the two is declared inside the button the author's own visibility stands.\n *\n * `toggle` dispatches `{ visible: boolean }`.\n *\n * @remarks\n * Behavior only — icon rendering is the consumer's, keyed off `aria-pressed` /\n * `data-state` (`hidden` / `visible`). Flipping `input.type` can drop focus and\n * the caret, so when (and only when) the input was the focused element its focus\n * and selection are restored afterward; when the toggle button holds focus\n * (keyboard use) focus is left on the button.\n *\n * The hooks are derived from the input's `type`, so they are re-derived whenever\n * a target enters or leaves — a field or button swapped in by a Turbo Stream\n * describes the state it actually has rather than the one the server rendered.\n *\n * An optional `autoHide` re-masks after a delay. The delay is armed on connect as\n * well, so a controller that reconnects onto an already revealed field still owes\n * the re-mask it promised; a value that is not a delay `setTimeout` can hold falls\n * back rather than inverting into an immediate one. Revealing is never carried\n * into a Turbo snapshot: the field is masked again before the page is cached, so\n * returning to it does not put a credential back on screen.\n */\nexport class PasswordRevealController extends Controller<HTMLElement> {\n static override targets = [\"input\", \"toggle\", \"onLabel\", \"offLabel\"];\n static override values = {\n autoHide: { type: Number, default: 0 },\n };\n static actions = [\"toggle\"] as const;\n static events = [\"toggle\"] as const;\n\n declare readonly inputTarget: HTMLInputElement;\n declare readonly toggleTarget: HTMLElement;\n declare readonly onLabelTargets: HTMLElement[];\n declare readonly offLabelTargets: HTMLElement[];\n declare readonly hasInputTarget: boolean;\n declare readonly hasToggleTarget: boolean;\n\n declare autoHideValue: number;\n\n /** Auto re-mask timer; torn down on disconnect. */\n #timers = new SafeTimeout();\n\n /**\n * Whether this controller is between `connect()` and `disconnect()`.\n *\n * Target callbacks outlive the controller: Stimulus stops the target observer\n * after `disconnect()`, so a field leaving after teardown still reaches\n * {@link PasswordRevealController.inputTargetDisconnected}. Arming from there\n * would put a timer back that nothing will clear. The element staying in the\n * document does not answer this — unloading the controller leaves it there.\n */\n #connected = false;\n\n /** Owns `hidden` on the label pair the toggle button carries. */\n readonly #labels = new StateRegions({\n whenTrue: () => this.onLabelTargets,\n whenFalse: () => this.offLabelTargets,\n });\n\n /** Masks the field before Turbo copies the page into its snapshot. */\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindToMasked());\n\n override connect(): void {\n this.#connected = true;\n const visible = this.#isVisible;\n this.#reflect(visible);\n this.#beforeCache.activate();\n // Reconnecting onto an already revealed field inherits the promise the\n // declaration made: without re-arming, the re-mask would never arrive.\n this.#arm(visible);\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.#timers.clearAll();\n this.#beforeCache.deactivate();\n }\n\n /** Re-derives the hooks and the re-mask for a field swapped in after connect. */\n inputTargetConnected(): void {\n const visible = this.#connected && this.#isVisible;\n this.#reflect(visible);\n this.#arm(visible);\n }\n\n /**\n * Re-derives from whatever field is left rather than assuming none is. A swap\n * delivers this callback next to the arrival in either order, so a revealed\n * replacement that answered \"masked\" here would be described as hidden while\n * showing the password, and would carry no re-mask.\n *\n * Teardown is the one case with nothing to derive: target callbacks run after\n * `disconnect()`, so the field is still revealed and re-arming from it would\n * outlive the controller. A detached root is the signal to stand down.\n */\n inputTargetDisconnected(): void {\n const visible = this.#connected && this.element.isConnected && this.#isVisible;\n this.#reflect(visible);\n this.#arm(visible);\n }\n\n /** Re-derives the pressed state for a button swapped in after connect. */\n toggleTargetConnected(): void {\n this.#reflect(this.#isVisible);\n }\n\n /** Describes the state to a revealed-side label that arrives after connect. */\n onLabelTargetConnected(): void {\n this.#reflect(this.#isVisible);\n }\n\n /** Describes the state to a masked-side label that arrives after connect. */\n offLabelTargetConnected(): void {\n this.#reflect(this.#isVisible);\n }\n\n /** Toggles the input between masked and revealed. Bound via `data-action`. */\n toggle(): void {\n this.#setVisible(!this.#isVisible);\n }\n\n /** Whether the input is currently revealed (`type=\"text\"`). */\n get #isVisible(): boolean {\n return this.hasInputTarget && this.inputTarget.type === \"text\";\n }\n\n /** Switches the masked/revealed state, preserving focus and caret. */\n #setVisible(visible: boolean): void {\n if (!this.hasInputTarget) return;\n const input = this.inputTarget;\n\n // Only the input's *own* focus is restored across the type change; if the\n // toggle button (keyboard) holds focus, it is left untouched.\n const restoreInputFocus = document.activeElement === input;\n const selectionStart = input.selectionStart;\n const selectionEnd = input.selectionEnd;\n\n input.type = visible ? \"text\" : \"password\";\n\n if (restoreInputFocus) {\n input.focus();\n // `selectionStart` / `selectionEnd` are `number | null` (null for input\n // types that don't expose a selection); only restore when both are present.\n if (selectionStart !== null && selectionEnd !== null) {\n try {\n input.setSelectionRange(selectionStart, selectionEnd);\n } catch {\n // Some input types reject selection access; focus alone is enough.\n }\n }\n }\n\n this.#reflect(visible);\n this.dispatch(\"toggle\", { detail: { visible } });\n this.#arm(visible);\n }\n\n /** Schedules the auto re-mask for a revealed field, replacing any pending one. */\n #arm(visible: boolean): void {\n this.#timers.clearAll();\n const delay = this.#autoHideDelay;\n if (visible && delay > 0) {\n this.#timers.set(() => this.#setVisible(false), delay);\n }\n }\n\n /**\n * The auto re-mask delay, held to what `setTimeout` can carry. Past that limit\n * a delay folds to zero, turning \"keep it showing\" into \"hide it at once\" —\n * the opposite of what the declaration asked for. A value that is no delay at\n * all stays out of the positive range {@link PasswordRevealController.#arm}\n * requires, so it schedules nothing.\n *\n * @stimeoRuntimeOnly `autoHide` is the delay of the one re-masking timer it arms.\n */\n get #autoHideDelay(): number {\n return Math.min(this.autoHideValue, MAX_DELAY);\n }\n\n /**\n * Returns the field to masked before Turbo copies the page. Silent and\n * focus-free: the page is about to be frozen, so there is no one to tell and\n * nowhere for focus to go.\n */\n #rewindToMasked(): void {\n this.#timers.clearAll();\n if (!this.hasInputTarget) return;\n this.inputTarget.type = \"password\";\n this.#reflect(false);\n }\n\n /** Reflects the visible state onto `aria-pressed`, `data-state` and the labels. */\n #reflect(visible: boolean): void {\n if (this.hasToggleTarget) {\n this.toggleTarget.setAttribute(\"aria-pressed\", visible ? \"true\" : \"false\");\n this.#labels.reflect(this.toggleTarget, visible);\n }\n this.element.setAttribute(\"data-state\", visible ? \"visible\" : \"hidden\");\n }\n}\n"]}