stimeo-ui 0.14.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +212 -0
- data/README.md +120 -0
- data/dist/cable/index.js +123 -29
- data/dist/controllers/accordion_controller.js +98 -9
- data/dist/controllers/announcer_controller.js +96 -62
- data/dist/controllers/auto_submit_controller.js +83 -7
- data/dist/controllers/avatar_controller.js +1 -1
- data/dist/controllers/breadcrumb_controller.js +38 -11
- data/dist/controllers/bulk_select_controller.js +7 -6
- data/dist/controllers/calendar_controller.js +340 -123
- data/dist/controllers/carousel_controller.js +263 -38
- data/dist/controllers/character_counter_controller.js +40 -2
- data/dist/controllers/checkbox_controller.js +81 -12
- data/dist/controllers/clipboard_controller.js +63 -5
- data/dist/controllers/collapsible_controller.js +99 -14
- data/dist/controllers/color_picker_controller.js +80 -34
- data/dist/controllers/combobox_controller.js +106 -16
- data/dist/controllers/command_palette_controller.js +35 -3
- data/dist/controllers/conditional_fields_controller.js +85 -17
- data/dist/controllers/confirm_controller.js +3 -0
- data/dist/controllers/context_menu_controller.js +32 -12
- data/dist/controllers/countdown_controller.js +129 -26
- data/dist/controllers/currency_input_controller.js +221 -67
- data/dist/controllers/data_grid_controller.js +195 -29
- data/dist/controllers/date_range_picker_controller.js +151 -30
- data/dist/controllers/dialog_controller.js +35 -8
- data/dist/controllers/direct_upload_controller.js +22 -4
- data/dist/controllers/dirty_form_controller.js +46 -13
- data/dist/controllers/dismissible_controller.js +1 -0
- data/dist/controllers/drawer_controller.js +54 -19
- data/dist/controllers/dropdown_controller.js +36 -9
- data/dist/controllers/editable_controller.js +34 -0
- data/dist/controllers/file_dropzone_controller.js +144 -51
- data/dist/controllers/filter_controller.js +20 -6
- data/dist/controllers/flash_controller.js +432 -71
- data/dist/controllers/focus_controller.js +1 -0
- data/dist/controllers/form_field_controller.js +7 -5
- data/dist/controllers/form_validation_controller.js +19 -13
- data/dist/controllers/frame_loading_controller.js +45 -8
- data/dist/controllers/highlight_controller.js +82 -25
- data/dist/controllers/hover_card_controller.js +40 -14
- data/dist/controllers/idle_controller.js +90 -5
- data/dist/controllers/input_mask_controller.js +65 -9
- data/dist/controllers/intersection_controller.js +3 -0
- data/dist/controllers/lazy_frame_controller.js +11 -2
- data/dist/controllers/listbox_controller.js +203 -45
- data/dist/controllers/local_time_controller.js +10 -5
- data/dist/controllers/masonry_controller.js +31 -15
- data/dist/controllers/menu_controller.js +45 -16
- data/dist/controllers/menubar_controller.js +58 -24
- data/dist/controllers/meter_controller.js +9 -5
- data/dist/controllers/multi_select_controller.js +278 -104
- data/dist/controllers/navigation_menu_controller.js +48 -15
- data/dist/controllers/nested_form_controller.js +37 -8
- data/dist/controllers/network_status_controller.js +9 -1
- data/dist/controllers/number_input_controller.js +124 -21
- data/dist/controllers/optimistic_controller.js +42 -5
- data/dist/controllers/otp_controller.js +198 -55
- data/dist/controllers/overflow_indicator_controller.js +115 -21
- data/dist/controllers/overflow_menu_controller.js +141 -44
- data/dist/controllers/pagination_controller.js +74 -28
- data/dist/controllers/password_reveal_controller.js +59 -2
- data/dist/controllers/persist_controller.js +30 -8
- data/dist/controllers/pointer_drag_controller.js +131 -52
- data/dist/controllers/popover_controller.js +45 -11
- data/dist/controllers/portal_controller.js +6 -2
- data/dist/controllers/preview_guard_controller.js +16 -1
- data/dist/controllers/progress_controller.js +8 -4
- data/dist/controllers/radio_group_controller.js +42 -17
- data/dist/controllers/range_slider_controller.js +88 -42
- data/dist/controllers/rating_controller.js +39 -15
- data/dist/controllers/read_more_controller.js +100 -7
- data/dist/controllers/reading_progress_controller.js +65 -19
- data/dist/controllers/relative_time_controller.js +10 -5
- data/dist/controllers/resizable_controller.js +82 -22
- data/dist/controllers/scroll_area_controller.js +75 -27
- data/dist/controllers/scroll_restore_controller.js +37 -16
- data/dist/controllers/scroll_visibility_controller.js +49 -30
- data/dist/controllers/scrollspy_controller.js +71 -26
- data/dist/controllers/separator_controller.js +66 -37
- data/dist/controllers/sidebar_controller.js +77 -18
- data/dist/controllers/skeleton_controller.js +6 -1
- data/dist/controllers/slider_controller.js +82 -47
- data/dist/controllers/smart_sticky_header_controller.js +60 -26
- data/dist/controllers/sortable_controller.js +17 -2
- data/dist/controllers/spinner_controller.js +10 -2
- data/dist/controllers/step_indicator_controller.js +18 -17
- data/dist/controllers/stepper_controller.js +101 -19
- data/dist/controllers/stick_to_bottom_controller.js +104 -8
- data/dist/controllers/submit_once_controller.js +45 -9
- data/dist/controllers/switch_controller.js +101 -10
- data/dist/controllers/tabs_controller.js +21 -2
- data/dist/controllers/tags_input_controller.js +209 -59
- data/dist/controllers/textarea_autosize_controller.js +29 -3
- data/dist/controllers/theme_controller.js +64 -14
- data/dist/controllers/time_picker_controller.js +23 -8
- data/dist/controllers/toast_controller.js +451 -105
- data/dist/controllers/toggle_group_controller.js +159 -23
- data/dist/controllers/toolbar_controller.js +32 -0
- data/dist/controllers/tooltip_controller.js +39 -13
- data/dist/controllers/transition_controller.js +4 -0
- data/dist/controllers/tree_view_controller.js +169 -16
- data/dist/index.js +5002 -1911
- data/dist/positioning/index.js +2 -0
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
|
@@ -106,11 +106,19 @@ var BeforeCacheReset = class _BeforeCacheReset {
|
|
|
106
106
|
};
|
|
107
107
|
|
|
108
108
|
// src/utils/composition_tracker.ts
|
|
109
|
+
var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
110
|
+
"insertCompositionText",
|
|
111
|
+
"insertFromComposition",
|
|
112
|
+
"deleteCompositionText",
|
|
113
|
+
"deleteByComposition"
|
|
114
|
+
]);
|
|
109
115
|
var CompositionTracker = class {
|
|
110
116
|
#observedTargets = /* @__PURE__ */ new Set();
|
|
111
117
|
#activeTargets = /* @__PURE__ */ new Set();
|
|
112
118
|
#onStart;
|
|
113
119
|
#onEnd;
|
|
120
|
+
/** The field whose confirming `input` is still owed, while the window is open. */
|
|
121
|
+
#confirmedTarget = null;
|
|
114
122
|
constructor(options = {}) {
|
|
115
123
|
this.#onStart = options.onStart;
|
|
116
124
|
this.#onEnd = options.onEnd;
|
|
@@ -120,6 +128,7 @@ var CompositionTracker = class {
|
|
|
120
128
|
if (this.#observedTargets.has(target)) return;
|
|
121
129
|
target.addEventListener("compositionstart", this.#handleStart);
|
|
122
130
|
target.addEventListener("compositionend", this.#handleEnd);
|
|
131
|
+
target.addEventListener("keydown", this.#handleKeydown);
|
|
123
132
|
this.#observedTargets.add(target);
|
|
124
133
|
}
|
|
125
134
|
/** Stops tracking one target and clears any active composition it owned. */
|
|
@@ -127,29 +136,52 @@ var CompositionTracker = class {
|
|
|
127
136
|
if (!this.#observedTargets.delete(target)) return;
|
|
128
137
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
129
138
|
target.removeEventListener("compositionend", this.#handleEnd);
|
|
139
|
+
target.removeEventListener("keydown", this.#handleKeydown);
|
|
130
140
|
this.#activeTargets.delete(target);
|
|
141
|
+
if (this.#confirmedTarget === target) this.#confirmedTarget = null;
|
|
131
142
|
}
|
|
132
143
|
/** Releases every listener and clears state so reconnect starts cleanly. */
|
|
133
144
|
disconnect() {
|
|
134
145
|
for (const target of this.#observedTargets) {
|
|
135
146
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
136
147
|
target.removeEventListener("compositionend", this.#handleEnd);
|
|
148
|
+
target.removeEventListener("keydown", this.#handleKeydown);
|
|
137
149
|
}
|
|
138
150
|
this.#observedTargets.clear();
|
|
139
151
|
this.#activeTargets.clear();
|
|
152
|
+
this.#confirmedTarget = null;
|
|
140
153
|
}
|
|
141
154
|
/** True when lifecycle tracking or the current event reports composition. */
|
|
142
155
|
isComposing(event) {
|
|
143
156
|
return this.#activeTargets.size > 0 || event?.isComposing === true;
|
|
144
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Whether `event` is the `input` echoing the composition just confirmed.
|
|
160
|
+
*
|
|
161
|
+
* Asking closes the window either way, so one confirmation is folded at most
|
|
162
|
+
* once and a consumer asks once per `input`.
|
|
163
|
+
*/
|
|
164
|
+
consumesConfirmedInput(event) {
|
|
165
|
+
const confirmed = this.#confirmedTarget;
|
|
166
|
+
this.#confirmedTarget = null;
|
|
167
|
+
if (confirmed === null || confirmed !== event.target) return false;
|
|
168
|
+
const inputType = event.inputType;
|
|
169
|
+
return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);
|
|
170
|
+
}
|
|
145
171
|
#handleStart = (event) => {
|
|
172
|
+
this.#confirmedTarget = null;
|
|
146
173
|
if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
|
|
147
174
|
this.#onStart?.(event);
|
|
148
175
|
};
|
|
149
176
|
#handleEnd = (event) => {
|
|
150
177
|
if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
|
|
178
|
+
this.#confirmedTarget = event.target;
|
|
151
179
|
this.#onEnd?.(event);
|
|
152
180
|
};
|
|
181
|
+
/** A key on an observed field opens an edit of its own, so no echo is owed. */
|
|
182
|
+
#handleKeydown = () => {
|
|
183
|
+
this.#confirmedTarget = null;
|
|
184
|
+
};
|
|
153
185
|
};
|
|
154
186
|
|
|
155
187
|
// src/utils/declared_value.ts
|
|
@@ -171,6 +203,54 @@ function compileRegExp(source, anchor = "none") {
|
|
|
171
203
|
);
|
|
172
204
|
}
|
|
173
205
|
|
|
206
|
+
// src/utils/form_reset_watcher.ts
|
|
207
|
+
var FormResetWatcher = class {
|
|
208
|
+
#owns;
|
|
209
|
+
#onReset;
|
|
210
|
+
#frames = /* @__PURE__ */ new Set();
|
|
211
|
+
#generation = 0;
|
|
212
|
+
/**
|
|
213
|
+
* @param owns - whether `form` owns a control this controller derives from.
|
|
214
|
+
* @param onReset - the reconciliation pass, run once per honoured reset.
|
|
215
|
+
*/
|
|
216
|
+
constructor(owns, onReset) {
|
|
217
|
+
this.#owns = owns;
|
|
218
|
+
this.#onReset = onReset;
|
|
219
|
+
}
|
|
220
|
+
/** Subscribes to `reset` at the document; call from `connect()`. Idempotent. */
|
|
221
|
+
observe() {
|
|
222
|
+
document.addEventListener("reset", this.#handleReset, true);
|
|
223
|
+
}
|
|
224
|
+
/** Unsubscribes and drops every pending pass; call from `disconnect()`. */
|
|
225
|
+
disconnect() {
|
|
226
|
+
document.removeEventListener("reset", this.#handleReset, true);
|
|
227
|
+
this.#generation += 1;
|
|
228
|
+
for (const frame of this.#frames) cancelAnimationFrame(frame);
|
|
229
|
+
this.#frames.clear();
|
|
230
|
+
}
|
|
231
|
+
#handleReset = (event) => {
|
|
232
|
+
const form = event.target;
|
|
233
|
+
if (!(form instanceof HTMLFormElement) || !this.#owns(form)) return;
|
|
234
|
+
const generation = this.#generation;
|
|
235
|
+
queueMicrotask(() => {
|
|
236
|
+
if (generation !== this.#generation) return;
|
|
237
|
+
if (event.eventPhase === Event.NONE) {
|
|
238
|
+
this.#settle(event);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const frame = requestAnimationFrame(() => {
|
|
242
|
+
this.#frames.delete(frame);
|
|
243
|
+
this.#settle(event);
|
|
244
|
+
});
|
|
245
|
+
this.#frames.add(frame);
|
|
246
|
+
});
|
|
247
|
+
};
|
|
248
|
+
/** Runs the pass unless a listener cancelled the reset before it landed. */
|
|
249
|
+
#settle(event) {
|
|
250
|
+
if (!event.defaultPrevented) this.#onReset();
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
174
254
|
// src/utils/half_width.ts
|
|
175
255
|
var FULL_WIDTH_SHIFT = 65248;
|
|
176
256
|
function halfWidthChar(char) {
|
|
@@ -219,6 +299,27 @@ var MicrotaskCoalescer = class {
|
|
|
219
299
|
};
|
|
220
300
|
|
|
221
301
|
// src/controllers/otp_controller.ts
|
|
302
|
+
var LEASE_MARKERS = {
|
|
303
|
+
"aria-invalid": "data-otp-invalid-lease",
|
|
304
|
+
"aria-errormessage": "data-otp-errormessage-lease",
|
|
305
|
+
"aria-describedby": "data-otp-describedby-lease"
|
|
306
|
+
};
|
|
307
|
+
function isErrorLease(value) {
|
|
308
|
+
const lease = value;
|
|
309
|
+
return typeof lease?.written === "string" && (lease.authored === null || typeof lease.authored === "string");
|
|
310
|
+
}
|
|
311
|
+
function readLease(field, marker) {
|
|
312
|
+
try {
|
|
313
|
+
const parsed = JSON.parse(field.getAttribute(marker) ?? "null");
|
|
314
|
+
return isErrorLease(parsed) ? parsed : null;
|
|
315
|
+
} catch {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function describedByWith(described, errorId) {
|
|
320
|
+
const tokens = (described ?? "").split(/\s+/).filter((token) => token.length > 0 && token !== errorId);
|
|
321
|
+
return [...tokens, errorId].join(" ");
|
|
322
|
+
}
|
|
222
323
|
var DEFAULT_PATTERN = "[0-9]";
|
|
223
324
|
var DEFAULT_MATCHER = new RegExp(`^(?:${DEFAULT_PATTERN})$`);
|
|
224
325
|
function hasModifier(event) {
|
|
@@ -240,38 +341,59 @@ var OtpController = class extends Controller {
|
|
|
240
341
|
#patternSource = DEFAULT_PATTERN;
|
|
241
342
|
/** Public state carried by the last dispatch; keeps a no-op sync silent. */
|
|
242
343
|
#published = null;
|
|
243
|
-
/** Field whose confirming `input` after `compositionend` is already handled. */
|
|
244
|
-
#confirmedField = null;
|
|
245
344
|
/** True between connect and disconnect, so pre-connect Value changes stay silent. */
|
|
246
345
|
#connected = false;
|
|
247
346
|
/** Digit each field last committed, restored when rejected input replaced it. */
|
|
248
347
|
#committed = /* @__PURE__ */ new WeakMap();
|
|
249
|
-
/**
|
|
348
|
+
/** The field being composed, and the text it held when the composition began. */
|
|
349
|
+
#composing = null;
|
|
350
|
+
/** A `pattern` change whose rejected characters wait for the composition to end. */
|
|
351
|
+
#dropHeld = false;
|
|
352
|
+
/**
|
|
353
|
+
* Collapses one batch of page changes — field target and `pattern` callbacks, a
|
|
354
|
+
* form reset — into a single reconciliation.
|
|
355
|
+
*/
|
|
250
356
|
#reconcile = new MicrotaskCoalescer(() => this.#reconcileFields());
|
|
251
|
-
#ariaInvalid = new AttributeLease("aria-invalid");
|
|
252
|
-
#ariaErrorMessage = new AttributeLease("aria-errormessage");
|
|
253
|
-
#ariaDescribedBy = new AttributeLease("aria-describedby");
|
|
254
357
|
#errorHidden = new AttributeLease("hidden");
|
|
255
358
|
#state = new AttributeLease("data-state");
|
|
256
359
|
/** Rewinds the transient error surface before Turbo freezes the page. */
|
|
257
360
|
#beforeCache = new BeforeCacheReset(() => this.#clearError());
|
|
258
|
-
/**
|
|
361
|
+
/** Reads the fields back once a native reset restored them, as a reconciliation. */
|
|
362
|
+
#formReset = new FormResetWatcher(
|
|
363
|
+
(form) => this.#ownedBy(form),
|
|
364
|
+
() => {
|
|
365
|
+
this.#adopt();
|
|
366
|
+
this.#reconcile.schedule();
|
|
367
|
+
}
|
|
368
|
+
);
|
|
369
|
+
/**
|
|
370
|
+
* Owns IME lifecycle state across every digit field. When a composition ends, a
|
|
371
|
+
* `pattern` drop it held back applies first — it was declared first — and the
|
|
372
|
+
* user's commit is taken after it.
|
|
373
|
+
*/
|
|
259
374
|
#composition = new CompositionTracker({
|
|
260
|
-
onStart: () => {
|
|
261
|
-
|
|
375
|
+
onStart: (event) => {
|
|
376
|
+
const field = this.#fieldFrom(event);
|
|
377
|
+
if (field) this.#composing = { field, before: field.value };
|
|
262
378
|
},
|
|
263
379
|
onEnd: (event) => {
|
|
380
|
+
const composing = this.#composing;
|
|
381
|
+
this.#composing = null;
|
|
264
382
|
const input = this.#fieldFrom(event);
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
this.#
|
|
383
|
+
const data = event.data ?? "";
|
|
384
|
+
const text = input && data.length <= input.value.length ? input.value : data;
|
|
385
|
+
const commit = input !== null && (composing?.field !== input || text !== composing.before);
|
|
386
|
+
if (this.#dropHeld) {
|
|
387
|
+
if (input && commit) input.value = this.#committed.get(input) ?? "";
|
|
388
|
+
this.#reconcileFields();
|
|
389
|
+
}
|
|
390
|
+
if (input && commit) this.#accept(input, text);
|
|
269
391
|
}
|
|
270
392
|
});
|
|
271
393
|
connect() {
|
|
272
394
|
this.#connected = true;
|
|
273
395
|
for (const field of this.fieldTargets) this.#bind(field);
|
|
274
|
-
|
|
396
|
+
this.#formReset.observe();
|
|
275
397
|
this.#beforeCache.activate();
|
|
276
398
|
this.#reconcile.activate();
|
|
277
399
|
this.#adopt();
|
|
@@ -281,9 +403,10 @@ var OtpController = class extends Controller {
|
|
|
281
403
|
this.#connected = false;
|
|
282
404
|
for (const field of this.fieldTargets) this.#unbind(field);
|
|
283
405
|
this.#composition.disconnect();
|
|
284
|
-
|
|
406
|
+
this.#formReset.disconnect();
|
|
285
407
|
this.#beforeCache.deactivate();
|
|
286
408
|
this.#reconcile.cancel();
|
|
409
|
+
this.#dropHeld = false;
|
|
287
410
|
this.#clearError();
|
|
288
411
|
this.#state.return(this.element);
|
|
289
412
|
}
|
|
@@ -300,18 +423,29 @@ var OtpController = class extends Controller {
|
|
|
300
423
|
/** Releases a dropped field's listeners and leases, then reconciles the rest. */
|
|
301
424
|
fieldTargetDisconnected(element) {
|
|
302
425
|
this.#unbind(element);
|
|
303
|
-
this.#
|
|
426
|
+
this.#returnLeases(element);
|
|
304
427
|
this.#reconcile.schedule();
|
|
305
428
|
}
|
|
306
429
|
/**
|
|
307
430
|
* Re-validates a changed `pattern` declaration once and drops any entered digit
|
|
308
431
|
* the new pattern no longer accepts, so the combined value stays interpretable.
|
|
432
|
+
* The page changed the declaration, so the move joins the same reconciliation
|
|
433
|
+
* as fields added or removed in that mutation. While a field is composing, its
|
|
434
|
+
* text is the IME's, and the drop waits for the composition to end.
|
|
309
435
|
*/
|
|
310
436
|
patternValueChanged() {
|
|
311
437
|
const compiled = compileRegExp(this.patternValue, "exact");
|
|
312
438
|
this.#patternSource = compiled ? this.patternValue : DEFAULT_PATTERN;
|
|
313
439
|
this.#pattern = compiled ?? DEFAULT_MATCHER;
|
|
314
440
|
if (!this.#connected) return;
|
|
441
|
+
if (this.#composing !== null) {
|
|
442
|
+
this.#dropHeld = true;
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (this.#dropRejected()) this.#reconcile.schedule();
|
|
446
|
+
}
|
|
447
|
+
/** Empties every writable field holding a character the pattern rejects; whether one did. */
|
|
448
|
+
#dropRejected() {
|
|
315
449
|
let dropped = false;
|
|
316
450
|
for (const field of this.fieldTargets) {
|
|
317
451
|
if (field.value === "" || !this.#isWritable(field)) continue;
|
|
@@ -319,15 +453,13 @@ var OtpController = class extends Controller {
|
|
|
319
453
|
this.#writeField(field, "");
|
|
320
454
|
dropped = true;
|
|
321
455
|
}
|
|
322
|
-
|
|
456
|
+
return dropped;
|
|
323
457
|
}
|
|
324
458
|
/** Handles keystroke inputs, distributes autofilled text, and advances focus. */
|
|
325
459
|
onInput(event) {
|
|
326
460
|
const input = this.#fieldFrom(event);
|
|
327
461
|
if (!input) return;
|
|
328
|
-
|
|
329
|
-
this.#confirmedField = null;
|
|
330
|
-
if (confirmed === input) return;
|
|
462
|
+
if (this.#composition.consumesConfirmedInput(event)) return;
|
|
331
463
|
if (this.#composition.isComposing(event)) return;
|
|
332
464
|
this.#accept(input);
|
|
333
465
|
}
|
|
@@ -336,7 +468,6 @@ var OtpController = class extends Controller {
|
|
|
336
468
|
if (isReservedArrowChord(event)) return;
|
|
337
469
|
const input = this.#fieldFrom(event);
|
|
338
470
|
if (!input) return;
|
|
339
|
-
this.#confirmedField = null;
|
|
340
471
|
if (this.#composition.isComposing(event)) return;
|
|
341
472
|
const index = this.fieldTargets.indexOf(input);
|
|
342
473
|
const fields = this.fieldTargets;
|
|
@@ -400,7 +531,6 @@ var OtpController = class extends Controller {
|
|
|
400
531
|
const input = this.#fieldFrom(event);
|
|
401
532
|
if (!input) return;
|
|
402
533
|
event.preventDefault();
|
|
403
|
-
this.#confirmedField = null;
|
|
404
534
|
this.#distribute(input, toHalfWidth(event.clipboardData?.getData("text") ?? ""));
|
|
405
535
|
}
|
|
406
536
|
/**
|
|
@@ -431,16 +561,6 @@ var OtpController = class extends Controller {
|
|
|
431
561
|
input.select();
|
|
432
562
|
}
|
|
433
563
|
};
|
|
434
|
-
/** Reconciles derived state after a non-cancelled reset restores the fields. */
|
|
435
|
-
#onReset = (event) => {
|
|
436
|
-
const form = event.target;
|
|
437
|
-
if (!(form instanceof HTMLFormElement) || !this.#ownedBy(form)) return;
|
|
438
|
-
queueMicrotask(() => {
|
|
439
|
-
if (event.defaultPrevented) return;
|
|
440
|
-
this.#adopt();
|
|
441
|
-
this.#syncAndDispatch();
|
|
442
|
-
});
|
|
443
|
-
};
|
|
444
564
|
/** Whether a form owns at least one field or the hidden combined value. */
|
|
445
565
|
#ownedBy(form) {
|
|
446
566
|
if (this.fieldTargets.some((field) => field.form === form)) return true;
|
|
@@ -453,7 +573,7 @@ var OtpController = class extends Controller {
|
|
|
453
573
|
#unbind(field) {
|
|
454
574
|
field.removeEventListener("focus", this.#onFieldFocus);
|
|
455
575
|
this.#composition.unobserve(field);
|
|
456
|
-
if (this.#
|
|
576
|
+
if (this.#composing?.field === field) this.#composing = null;
|
|
457
577
|
}
|
|
458
578
|
/** Reads every field back so a restored or reset group starts consistent. */
|
|
459
579
|
#adopt() {
|
|
@@ -461,25 +581,55 @@ var OtpController = class extends Controller {
|
|
|
461
581
|
this.#clearError();
|
|
462
582
|
if (this.hasErrorTarget) this.errorTarget.setAttribute("hidden", "");
|
|
463
583
|
}
|
|
584
|
+
/** Writes one error attribute, keeping the first authored value it displaces. */
|
|
585
|
+
#lease(field, attribute, value) {
|
|
586
|
+
const marker = LEASE_MARKERS[attribute];
|
|
587
|
+
const held = readLease(field, marker);
|
|
588
|
+
const authored = held ? held.authored : field.getAttribute(attribute);
|
|
589
|
+
field.setAttribute(marker, JSON.stringify({ authored, written: value }));
|
|
590
|
+
field.setAttribute(attribute, value);
|
|
591
|
+
}
|
|
592
|
+
/** Gives one field's error attributes back where they still hold what this wrote. */
|
|
593
|
+
#returnLeases(field) {
|
|
594
|
+
for (const [attribute, marker] of Object.entries(LEASE_MARKERS)) {
|
|
595
|
+
const held = readLease(field, marker);
|
|
596
|
+
if (!held) continue;
|
|
597
|
+
field.removeAttribute(marker);
|
|
598
|
+
if (field.getAttribute(attribute) !== held.written) continue;
|
|
599
|
+
if (held.authored === null) field.removeAttribute(attribute);
|
|
600
|
+
else field.setAttribute(attribute, held.authored);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
464
603
|
/** Takes one field's current value as the truth behind its derived state. */
|
|
465
604
|
#adoptField(field) {
|
|
466
605
|
this.#committed.set(field, field.value);
|
|
467
606
|
this.#markFilled(field, field.value);
|
|
468
607
|
}
|
|
469
608
|
/**
|
|
470
|
-
* Absorbs
|
|
609
|
+
* Absorbs one batch of page changes — fields added or removed, entered digits a
|
|
610
|
+
* new `pattern` rejects, fields a native form reset restored — as one state
|
|
611
|
+
* transition.
|
|
471
612
|
*
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
613
|
+
* Nobody edited the passcode here, so the move is reported as `reconcile`:
|
|
614
|
+
* automation listening for `change` must not read a re-render or a reset as an
|
|
615
|
+
* edit, and a passcode that happens to end up full must not fire the
|
|
475
616
|
* `complete` that submits it.
|
|
476
617
|
*
|
|
477
618
|
* Completeness moves on its own when the field count changes: dropping a
|
|
478
619
|
* trailing empty field completes a passcode whose combined value never moved,
|
|
479
620
|
* and adding one un-completes it. Comparing the whole derived state, not the
|
|
480
621
|
* string it contains, is what makes those transitions reportable.
|
|
622
|
+
*
|
|
623
|
+
* A change already in the DOM — a field added or removed, a reset — is
|
|
624
|
+
* published at once, even while a field is composing: that field counts by the
|
|
625
|
+
* character it had committed, never by the IME's uncommitted text. Only a
|
|
626
|
+
* `pattern` drop waits for the composition to end.
|
|
481
627
|
*/
|
|
482
628
|
#reconcileFields() {
|
|
629
|
+
if (this.#dropHeld && this.#composing === null) {
|
|
630
|
+
this.#dropHeld = false;
|
|
631
|
+
this.#dropRejected();
|
|
632
|
+
}
|
|
483
633
|
const previous = this.#published;
|
|
484
634
|
const current = this.#sync();
|
|
485
635
|
if (previous && !statesDiffer(previous, current)) return;
|
|
@@ -571,12 +721,16 @@ var OtpController = class extends Controller {
|
|
|
571
721
|
return this.fieldTargets.find((field) => field === input) ?? null;
|
|
572
722
|
}
|
|
573
723
|
#combinedValue() {
|
|
574
|
-
return this.fieldTargets.map((field) => field
|
|
724
|
+
return this.fieldTargets.map((field) => this.#publishedText(field)).join("");
|
|
575
725
|
}
|
|
576
726
|
/** Every field carries a character, and there is at least one field. */
|
|
577
727
|
#isComplete() {
|
|
578
728
|
const fields = this.fieldTargets;
|
|
579
|
-
return fields.length > 0 && fields.every((field) => field.
|
|
729
|
+
return fields.length > 0 && fields.every((field) => this.#publishedText(field).length > 0);
|
|
730
|
+
}
|
|
731
|
+
/** A field's text as the combined value counts it: a composing field by its committed character. */
|
|
732
|
+
#publishedText(field) {
|
|
733
|
+
return field === this.#composing?.field ? this.#committed.get(field) ?? "" : field.value;
|
|
580
734
|
}
|
|
581
735
|
/**
|
|
582
736
|
* Mirrors the combined value into the form and the root's readable state, and
|
|
@@ -610,31 +764,20 @@ var OtpController = class extends Controller {
|
|
|
610
764
|
#showError() {
|
|
611
765
|
const errorId = this.hasErrorTarget ? ensureId(this.errorTarget, "stimeo--otp-error") : null;
|
|
612
766
|
for (const field of this.fieldTargets) {
|
|
613
|
-
this.#
|
|
767
|
+
this.#lease(field, "aria-invalid", "true");
|
|
614
768
|
if (!errorId) continue;
|
|
615
|
-
this.#
|
|
616
|
-
|
|
769
|
+
this.#lease(field, "aria-errormessage", errorId);
|
|
770
|
+
const described = field.getAttribute("aria-describedby");
|
|
771
|
+
this.#lease(field, "aria-describedby", describedByWith(described, errorId));
|
|
617
772
|
}
|
|
618
773
|
if (this.hasErrorTarget) this.#errorHidden.write(this.errorTarget, null);
|
|
619
774
|
this.dispatch("invalid", { detail: { pattern: this.#patternSource } });
|
|
620
775
|
}
|
|
621
776
|
/** Returns every error lease, restoring the authored error surface. */
|
|
622
777
|
#clearError() {
|
|
623
|
-
this.#
|
|
624
|
-
this.#ariaErrorMessage.returnAll();
|
|
625
|
-
this.#ariaDescribedBy.returnAll();
|
|
778
|
+
for (const field of this.fieldTargets) this.#returnLeases(field);
|
|
626
779
|
this.#errorHidden.returnAll();
|
|
627
780
|
}
|
|
628
|
-
#returnFieldLeases(field) {
|
|
629
|
-
this.#ariaInvalid.return(field);
|
|
630
|
-
this.#ariaErrorMessage.return(field);
|
|
631
|
-
this.#ariaDescribedBy.return(field);
|
|
632
|
-
}
|
|
633
|
-
/** The field's own description tokens with the error id appended once. */
|
|
634
|
-
#describedByWith(field, errorId) {
|
|
635
|
-
const tokens = (field.getAttribute("aria-describedby") ?? "").split(/\s+/).filter((token) => token.length > 0 && token !== errorId);
|
|
636
|
-
return [...tokens, errorId].join(" ");
|
|
637
|
-
}
|
|
638
781
|
};
|
|
639
782
|
|
|
640
783
|
export { OtpController };
|