stimeo-ui 0.16.0 → 0.18.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 (117) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +136 -0
  3. data/README.md +27 -11
  4. data/dist/cable/index.js +384 -449
  5. data/dist/controllers/accordion_controller.js +174 -55
  6. data/dist/controllers/alert_dialog_controller.js +903 -235
  7. data/dist/controllers/announcer_controller.js +238 -205
  8. data/dist/controllers/aspect_ratio_controller.js +0 -10
  9. data/dist/controllers/auto_submit_controller.js +414 -145
  10. data/dist/controllers/avatar_controller.js +185 -107
  11. data/dist/controllers/breadcrumb_controller.js +0 -130
  12. data/dist/controllers/bulk_select_controller.js +282 -58
  13. data/dist/controllers/calendar_controller.js +228 -260
  14. data/dist/controllers/carousel_controller.js +536 -436
  15. data/dist/controllers/character_counter_controller.js +493 -207
  16. data/dist/controllers/checkbox_controller.js +38 -74
  17. data/dist/controllers/clipboard_controller.js +261 -149
  18. data/dist/controllers/collapsible_controller.js +304 -120
  19. data/dist/controllers/color_picker_controller.js +178 -146
  20. data/dist/controllers/combobox_controller.js +522 -156
  21. data/dist/controllers/command_palette_controller.js +1156 -395
  22. data/dist/controllers/conditional_fields_controller.js +300 -171
  23. data/dist/controllers/confirm_controller.js +980 -262
  24. data/dist/controllers/context_menu_controller.js +346 -153
  25. data/dist/controllers/count_up_controller.js +73 -21
  26. data/dist/controllers/countdown_controller.js +281 -158
  27. data/dist/controllers/currency_input_controller.js +575 -225
  28. data/dist/controllers/data_grid_controller.js +248 -214
  29. data/dist/controllers/date_range_picker_controller.js +338 -235
  30. data/dist/controllers/dialog_controller.js +899 -237
  31. data/dist/controllers/direct_upload_controller.js +97 -167
  32. data/dist/controllers/dirty_form_controller.js +72 -69
  33. data/dist/controllers/dismissible_controller.js +196 -24
  34. data/dist/controllers/drawer_controller.js +974 -355
  35. data/dist/controllers/dropdown_controller.js +339 -96
  36. data/dist/controllers/editable_controller.js +315 -88
  37. data/dist/controllers/empty_state_controller.js +79 -58
  38. data/dist/controllers/file_dropzone_controller.js +322 -211
  39. data/dist/controllers/filter_controller.js +77 -42
  40. data/dist/controllers/flash_controller.js +238 -347
  41. data/dist/controllers/focus_controller.js +679 -240
  42. data/dist/controllers/form_field_controller.js +156 -132
  43. data/dist/controllers/form_validation_controller.js +31 -97
  44. data/dist/controllers/frame_loading_controller.js +275 -217
  45. data/dist/controllers/highlight_controller.js +107 -75
  46. data/dist/controllers/hover_card_controller.js +327 -130
  47. data/dist/controllers/idle_controller.js +271 -101
  48. data/dist/controllers/input_mask_controller.js +483 -97
  49. data/dist/controllers/intersection_controller.js +150 -106
  50. data/dist/controllers/lazy_frame_controller.js +24 -68
  51. data/dist/controllers/listbox_controller.js +523 -240
  52. data/dist/controllers/local_time_controller.js +102 -77
  53. data/dist/controllers/masonry_controller.js +80 -96
  54. data/dist/controllers/menu_controller.js +302 -188
  55. data/dist/controllers/menubar_controller.js +232 -345
  56. data/dist/controllers/meter_controller.js +417 -87
  57. data/dist/controllers/multi_select_controller.js +785 -393
  58. data/dist/controllers/navigation_menu_controller.js +190 -250
  59. data/dist/controllers/nested_form_controller.js +396 -192
  60. data/dist/controllers/network_status_controller.js +268 -62
  61. data/dist/controllers/number_input_controller.js +610 -303
  62. data/dist/controllers/optimistic_controller.js +45 -87
  63. data/dist/controllers/otp_controller.js +658 -255
  64. data/dist/controllers/overflow_indicator_controller.js +122 -113
  65. data/dist/controllers/overflow_menu_controller.js +470 -295
  66. data/dist/controllers/pagination_controller.js +332 -123
  67. data/dist/controllers/password_reveal_controller.js +392 -121
  68. data/dist/controllers/password_strength_controller.js +111 -158
  69. data/dist/controllers/persist_controller.js +165 -111
  70. data/dist/controllers/pointer_drag_controller.js +434 -188
  71. data/dist/controllers/popover_controller.js +269 -113
  72. data/dist/controllers/portal_controller.js +215 -76
  73. data/dist/controllers/preview_guard_controller.js +243 -116
  74. data/dist/controllers/progress_controller.js +391 -78
  75. data/dist/controllers/radio_group_controller.js +278 -211
  76. data/dist/controllers/range_slider_controller.js +220 -113
  77. data/dist/controllers/rating_controller.js +458 -211
  78. data/dist/controllers/read_more_controller.js +138 -63
  79. data/dist/controllers/reading_progress_controller.js +166 -137
  80. data/dist/controllers/relative_time_controller.js +211 -112
  81. data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +103 -38
  82. data/dist/controllers/resizable_controller.js +299 -150
  83. data/dist/controllers/roving_controller.js +49 -88
  84. data/dist/controllers/scroll_area_controller.js +490 -246
  85. data/dist/controllers/scroll_restore_controller.js +14 -55
  86. data/dist/controllers/scroll_visibility_controller.js +432 -149
  87. data/dist/controllers/scrollspy_controller.js +193 -248
  88. data/dist/controllers/separator_controller.js +285 -142
  89. data/dist/controllers/sidebar_controller.js +1069 -404
  90. data/dist/controllers/skeleton_controller.js +259 -101
  91. data/dist/controllers/slider_controller.js +187 -86
  92. data/dist/controllers/smart_sticky_header_controller.js +173 -66
  93. data/dist/controllers/sortable_controller.js +26 -125
  94. data/dist/controllers/spinner_controller.js +288 -173
  95. data/dist/controllers/step_indicator_controller.js +105 -46
  96. data/dist/controllers/stepper_controller.js +120 -73
  97. data/dist/controllers/stick_to_bottom_controller.js +275 -140
  98. data/dist/controllers/sticky_observer_controller.js +220 -49
  99. data/dist/controllers/submit_once_controller.js +376 -227
  100. data/dist/controllers/switch_controller.js +201 -84
  101. data/dist/controllers/tabs_controller.js +41 -30
  102. data/dist/controllers/tags_input_controller.js +293 -173
  103. data/dist/controllers/textarea_autosize_controller.js +108 -74
  104. data/dist/controllers/theme_controller.js +140 -116
  105. data/dist/controllers/time_picker_controller.js +331 -127
  106. data/dist/controllers/toast_controller.js +244 -324
  107. data/dist/controllers/toggle_group_controller.js +252 -191
  108. data/dist/controllers/toolbar_controller.js +219 -120
  109. data/dist/controllers/tooltip_controller.js +158 -124
  110. data/dist/controllers/transition_controller.js +123 -119
  111. data/dist/controllers/tree_view_controller.js +283 -369
  112. data/dist/index.js +8573 -10410
  113. data/dist/positioning/index.js +154 -47
  114. data/lib/generators/stimeo/install/install_generator.rb +7 -2
  115. data/lib/generators/stimeo/install/templates/stimeo.js +19 -6
  116. data/lib/stimeo/ui/version.rb +1 -1
  117. metadata +3 -3
@@ -1,15 +1,11 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/form_validation_controller.ts
4
-
5
- // src/utils/default_attribute.ts
6
3
  function setDefaultAttribute(element, name, value) {
7
4
  if (element.hasAttribute(name)) return false;
8
5
  element.setAttribute(name, value);
9
6
  return true;
10
7
  }
11
8
 
12
- // src/utils/focus_candidate.ts
13
9
  function inheritsFieldsetDisabled(control) {
14
10
  let fieldset = control.closest("fieldset[disabled]");
15
11
  while (fieldset) {
@@ -19,8 +15,19 @@ function inheritsFieldsetDisabled(control) {
19
15
  }
20
16
  return false;
21
17
  }
18
+ function unslotted(element) {
19
+ return element.assignedSlot === null && element.parentElement?.shadowRoot != null;
20
+ }
21
+ function flatTreeParent(element) {
22
+ if (unslotted(element)) return null;
23
+ const parent = element.assignedSlot ?? element.parentNode;
24
+ if (parent instanceof ShadowRoot) return parent.host;
25
+ return parent instanceof Element ? parent : null;
26
+ }
22
27
  function canTakeFocus(element) {
23
- if (element.closest("[hidden], [inert]")) return false;
28
+ for (let node = element; node; node = flatTreeParent(node)) {
29
+ if (unslotted(node) || node.matches("[hidden], [inert]")) return false;
30
+ }
24
31
  if (element instanceof HTMLInputElement && element.type === "hidden") return false;
25
32
  if (!("disabled" in element)) return true;
26
33
  if (element.disabled) return false;
@@ -84,7 +91,17 @@ function firstTabStop(root) {
84
91
  return null;
85
92
  }
86
93
 
87
- // src/controllers/form_validation_controller.ts
94
+ var ListenerSet = class {
95
+ #abort = new AbortController();
96
+ add(target, type, handler, options) {
97
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
98
+ }
99
+ dispose() {
100
+ this.#abort.abort();
101
+ this.#abort = new AbortController();
102
+ }
103
+ };
104
+
88
105
  var CONSTRAINT_MESSAGE_KEYS = [
89
106
  ["valueMissing", "value-missing"],
90
107
  ["typeMismatch", "type-mismatch"],
@@ -101,11 +118,9 @@ var DISALLOW_PART = "disallow";
101
118
  var DISALLOW_WHITESPACE_DEFAULT = "Please enter a value that is not only whitespace.";
102
119
  var FORM_FIELD_INVALID_ATTR = "data-stimeo--form-field-invalid";
103
120
  var FormValidationController = class _FormValidationController extends Controller {
104
- /** The generic message override, in the namespace this controller is registered under. */
105
121
  get #messageAttribute() {
106
122
  return `data-${this.identifier}-${MESSAGE_PART}`;
107
123
  }
108
- /** Marks the `novalidate` this controller applied, so an authored one is left alone. */
109
124
  get #novalidateMarker() {
110
125
  return `data-${this.identifier}-${_FormValidationController.#NOVALIDATE_PART}`;
111
126
  }
@@ -118,23 +133,12 @@ var FormValidationController = class _FormValidationController extends Controlle
118
133
  };
119
134
  static actions = ["validate"];
120
135
  static events = ["valid", "invalid"];
121
- /** Suffix of the marker recording that we added `novalidate`, so we only remove our own. */
122
136
  static #NOVALIDATE_PART = "novalidate";
123
- /** Object-backed groups already interacted with — the input revalidation gate. */
124
137
  #touchedGroups = /* @__PURE__ */ new WeakSet();
125
- /** String-backed fallback radio groups already interacted with. */
126
138
  #touchedRadioGroups = /* @__PURE__ */ new Set();
127
- /**
128
- * Last message this controller wrote through `setCustomValidity`, kept as a
129
- * two-layer ownership ledger: a value is cleared only while the live message
130
- * still equals the recorded write. Iterable so disconnect can release every
131
- * surviving loan without touching a consumer's later custom error.
132
- */
133
139
  #ownedCustomErrors = /* @__PURE__ */ new Map();
134
- /** Last invalid message routed to each field, suppressing duplicate reports. */
135
140
  #reportedErrors = /* @__PURE__ */ new WeakMap();
136
- /** Exact document that owns the delegated listeners for this connection. */
137
- #listenerDocument = null;
141
+ #listeners = new ListenerSet();
138
142
  #onSubmit = (event) => {
139
143
  if (event.target !== this.element) return;
140
144
  const invalid = this.#validateAll();
@@ -172,50 +176,30 @@ var FormValidationController = class _FormValidationController extends Controlle
172
176
  this.#markTouched(group.key);
173
177
  this.#applyGroup(group);
174
178
  };
175
- /** Suppresses native bubbles and binds the submit / blur / input listeners. */
176
179
  connect() {
177
180
  if (setDefaultAttribute(this.element, "novalidate", "")) {
178
181
  this.element.setAttribute(this.#novalidateMarker, "");
179
182
  }
180
- this.#listenerDocument = this.element.ownerDocument;
181
- this.#listenerDocument.addEventListener("submit", this.#onSubmit, true);
182
- this.#listenerDocument.addEventListener("focusout", this.#onFocusOut);
183
- this.#listenerDocument.addEventListener("input", this.#onInput);
184
- this.#listenerDocument.addEventListener("change", this.#onChange);
183
+ const { ownerDocument } = this.element;
184
+ this.#listeners.add(ownerDocument, "submit", this.#onSubmit, { capture: true });
185
+ this.#listeners.add(ownerDocument, "focusout", this.#onFocusOut);
186
+ this.#listeners.add(ownerDocument, "input", this.#onInput);
187
+ this.#listeners.add(ownerDocument, "change", this.#onChange);
185
188
  }
186
- /** Tears down listeners and restores `novalidate` if we added it. */
187
189
  disconnect() {
188
- this.#listenerDocument?.removeEventListener("submit", this.#onSubmit, true);
189
- this.#listenerDocument?.removeEventListener("focusout", this.#onFocusOut);
190
- this.#listenerDocument?.removeEventListener("input", this.#onInput);
191
- this.#listenerDocument?.removeEventListener("change", this.#onChange);
192
- this.#listenerDocument = null;
190
+ this.#listeners.dispose();
193
191
  for (const [control, message] of this.#ownedCustomErrors) {
194
192
  if (control.validationMessage === message) control.setCustomValidity("");
195
193
  }
196
194
  this.#ownedCustomErrors.clear();
197
- this.#touchedRadioGroups.clear();
198
195
  if (this.element.hasAttribute(this.#novalidateMarker)) {
199
196
  this.element.removeAttribute("novalidate");
200
197
  this.element.removeAttribute(this.#novalidateMarker);
201
198
  }
202
199
  }
203
- /**
204
- * Validates every control now, rendering or clearing each field's message, and
205
- * returns whether the whole form is valid. Marks every field/group touched so
206
- * a later input from any sibling re-validates it. Bound via `data-action`
207
- * (`#validate`) or callable directly (e.g. before a programmatic submit).
208
- */
209
200
  validate() {
210
201
  return this.#validateAll().length === 0;
211
202
  }
212
- /**
213
- * Validates every control and returns one invalid control per field. Controls
214
- * are grouped by field first (see {@link #keyFor}) so a field with several
215
- * controls — a radio group, or a mirror plus its visible control — reflects
216
- * *all* of them: a valid sibling must never clear an invalid one's message.
217
- * Each group's first invalid control supplies the message and the focus target.
218
- */
219
203
  #validateAll() {
220
204
  const invalid = [];
221
205
  for (const group of this.#snapshot().groups.values()) {
@@ -225,7 +209,6 @@ var FormValidationController = class _FormValidationController extends Controlle
225
209
  }
226
210
  return invalid;
227
211
  }
228
- /** Builds the current field table in one pass over form controls and DOM depth. */
229
212
  #snapshot() {
230
213
  const fields = this.#fieldOutlets();
231
214
  const groups = /* @__PURE__ */ new Map();
@@ -243,7 +226,6 @@ var FormValidationController = class _FormValidationController extends Controlle
243
226
  }
244
227
  return { groups, groupByControl };
245
228
  }
246
- /** Records that a whole field/group, rather than one sibling control, was visited. */
247
229
  #markTouched(key) {
248
230
  if (typeof key === "string") {
249
231
  this.#touchedRadioGroups.add(key);
@@ -254,13 +236,6 @@ var FormValidationController = class _FormValidationController extends Controlle
254
236
  #isTouched(key) {
255
237
  return typeof key === "string" ? this.#touchedRadioGroups.has(key) : this.#touchedGroups.has(key);
256
238
  }
257
- /**
258
- * Runs native constraint validation across a field's controls and routes the
259
- * result to its `stimeo--form-field` outlet: the first invalid control's
260
- * `validationMessage` is shown, an all-valid field is cleared. Returns the
261
- * first invalid control (for the invalid list / focus), or `null` when valid.
262
- * Routing goes through the outlet, so the ARIA wiring is never duplicated here.
263
- */
264
239
  #applyGroup(group) {
265
240
  for (const control of group.controls) this.#syncCustomValidity(control);
266
241
  const firstInvalid = group.controls.find((control) => !control.checkValidity()) ?? null;
@@ -277,14 +252,6 @@ var FormValidationController = class _FormValidationController extends Controlle
277
252
  }
278
253
  return firstInvalid;
279
254
  }
280
- /**
281
- * Resolves the message to show for an invalid control: a per-constraint
282
- * override (`data-stimeo--form-validation-message-<constraint>`) for the first failing
283
- * `ValidityState` flag, then a generic `data-stimeo--form-validation-message`
284
- * override, then the browser's native `validationMessage`. Authoring an override
285
- * gives controlled, localizable, theme-able wording with **no consumer JS** —
286
- * and sidesteps headless browsers that return an empty native message.
287
- */
288
255
  #messageFor(control) {
289
256
  if (control.validity.customError) return control.validationMessage;
290
257
  for (const [flag, key] of CONSTRAINT_MESSAGE_KEYS) {
@@ -294,17 +261,6 @@ var FormValidationController = class _FormValidationController extends Controlle
294
261
  }
295
262
  return control.validationMessage || this.#authoredMessage(control, this.#messageAttribute) || "";
296
263
  }
297
- /**
298
- * Applies (or clears) a declarative custom constraint via `setCustomValidity`,
299
- * for controls that opt in with `data-stimeo--form-validation-disallow`. The supported
300
- * rule is `"whitespace"` — a value that is non-empty but blank
301
- * after trimming (which slips past `required` / `minlength`); its message follows
302
- * the whitespace-specific → generic → default chain.
303
- *
304
- * Don't-clobber-authored-state: an unknown/absent rule is never written, an
305
- * existing consumer custom error wins, and a controller error is only cleared
306
- * while the live message still equals the recorded controller write.
307
- */
308
264
  #syncCustomValidity(control) {
309
265
  const recorded = this.#ownedCustomErrors.get(control);
310
266
  const ownsCurrent = recorded !== void 0 && control.validationMessage === recorded;
@@ -319,16 +275,10 @@ var FormValidationController = class _FormValidationController extends Controlle
319
275
  if (!ownsCurrent || recorded !== message) control.setCustomValidity(message);
320
276
  this.#ownedCustomErrors.set(control, message);
321
277
  }
322
- /** Returns a non-blank authored message, otherwise falls through to a fallback. */
323
278
  #authoredMessage(control, attribute) {
324
279
  const message = control.getAttribute(attribute);
325
280
  return message && message.trim().length > 0 ? message : null;
326
281
  }
327
- /**
328
- * A grouping key that collects controls belonging to the same field: the owning
329
- * `stimeo--form-field` when present, else a radio group's shared `name`, else
330
- * the control itself (always distinct).
331
- */
332
282
  #keyFor(control, field) {
333
283
  if (field) return field;
334
284
  if (control instanceof HTMLInputElement && control.type === "radio" && control.name) {
@@ -336,15 +286,6 @@ var FormValidationController = class _FormValidationController extends Controlle
336
286
  }
337
287
  return control;
338
288
  }
339
- /**
340
- * Where focus should land for an invalid control. A visible control is focused
341
- * directly — the case for native fields and radios. A validatable mirror
342
- * (the `hidden` attribute) cannot receive focus, so focus is delegated to the
343
- * visible widget: the owning field's `control` target when it is itself
344
- * focusable, else its first focusable descendant (e.g. a roving-tabindex
345
- * member). Resolved structurally — never by probing `focus()` — so behavior
346
- * is deterministic and CSS-independent.
347
- */
348
289
  #focusTargetFor(control) {
349
290
  if (canTakeFocus(control)) return control;
350
291
  const field = this.#fieldFor(control, this.#fieldOutlets());
@@ -353,7 +294,6 @@ var FormValidationController = class _FormValidationController extends Controlle
353
294
  if (isTabStop(root)) return root;
354
295
  return firstTabStop(root);
355
296
  }
356
- /** Pairs each live outlet element with its controller once per operation. */
357
297
  #fieldOutlets() {
358
298
  const fields = /* @__PURE__ */ new Map();
359
299
  const elements = this.stimeoFormFieldOutletElements;
@@ -365,7 +305,6 @@ var FormValidationController = class _FormValidationController extends Controlle
365
305
  }
366
306
  return fields;
367
307
  }
368
- /** The nearest configured field ancestor of `control`, if any. */
369
308
  #fieldFor(control, fields) {
370
309
  let ancestor = control;
371
310
  while (ancestor) {
@@ -375,7 +314,6 @@ var FormValidationController = class _FormValidationController extends Controlle
375
314
  }
376
315
  return void 0;
377
316
  }
378
- /** This form's native controls that participate in constraint validation. */
379
317
  get #controls() {
380
318
  const controls = [];
381
319
  for (const element of Array.from(this.element.elements)) {
@@ -383,18 +321,14 @@ var FormValidationController = class _FormValidationController extends Controlle
383
321
  }
384
322
  return controls;
385
323
  }
386
- /** Narrows an event target to a validatable control. */
387
324
  #controlFrom(target) {
388
325
  return target instanceof Element && this.#isValidatable(target) && target.form === this.element ? target : null;
389
326
  }
390
327
  #isValidatable(element) {
391
- return (element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement) && // `willValidate` already excludes disabled, read-only, input[type=hidden],
392
- // and button-type controls. The `hidden` attribute intentionally does not
393
- // bar a text mirror from constraint validation.
328
+ return (element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement) &&
394
329
  element.willValidate;
395
330
  }
396
331
  };
397
332
 
398
333
  export { FormValidationController };
399
- //# sourceMappingURL=form_validation_controller.js.map
400
334
  //# sourceMappingURL=form_validation_controller.js.map