stimeo-ui 0.5.0 → 0.7.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 (230) hide show
  1. package/CHANGELOG.md +178 -0
  2. package/dist/controllers/alert_dialog_controller.d.ts +2 -0
  3. package/dist/controllers/alert_dialog_controller.js +75 -4
  4. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  5. package/dist/controllers/aspect_ratio_controller.d.ts +1 -3
  6. package/dist/controllers/aspect_ratio_controller.js +19 -11
  7. package/dist/controllers/aspect_ratio_controller.js.map +1 -1
  8. package/dist/controllers/auto_submit_controller.d.ts +2 -0
  9. package/dist/controllers/auto_submit_controller.js.map +1 -1
  10. package/dist/controllers/avatar_controller.d.ts +36 -15
  11. package/dist/controllers/avatar_controller.js +237 -40
  12. package/dist/controllers/avatar_controller.js.map +1 -1
  13. package/dist/controllers/breadcrumb_controller.d.ts +2 -0
  14. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  15. package/dist/controllers/bulk_select_controller.d.ts +2 -0
  16. package/dist/controllers/bulk_select_controller.js.map +1 -1
  17. package/dist/controllers/calendar_controller.d.ts +2 -0
  18. package/dist/controllers/calendar_controller.js.map +1 -1
  19. package/dist/controllers/carousel_controller.d.ts +12 -3
  20. package/dist/controllers/carousel_controller.js +85 -9
  21. package/dist/controllers/carousel_controller.js.map +1 -1
  22. package/dist/controllers/character_counter_controller.d.ts +52 -20
  23. package/dist/controllers/character_counter_controller.js +338 -63
  24. package/dist/controllers/character_counter_controller.js.map +1 -1
  25. package/dist/controllers/checkbox_controller.d.ts +34 -5
  26. package/dist/controllers/checkbox_controller.js +136 -25
  27. package/dist/controllers/checkbox_controller.js.map +1 -1
  28. package/dist/controllers/color_picker_controller.d.ts +10 -1
  29. package/dist/controllers/color_picker_controller.js +35 -9
  30. package/dist/controllers/color_picker_controller.js.map +1 -1
  31. package/dist/controllers/combobox_controller.d.ts +2 -0
  32. package/dist/controllers/combobox_controller.js.map +1 -1
  33. package/dist/controllers/command_palette_controller.d.ts +2 -0
  34. package/dist/controllers/command_palette_controller.js +75 -4
  35. package/dist/controllers/command_palette_controller.js.map +1 -1
  36. package/dist/controllers/conditional_fields_controller.d.ts +42 -14
  37. package/dist/controllers/conditional_fields_controller.js +345 -51
  38. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  39. package/dist/controllers/confirm_controller.d.ts +2 -0
  40. package/dist/controllers/confirm_controller.js +75 -4
  41. package/dist/controllers/confirm_controller.js.map +1 -1
  42. package/dist/controllers/count_up_controller.d.ts +2 -0
  43. package/dist/controllers/count_up_controller.js.map +1 -1
  44. package/dist/controllers/countdown_controller.d.ts +4 -0
  45. package/dist/controllers/countdown_controller.js.map +1 -1
  46. package/dist/controllers/currency_input_controller.d.ts +2 -0
  47. package/dist/controllers/currency_input_controller.js.map +1 -1
  48. package/dist/controllers/data_grid_controller.d.ts +3 -0
  49. package/dist/controllers/data_grid_controller.js.map +1 -1
  50. package/dist/controllers/date_range_picker_controller.d.ts +23 -4
  51. package/dist/controllers/date_range_picker_controller.js +157 -30
  52. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  53. package/dist/controllers/dialog_controller.js +75 -4
  54. package/dist/controllers/dialog_controller.js.map +1 -1
  55. package/dist/controllers/direct_upload_controller.d.ts +56 -24
  56. package/dist/controllers/direct_upload_controller.js +201 -45
  57. package/dist/controllers/direct_upload_controller.js.map +1 -1
  58. package/dist/controllers/dirty_form_controller.d.ts +14 -6
  59. package/dist/controllers/dirty_form_controller.js +192 -29
  60. package/dist/controllers/dirty_form_controller.js.map +1 -1
  61. package/dist/controllers/dismissible_controller.d.ts +2 -0
  62. package/dist/controllers/dismissible_controller.js +83 -18
  63. package/dist/controllers/dismissible_controller.js.map +1 -1
  64. package/dist/controllers/drawer_controller.js +75 -4
  65. package/dist/controllers/drawer_controller.js.map +1 -1
  66. package/dist/controllers/empty_state_controller.d.ts +2 -0
  67. package/dist/controllers/empty_state_controller.js.map +1 -1
  68. package/dist/controllers/file_dropzone_controller.d.ts +9 -1
  69. package/dist/controllers/file_dropzone_controller.js +26 -3
  70. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  71. package/dist/controllers/filter_controller.d.ts +2 -0
  72. package/dist/controllers/filter_controller.js.map +1 -1
  73. package/dist/controllers/flash_controller.d.ts +4 -0
  74. package/dist/controllers/flash_controller.js.map +1 -1
  75. package/dist/controllers/focus_controller.d.ts +4 -3
  76. package/dist/controllers/focus_controller.js +75 -4
  77. package/dist/controllers/focus_controller.js.map +1 -1
  78. package/dist/controllers/form_field_controller.d.ts +50 -10
  79. package/dist/controllers/form_field_controller.js +280 -62
  80. package/dist/controllers/form_field_controller.js.map +1 -1
  81. package/dist/controllers/form_validation_controller.d.ts +10 -8
  82. package/dist/controllers/form_validation_controller.js +208 -83
  83. package/dist/controllers/form_validation_controller.js.map +1 -1
  84. package/dist/controllers/frame_loading_controller.d.ts +2 -0
  85. package/dist/controllers/frame_loading_controller.js.map +1 -1
  86. package/dist/controllers/highlight_controller.d.ts +2 -0
  87. package/dist/controllers/highlight_controller.js.map +1 -1
  88. package/dist/controllers/hover_card_controller.d.ts +2 -2
  89. package/dist/controllers/hover_card_controller.js.map +1 -1
  90. package/dist/controllers/idle_controller.d.ts +5 -3
  91. package/dist/controllers/idle_controller.js +27 -5
  92. package/dist/controllers/idle_controller.js.map +1 -1
  93. package/dist/controllers/input_mask_controller.d.ts +2 -0
  94. package/dist/controllers/input_mask_controller.js.map +1 -1
  95. package/dist/controllers/lazy_frame_controller.d.ts +2 -0
  96. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  97. package/dist/controllers/listbox_controller.d.ts +2 -0
  98. package/dist/controllers/listbox_controller.js.map +1 -1
  99. package/dist/controllers/local_time_controller.d.ts +2 -0
  100. package/dist/controllers/local_time_controller.js.map +1 -1
  101. package/dist/controllers/masonry_controller.d.ts +2 -0
  102. package/dist/controllers/masonry_controller.js.map +1 -1
  103. package/dist/controllers/menubar_controller.js +5 -3
  104. package/dist/controllers/menubar_controller.js.map +1 -1
  105. package/dist/controllers/meter_controller.d.ts +2 -0
  106. package/dist/controllers/meter_controller.js.map +1 -1
  107. package/dist/controllers/multi_select_controller.d.ts +48 -12
  108. package/dist/controllers/multi_select_controller.js +460 -151
  109. package/dist/controllers/multi_select_controller.js.map +1 -1
  110. package/dist/controllers/nested_form_controller.d.ts +2 -0
  111. package/dist/controllers/nested_form_controller.js.map +1 -1
  112. package/dist/controllers/network_status_controller.d.ts +2 -0
  113. package/dist/controllers/network_status_controller.js.map +1 -1
  114. package/dist/controllers/number_input_controller.d.ts +26 -6
  115. package/dist/controllers/number_input_controller.js +317 -51
  116. package/dist/controllers/number_input_controller.js.map +1 -1
  117. package/dist/controllers/otp_controller.d.ts +3 -0
  118. package/dist/controllers/otp_controller.js.map +1 -1
  119. package/dist/controllers/overflow_indicator_controller.d.ts +2 -0
  120. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  121. package/dist/controllers/overflow_menu_controller.d.ts +2 -0
  122. package/dist/controllers/overflow_menu_controller.js +6 -1
  123. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  124. package/dist/controllers/pagination_controller.d.ts +2 -0
  125. package/dist/controllers/pagination_controller.js +35 -1
  126. package/dist/controllers/pagination_controller.js.map +1 -1
  127. package/dist/controllers/password_reveal_controller.d.ts +2 -0
  128. package/dist/controllers/password_reveal_controller.js.map +1 -1
  129. package/dist/controllers/password_strength_controller.d.ts +5 -3
  130. package/dist/controllers/password_strength_controller.js +20 -2
  131. package/dist/controllers/password_strength_controller.js.map +1 -1
  132. package/dist/controllers/persist_controller.d.ts +33 -19
  133. package/dist/controllers/persist_controller.js +449 -120
  134. package/dist/controllers/persist_controller.js.map +1 -1
  135. package/dist/controllers/pointer_drag_controller.d.ts +3 -0
  136. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  137. package/dist/controllers/popover_controller.d.ts +2 -2
  138. package/dist/controllers/popover_controller.js +77 -4
  139. package/dist/controllers/popover_controller.js.map +1 -1
  140. package/dist/controllers/portal_controller.d.ts +3 -2
  141. package/dist/controllers/portal_controller.js.map +1 -1
  142. package/dist/controllers/preview_guard_controller.d.ts +2 -0
  143. package/dist/controllers/preview_guard_controller.js.map +1 -1
  144. package/dist/controllers/progress_controller.d.ts +3 -0
  145. package/dist/controllers/progress_controller.js.map +1 -1
  146. package/dist/controllers/radio_group_controller.d.ts +44 -15
  147. package/dist/controllers/radio_group_controller.js +540 -56
  148. package/dist/controllers/radio_group_controller.js.map +1 -1
  149. package/dist/controllers/rating_controller.d.ts +38 -31
  150. package/dist/controllers/rating_controller.js +276 -89
  151. package/dist/controllers/rating_controller.js.map +1 -1
  152. package/dist/controllers/reading_progress_controller.d.ts +2 -0
  153. package/dist/controllers/reading_progress_controller.js.map +1 -1
  154. package/dist/controllers/resizable_controller.d.ts +2 -0
  155. package/dist/controllers/resizable_controller.js +33 -0
  156. package/dist/controllers/resizable_controller.js.map +1 -1
  157. package/dist/controllers/roving_controller.d.ts +6 -0
  158. package/dist/controllers/roving_controller.js +60 -5
  159. package/dist/controllers/roving_controller.js.map +1 -1
  160. package/dist/controllers/scroll_area_controller.d.ts +25 -11
  161. package/dist/controllers/scroll_area_controller.js +557 -125
  162. package/dist/controllers/scroll_area_controller.js.map +1 -1
  163. package/dist/controllers/scroll_visibility_controller.d.ts +2 -0
  164. package/dist/controllers/scroll_visibility_controller.js +33 -0
  165. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  166. package/dist/controllers/scrollspy_controller.d.ts +2 -0
  167. package/dist/controllers/scrollspy_controller.js.map +1 -1
  168. package/dist/controllers/separator_controller.d.ts +54 -11
  169. package/dist/controllers/separator_controller.js +354 -38
  170. package/dist/controllers/separator_controller.js.map +1 -1
  171. package/dist/controllers/sidebar_controller.js +83 -10
  172. package/dist/controllers/sidebar_controller.js.map +1 -1
  173. package/dist/controllers/skeleton_controller.d.ts +2 -0
  174. package/dist/controllers/skeleton_controller.js.map +1 -1
  175. package/dist/controllers/slider_controller.d.ts +2 -0
  176. package/dist/controllers/slider_controller.js.map +1 -1
  177. package/dist/controllers/smart_sticky_header_controller.d.ts +2 -0
  178. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  179. package/dist/controllers/spinner_controller.d.ts +2 -0
  180. package/dist/controllers/spinner_controller.js.map +1 -1
  181. package/dist/controllers/step_indicator_controller.d.ts +2 -0
  182. package/dist/controllers/step_indicator_controller.js.map +1 -1
  183. package/dist/controllers/stepper_controller.d.ts +2 -0
  184. package/dist/controllers/stepper_controller.js.map +1 -1
  185. package/dist/controllers/stick_to_bottom_controller.d.ts +2 -0
  186. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  187. package/dist/controllers/sticky_observer_controller.d.ts +2 -0
  188. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  189. package/dist/controllers/submit_once_controller.d.ts +94 -38
  190. package/dist/controllers/submit_once_controller.js +399 -121
  191. package/dist/controllers/submit_once_controller.js.map +1 -1
  192. package/dist/controllers/switch_controller.d.ts +2 -0
  193. package/dist/controllers/switch_controller.js.map +1 -1
  194. package/dist/controllers/tags_input_controller.d.ts +43 -11
  195. package/dist/controllers/tags_input_controller.js +356 -120
  196. package/dist/controllers/tags_input_controller.js.map +1 -1
  197. package/dist/controllers/textarea_autosize_controller.d.ts +2 -0
  198. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  199. package/dist/controllers/theme_controller.d.ts +2 -0
  200. package/dist/controllers/theme_controller.js +8 -6
  201. package/dist/controllers/theme_controller.js.map +1 -1
  202. package/dist/controllers/time_picker_controller.d.ts +44 -13
  203. package/dist/controllers/time_picker_controller.js +296 -107
  204. package/dist/controllers/time_picker_controller.js.map +1 -1
  205. package/dist/controllers/toast_controller.d.ts +4 -0
  206. package/dist/controllers/toast_controller.js.map +1 -1
  207. package/dist/controllers/toggle_group_controller.d.ts +41 -13
  208. package/dist/controllers/toggle_group_controller.js +378 -55
  209. package/dist/controllers/toggle_group_controller.js.map +1 -1
  210. package/dist/controllers/toolbar_controller.js +5 -3
  211. package/dist/controllers/toolbar_controller.js.map +1 -1
  212. package/dist/controllers/tooltip_controller.d.ts +2 -2
  213. package/dist/controllers/tooltip_controller.js.map +1 -1
  214. package/dist/controllers/transition_controller.d.ts +4 -3
  215. package/dist/controllers/transition_controller.js.map +1 -1
  216. package/dist/controllers/tree_view_controller.d.ts +2 -0
  217. package/dist/controllers/tree_view_controller.js +7 -4
  218. package/dist/controllers/tree_view_controller.js.map +1 -1
  219. package/dist/index.js +4963 -1581
  220. package/dist/index.js.map +1 -1
  221. package/dist/inspector/cli.d.ts +123 -6
  222. package/dist/inspector/cli.js +212 -18
  223. package/dist/inspector/cli.js.map +1 -1
  224. package/dist/inspector/cli_bin.js +274 -50
  225. package/dist/inspector/cli_bin.js.map +1 -1
  226. package/dist/inspector/examples.json +15 -15
  227. package/dist/inspector/manifest.json +602 -80
  228. package/dist/positioning/index.d.ts +4 -2
  229. package/dist/positioning/index.js.map +1 -1
  230. package/package.json +1 -1
@@ -13,6 +13,76 @@ function isReservedArrowChord(event, allow = []) {
13
13
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
14
14
  }
15
15
 
16
+ // src/utils/attribute_lease.ts
17
+ var AttributeLease = class {
18
+ #attribute;
19
+ #records = /* @__PURE__ */ new Map();
20
+ /** @param attribute - The attribute whose temporary values this lease owns. */
21
+ constructor(attribute) {
22
+ this.#attribute = attribute;
23
+ }
24
+ /** Writes or removes the leased attribute while preserving its authored value. */
25
+ write(element, value) {
26
+ const existing = this.#records.get(element);
27
+ if (existing) {
28
+ existing.written = value;
29
+ } else {
30
+ this.#records.set(element, {
31
+ original: element.getAttribute(this.#attribute),
32
+ written: value
33
+ });
34
+ }
35
+ this.#reflect(element, value);
36
+ }
37
+ /** Returns one lease without overwriting a value subsequently authored by a consumer. */
38
+ return(element) {
39
+ const record = this.#records.get(element);
40
+ if (!record) return;
41
+ this.#records.delete(element);
42
+ const stillOwned = element.getAttribute(this.#attribute) === record.written;
43
+ if (stillOwned) this.#reflect(element, record.original);
44
+ }
45
+ /** Reflects only a real value transition, avoiding self-triggered mutation work. */
46
+ #reflect(element, value) {
47
+ if (element.getAttribute(this.#attribute) === value) return;
48
+ if (value === null) element.removeAttribute(this.#attribute);
49
+ else element.setAttribute(this.#attribute, value);
50
+ }
51
+ /** Returns every outstanding lease using the same ownership check as {@link return}. */
52
+ returnAll() {
53
+ for (const element of Array.from(this.#records.keys())) this.return(element);
54
+ }
55
+ };
56
+
57
+ // src/utils/before_cache_reset.ts
58
+ var BeforeCacheReset = class _BeforeCacheReset {
59
+ /** Every subscribed instance, iterated by the one shared document listener. */
60
+ static #subscribers = /* @__PURE__ */ new Set();
61
+ /** The shared listener; installed while at least one instance is subscribed. */
62
+ static #onBeforeCache = () => {
63
+ for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
64
+ };
65
+ #rewind;
66
+ /** @param rewind - the pass that returns this controller's state to its initial form. */
67
+ constructor(rewind) {
68
+ this.#rewind = rewind;
69
+ }
70
+ /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
71
+ activate() {
72
+ const first = _BeforeCacheReset.#subscribers.size === 0;
73
+ _BeforeCacheReset.#subscribers.add(this);
74
+ if (first) {
75
+ document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
76
+ }
77
+ }
78
+ /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
79
+ deactivate() {
80
+ _BeforeCacheReset.#subscribers.delete(this);
81
+ if (_BeforeCacheReset.#subscribers.size > 0) return;
82
+ document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
83
+ }
84
+ };
85
+
16
86
  // src/utils/microtask_coalescer.ts
17
87
  var MicrotaskCoalescer = class {
18
88
  #run;
@@ -68,10 +138,12 @@ var RovingTabindex = class {
68
138
  * "nothing is currently tabbable".
69
139
  *
70
140
  * @param index - Position of the item to make tabbable.
71
- * @param options - Pass `{ focus: true }` to also move DOM focus to that item.
141
+ * @param options - Pass `{ focus: true }` to also move DOM focus to that item,
142
+ * and `items` to reuse an event-scoped collection snapshot.
72
143
  */
73
- setActive(index, { focus = false } = {}) {
74
- const items = this.#getItems();
144
+ setActive(index, options = {}) {
145
+ const { focus = false } = options;
146
+ const items = options.items ?? this.#getItems();
75
147
  items.forEach((item, i) => {
76
148
  item.tabIndex = i === index ? 0 : -1;
77
149
  });
@@ -79,167 +151,282 @@ var RovingTabindex = class {
79
151
  }
80
152
  };
81
153
 
154
+ // src/utils/tabindex_loan.ts
155
+ var TabindexLoan = class {
156
+ #value;
157
+ #lent = /* @__PURE__ */ new Set();
158
+ /** Returns live loans before Turbo can copy them into its page snapshot. */
159
+ #beforeCache = new BeforeCacheReset(() => this.returnAll());
160
+ /**
161
+ * @param value - the `tabindex` to lend. `"-1"` (the default) is
162
+ * programmatically focusable but not a Tab stop; `"0"` is a real Tab stop,
163
+ * which a scroll region with no focusable content of its own needs.
164
+ */
165
+ constructor(value = "-1") {
166
+ this.#value = value;
167
+ }
168
+ /** Lends `element` the value; no-ops when it already carries a `tabindex`. */
169
+ lend(element) {
170
+ if (element.hasAttribute("tabindex")) return;
171
+ element.setAttribute("tabindex", this.#value);
172
+ this.#lent.add(element);
173
+ this.#beforeCache.activate();
174
+ }
175
+ /** Takes back every loan whose value is still the one that was lent. */
176
+ returnAll() {
177
+ for (const element of this.#lent) {
178
+ if (element.getAttribute("tabindex") === this.#value) element.removeAttribute("tabindex");
179
+ }
180
+ this.#lent.clear();
181
+ this.#beforeCache.deactivate();
182
+ }
183
+ };
184
+
82
185
  // src/controllers/rating_controller.ts
83
186
  var RatingController = class extends Controller {
84
187
  static targets = ["symbol", "field"];
85
188
  static values = {
86
189
  value: { type: Number, default: 0 },
87
- max: { type: Number, default: 5 },
88
190
  clearable: { type: Boolean, default: true },
89
191
  readonly: { type: Boolean, default: false }
90
192
  };
91
193
  static actions = ["endPreview", "onKeydown", "preview", "select"];
92
- static events = ["change"];
194
+ static events = ["change", "reconcile"];
93
195
  #roving = new RovingTabindex(() => this.symbolTargets);
94
- /** Reflects the initial value, or switches to the non-interactive readonly view. */
95
- /**
96
- * Collapses a morph that swaps render inputs into one repaint, and refuses the
97
- * pass Stimulus delivers before `connect()`.
98
- */
99
- #repaint = new MicrotaskCoalescer(() => {
100
- if (this.readonlyValue) {
101
- this.#applyReadonly();
102
- return;
103
- }
104
- this.#apply(this.#clamp(this.valueValue), { focus: false });
105
- });
196
+ #rootRole = new AttributeLease("role");
197
+ #symbolRole = new AttributeLease("role");
198
+ #symbolAriaHidden = new AttributeLease("aria-hidden");
199
+ #rootTabindex = new TabindexLoan();
200
+ #repaint = new MicrotaskCoalescer(() => this.#reconcileScale());
201
+ #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
202
+ #connected = false;
203
+ #rescuedFocus = false;
204
+ /** Reflects declarative state without announcing an initial user change. */
106
205
  connect() {
107
206
  this.#repaint.activate();
108
- if (this.readonlyValue) {
109
- this.#applyReadonly();
110
- return;
111
- }
112
- this.#apply(this.#clamp(this.valueValue), { focus: false });
207
+ this.#beforeCache.activate();
208
+ this.#apply(this.#normalize(this.valueValue), { focus: false });
209
+ this.#connected = true;
113
210
  }
114
- /** Closes the window in which a queued repaint may still run. */
211
+ /** Drops a queued reconciliation and hands every borrowed attribute back. */
115
212
  disconnect() {
213
+ this.#connected = false;
116
214
  this.#repaint.cancel();
215
+ this.#beforeCache.deactivate();
216
+ this.#releaseReadonly();
217
+ }
218
+ /** Removes a runtime-added symbol's authored Tab stop before the batch repaint. */
219
+ symbolTargetConnected(symbol) {
220
+ if (this.#connected === false) return;
221
+ symbol.tabIndex = -1;
222
+ this.#repaint.schedule();
223
+ }
224
+ /** Releases readonly ownership and reconciles the remaining DOM-ordered scale. */
225
+ symbolTargetDisconnected(symbol) {
226
+ this.#symbolRole.return(symbol);
227
+ this.#symbolAriaHidden.return(symbol);
228
+ this.#repaint.schedule();
229
+ }
230
+ /** Reflects into a hidden field added or replaced after connection. */
231
+ fieldTargetConnected() {
232
+ this.#repaint.schedule();
233
+ }
234
+ /** Reconciles after a hidden field is removed or replaced. */
235
+ fieldTargetDisconnected() {
236
+ this.#repaint.schedule();
117
237
  }
118
- /** Repaints when application code (or a Turbo morph) changes `value` at runtime. */
238
+ /** Repaints when application code or a Turbo morph changes `value`. */
119
239
  valueValueChanged() {
120
240
  this.#repaint.schedule();
121
241
  }
122
- /** Selects (or clears) the clicked symbol. Bound via `data-action` (click). */
242
+ /** Repaints when application code changes whether value 0 is permitted. */
243
+ clearableValueChanged() {
244
+ this.#repaint.schedule();
245
+ }
246
+ /** Repaints when application code enters or leaves the readonly snapshot. */
247
+ readonlyValueChanged() {
248
+ this.#repaint.schedule();
249
+ }
250
+ /** Selects or clears the clicked symbol. Bound via `data-action` (click). */
123
251
  select(event) {
124
252
  if (this.readonlyValue) return;
125
- const value = this.#clamp(this.#symbolValue(event.currentTarget));
126
- if (this.clearableValue && value === this.valueValue) {
127
- this.#render(0, { focus: true });
128
- } else {
129
- this.#render(value, { focus: false });
130
- }
253
+ const ordinal = this.#symbolOrdinal(event.currentTarget);
254
+ if (ordinal === null) return;
255
+ const current = this.#normalize(this.valueValue);
256
+ this.#commit(this.clearableValue && ordinal === current ? 0 : ordinal, {
257
+ focus: ordinal === current
258
+ });
131
259
  }
132
- /** Previews a fill range on hover/focus. Bound via `data-action` (mouseenter/focus). */
260
+ /** Previews a fill range on hover or focus without committing it. */
133
261
  preview(event) {
134
262
  if (this.readonlyValue) return;
135
- this.#setFillRange(this.#clamp(this.#symbolValue(event.currentTarget)));
263
+ const ordinal = this.#symbolOrdinal(event.currentTarget);
264
+ if (ordinal !== null) this.#setFillRange(ordinal);
136
265
  }
137
- /** Restores the fill range to the selected value. Bound via `data-action` (mouseleave/blur). */
266
+ /** Restores the fill range after hover or focus leaves a symbol. */
138
267
  endPreview() {
139
268
  if (this.readonlyValue) return;
140
- this.#setFillRange(this.valueValue);
269
+ this.#setFillRange(this.#normalize(this.valueValue));
141
270
  }
142
- /** Arrow/Home/End/Space keyboard control, clamped (no wrap). */
271
+ /** Arrow/Home/End/Space/Delete keyboard control, clamped without wrapping. */
143
272
  onKeydown(event) {
144
- if (event.defaultPrevented) return;
145
- if (isReservedArrowChord(event)) return;
146
- if (this.readonlyValue) return;
273
+ if (event.defaultPrevented || isReservedArrowChord(event) || this.readonlyValue) return;
274
+ const current = this.#normalize(this.valueValue);
147
275
  let next = null;
148
276
  const rtl = isRtl(this.element);
149
277
  switch (event.key) {
150
278
  case "ArrowRight":
151
279
  case "ArrowUp":
152
- next = this.valueValue + (event.key === "ArrowRight" && rtl ? -1 : 1);
280
+ next = current + (event.key === "ArrowRight" && rtl ? -1 : 1);
153
281
  break;
154
282
  case "ArrowLeft":
155
283
  case "ArrowDown":
156
- next = this.valueValue - (event.key === "ArrowLeft" && rtl ? -1 : 1);
284
+ next = current - (event.key === "ArrowLeft" && rtl ? -1 : 1);
157
285
  break;
158
286
  case "Home":
159
287
  next = this.#minValue;
160
288
  break;
161
289
  case "End":
162
- next = this.maxValue;
290
+ next = this.symbolTargets.length;
163
291
  break;
164
292
  case " ":
165
293
  case "Enter":
166
- next = this.#symbolValue(event.currentTarget);
294
+ next = this.#symbolOrdinal(event.currentTarget);
295
+ break;
296
+ case "Delete":
297
+ case "Backspace":
298
+ if (!this.clearableValue) return;
299
+ next = 0;
167
300
  break;
168
301
  default:
169
302
  return;
170
303
  }
304
+ if (next === null) return;
171
305
  event.preventDefault();
172
- this.#render(this.#clamp(next), { focus: true });
306
+ this.#commit(next, { focus: true });
173
307
  }
174
308
  /**
175
- * Applies `value` (already clamped) everywhere, then dispatches `change`.
176
- * Use for user-driven changes; on connect call `#apply` directly so
177
- * initialization mirrors state without emitting an event.
309
+ * Repaints one settled target/Value mutation batch and reports only a value
310
+ * this controller had to normalize.
178
311
  *
179
312
  * @stimeoRenderRoot
180
313
  */
181
- #render(value, { focus }) {
314
+ #reconcileScale() {
315
+ const requested = this.valueValue;
316
+ const value = this.#normalize(requested);
317
+ this.#apply(value, { focus: false });
318
+ if (!Object.is(value, requested)) {
319
+ this.dispatch("reconcile", { detail: { value } });
320
+ }
321
+ }
322
+ /** Applies one user operation and emits only when its committed value changes. */
323
+ #commit(raw, { focus }) {
324
+ const previous = this.#normalize(this.valueValue);
325
+ const value = this.#normalize(raw);
182
326
  this.#apply(value, { focus });
183
- this.dispatch("change", { detail: { value } });
327
+ if (value !== previous) this.dispatch("change", { detail: { value } });
184
328
  }
185
- /**
186
- * Stores `value`, syncs `aria-checked` and the roving Tab stop, the hidden
187
- * field, and the fill range — without dispatching `change`. Idempotent and
188
- * safe on connect (and across Turbo morphing).
189
- */
329
+ /** Synchronizes value, ARIA, roving focus, form state, and the visual fill hook. */
190
330
  #apply(value, { focus }) {
191
- this.valueValue = value;
192
- this.symbolTargets.forEach((symbol) => {
193
- symbol.setAttribute(
194
- "aria-checked",
195
- value > 0 && this.#symbolValue(symbol) === value ? "true" : "false"
196
- );
331
+ if (!Object.is(this.valueValue, value)) this.valueValue = value;
332
+ this.symbolTargets.forEach((symbol, index) => {
333
+ symbol.setAttribute("aria-checked", value > 0 && index + 1 === value ? "true" : "false");
197
334
  });
198
- this.#roving.setActive(value > 0 ? value - 1 : 0, { focus });
335
+ if (this.readonlyValue) {
336
+ this.#applyReadonly();
337
+ } else {
338
+ const returning = this.#releaseReadonly();
339
+ this.#roving.setActive(value > 0 ? value - 1 : 0, { focus: focus || returning });
340
+ }
199
341
  if (this.hasFieldTarget) this.fieldTarget.value = String(value);
200
342
  this.#setFillRange(value);
201
343
  }
202
- /** Marks symbols up to `range` with `data-rating-hover` (the consumer's fill hook). */
344
+ /** Marks the first `range` symbols with the consumer-owned fill hook. */
203
345
  #setFillRange(range) {
204
- for (const symbol of this.symbolTargets) {
205
- if (this.#symbolValue(symbol) <= range && range > 0) {
206
- symbol.setAttribute("data-rating-hover", "");
207
- } else {
208
- symbol.removeAttribute("data-rating-hover");
209
- }
210
- }
346
+ this.symbolTargets.forEach((symbol, index) => {
347
+ symbol.toggleAttribute("data-rating-hover", range > 0 && index < range);
348
+ });
211
349
  }
212
- /** Turns the group into a non-interactive `role="img"` snapshot of the value. */
350
+ /**
351
+ * Temporarily turns the radiogroup into a non-interactive image snapshot.
352
+ *
353
+ * Each lease is returned before it is taken again, so a value the consumer
354
+ * wrote while readonly becomes the value the lease restores on release. The
355
+ * return is a no-op on an attribute still holding this controller's own
356
+ * write, which is the ordinary case.
357
+ */
213
358
  #applyReadonly() {
214
- const value = this.#clamp(this.valueValue);
215
- this.valueValue = value;
216
- this.element.setAttribute("role", "img");
359
+ this.#rescueFocus();
360
+ this.#rootRole.return(this.element);
361
+ this.#rootRole.write(this.element, "img");
217
362
  for (const symbol of this.symbolTargets) {
218
- symbol.removeAttribute("role");
219
- symbol.setAttribute("aria-hidden", "true");
220
- symbol.tabIndex = -1;
363
+ this.#symbolRole.return(symbol);
364
+ this.#symbolRole.write(symbol, null);
365
+ this.#symbolAriaHidden.return(symbol);
366
+ this.#symbolAriaHidden.write(symbol, "true");
221
367
  }
222
- if (this.hasFieldTarget) this.fieldTarget.value = String(value);
223
- this.#setFillRange(value);
368
+ this.#roving.setActive(-1);
224
369
  }
225
370
  /**
226
- * Clamps `value` to `[min, max]` (min is 0 when clearable, else 1). The upper
227
- * bound is also capped at the number of symbols so the roving Tab stop
228
- * (`value - 1`) always maps to a real symbol — even if the consumer's `max`
229
- * value and rendered symbol count disagree, the tabbable item is never lost.
371
+ * Hands every readonly borrowing back before Turbo clones the page.
372
+ *
373
+ * The snapshot is taken while the controller is still connected, so an
374
+ * element left as `role="img"` with hidden symbols is what a restored page
375
+ * connects against — and that markup would be read as the authored one,
376
+ * leaving no way back to the radiogroup. Rewinding first keeps the cached
377
+ * copy identical to what the consumer wrote.
230
378
  */
231
- #clamp(value) {
232
- const max = Math.min(this.maxValue, this.symbolTargets.length);
233
- return Math.min(max, Math.max(this.#minValue, value));
379
+ #rewindForCache() {
380
+ this.#releaseReadonly();
381
+ const value = this.#normalize(this.valueValue);
382
+ this.#roving.setActive(value > 0 ? value - 1 : 0);
383
+ }
384
+ /**
385
+ * Lands focus on the root before the symbols leave the accessibility tree.
386
+ *
387
+ * A symbol holding focus when readonly begins would keep it while losing its
388
+ * role and gaining `aria-hidden`, stranding the user on a node no longer in
389
+ * the tree. The root is the one element that survives the transition named:
390
+ * it carries the consumer's accessible name under `role="img"`.
391
+ */
392
+ #rescueFocus() {
393
+ const active = document.activeElement;
394
+ if (!(active instanceof HTMLElement) || active === this.element) return;
395
+ if (!this.element.contains(active)) return;
396
+ this.#rootTabindex.lend(this.element);
397
+ this.element.focus();
398
+ this.#rescuedFocus = true;
399
+ }
400
+ /**
401
+ * Restores authored roles and visibility after leaving readonly mode.
402
+ *
403
+ * @returns whether focus is standing on the root because {@link #rescueFocus}
404
+ * put it there, and therefore belongs back on the Tab stop.
405
+ */
406
+ #releaseReadonly() {
407
+ const returning = this.#rescuedFocus && document.activeElement === this.element;
408
+ this.#rescuedFocus = false;
409
+ this.#rootRole.return(this.element);
410
+ this.#symbolRole.returnAll();
411
+ this.#symbolAriaHidden.returnAll();
412
+ this.#rootTabindex.returnAll();
413
+ return returning;
414
+ }
415
+ /** Normalizes a raw value to an integer ordinal in the live DOM range. */
416
+ #normalize(raw) {
417
+ const maximum = this.symbolTargets.length;
418
+ const ordinal = Number.isFinite(raw) ? Math.round(raw) : this.#minValue;
419
+ return Math.min(maximum, Math.max(this.#minValue, ordinal));
234
420
  }
235
421
  /** Lowest selectable value: 0 when clearable, otherwise 1. */
236
422
  get #minValue() {
237
423
  return this.clearableValue ? 0 : 1;
238
424
  }
239
- /** A symbol's ordinal value (`data-rating-value`, defaulting to its position). */
240
- #symbolValue(symbol) {
241
- const raw = Number(symbol.getAttribute("data-rating-value"));
242
- return Number.isFinite(raw) && raw > 0 ? raw : this.symbolTargets.indexOf(symbol) + 1;
425
+ /** Returns a target's 1-based position, or null when the action host is invalid. */
426
+ #symbolOrdinal(target) {
427
+ const targets = this.symbolTargets;
428
+ const index = targets.indexOf(target);
429
+ return index < 0 ? null : index + 1;
243
430
  }
244
431
  };
245
432