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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +178 -0
- data/dist/controllers/alert_dialog_controller.js +75 -4
- data/dist/controllers/aspect_ratio_controller.js +19 -11
- data/dist/controllers/avatar_controller.js +237 -40
- data/dist/controllers/carousel_controller.js +85 -9
- data/dist/controllers/character_counter_controller.js +338 -63
- data/dist/controllers/checkbox_controller.js +136 -25
- data/dist/controllers/color_picker_controller.js +35 -9
- data/dist/controllers/command_palette_controller.js +75 -4
- data/dist/controllers/conditional_fields_controller.js +345 -51
- data/dist/controllers/confirm_controller.js +75 -4
- data/dist/controllers/date_range_picker_controller.js +157 -30
- data/dist/controllers/dialog_controller.js +75 -4
- data/dist/controllers/direct_upload_controller.js +201 -45
- data/dist/controllers/dirty_form_controller.js +192 -29
- data/dist/controllers/dismissible_controller.js +83 -18
- data/dist/controllers/drawer_controller.js +75 -4
- data/dist/controllers/file_dropzone_controller.js +26 -3
- data/dist/controllers/focus_controller.js +75 -4
- data/dist/controllers/form_field_controller.js +280 -62
- data/dist/controllers/form_validation_controller.js +208 -83
- data/dist/controllers/idle_controller.js +27 -5
- data/dist/controllers/menubar_controller.js +5 -3
- data/dist/controllers/multi_select_controller.js +460 -151
- data/dist/controllers/number_input_controller.js +317 -51
- data/dist/controllers/overflow_menu_controller.js +6 -1
- data/dist/controllers/pagination_controller.js +35 -1
- data/dist/controllers/password_strength_controller.js +20 -2
- data/dist/controllers/persist_controller.js +449 -120
- data/dist/controllers/popover_controller.js +77 -4
- data/dist/controllers/radio_group_controller.js +540 -56
- data/dist/controllers/rating_controller.js +276 -89
- data/dist/controllers/resizable_controller.js +33 -0
- data/dist/controllers/roving_controller.js +60 -5
- data/dist/controllers/scroll_area_controller.js +557 -125
- data/dist/controllers/scroll_visibility_controller.js +33 -0
- data/dist/controllers/separator_controller.js +354 -38
- data/dist/controllers/sidebar_controller.js +83 -10
- data/dist/controllers/submit_once_controller.js +399 -121
- data/dist/controllers/tags_input_controller.js +356 -120
- data/dist/controllers/theme_controller.js +8 -6
- data/dist/controllers/time_picker_controller.js +296 -107
- data/dist/controllers/toggle_group_controller.js +378 -55
- data/dist/controllers/toolbar_controller.js +5 -3
- data/dist/controllers/tree_view_controller.js +7 -4
- data/dist/index.js +4963 -1581
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
|
@@ -2,6 +2,17 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/form_field_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/announce.ts
|
|
6
|
+
function announce(message, options = {}) {
|
|
7
|
+
const text = message.trim();
|
|
8
|
+
if (text.length === 0) return;
|
|
9
|
+
window.dispatchEvent(
|
|
10
|
+
new CustomEvent("stimeo--announcer:announce", {
|
|
11
|
+
detail: { message: text, assertive: options.assertive === true }
|
|
12
|
+
})
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
5
16
|
// src/utils/aria_ids.ts
|
|
6
17
|
var counter = 0;
|
|
7
18
|
function uniqueId(prefix = "stimeo") {
|
|
@@ -19,7 +30,111 @@ function ensureId(element, prefix = "stimeo") {
|
|
|
19
30
|
return id;
|
|
20
31
|
}
|
|
21
32
|
|
|
33
|
+
// src/utils/attribute_lease.ts
|
|
34
|
+
var AttributeLease = class {
|
|
35
|
+
#attribute;
|
|
36
|
+
#records = /* @__PURE__ */ new Map();
|
|
37
|
+
/** @param attribute - The attribute whose temporary values this lease owns. */
|
|
38
|
+
constructor(attribute) {
|
|
39
|
+
this.#attribute = attribute;
|
|
40
|
+
}
|
|
41
|
+
/** Writes or removes the leased attribute while preserving its authored value. */
|
|
42
|
+
write(element, value) {
|
|
43
|
+
const existing = this.#records.get(element);
|
|
44
|
+
if (existing) {
|
|
45
|
+
existing.written = value;
|
|
46
|
+
} else {
|
|
47
|
+
this.#records.set(element, {
|
|
48
|
+
original: element.getAttribute(this.#attribute),
|
|
49
|
+
written: value
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
this.#reflect(element, value);
|
|
53
|
+
}
|
|
54
|
+
/** Returns one lease without overwriting a value subsequently authored by a consumer. */
|
|
55
|
+
return(element) {
|
|
56
|
+
const record = this.#records.get(element);
|
|
57
|
+
if (!record) return;
|
|
58
|
+
this.#records.delete(element);
|
|
59
|
+
const stillOwned = element.getAttribute(this.#attribute) === record.written;
|
|
60
|
+
if (stillOwned) this.#reflect(element, record.original);
|
|
61
|
+
}
|
|
62
|
+
/** Reflects only a real value transition, avoiding self-triggered mutation work. */
|
|
63
|
+
#reflect(element, value) {
|
|
64
|
+
if (element.getAttribute(this.#attribute) === value) return;
|
|
65
|
+
if (value === null) element.removeAttribute(this.#attribute);
|
|
66
|
+
else element.setAttribute(this.#attribute, value);
|
|
67
|
+
}
|
|
68
|
+
/** Returns every outstanding lease using the same ownership check as {@link return}. */
|
|
69
|
+
returnAll() {
|
|
70
|
+
for (const element of Array.from(this.#records.keys())) this.return(element);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// src/utils/before_cache_reset.ts
|
|
75
|
+
var BeforeCacheReset = class _BeforeCacheReset {
|
|
76
|
+
/** Every subscribed instance, iterated by the one shared document listener. */
|
|
77
|
+
static #subscribers = /* @__PURE__ */ new Set();
|
|
78
|
+
/** The shared listener; installed while at least one instance is subscribed. */
|
|
79
|
+
static #onBeforeCache = () => {
|
|
80
|
+
for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
|
|
81
|
+
};
|
|
82
|
+
#rewind;
|
|
83
|
+
/** @param rewind - the pass that returns this controller's state to its initial form. */
|
|
84
|
+
constructor(rewind) {
|
|
85
|
+
this.#rewind = rewind;
|
|
86
|
+
}
|
|
87
|
+
/** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
|
|
88
|
+
activate() {
|
|
89
|
+
const first = _BeforeCacheReset.#subscribers.size === 0;
|
|
90
|
+
_BeforeCacheReset.#subscribers.add(this);
|
|
91
|
+
if (first) {
|
|
92
|
+
document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
|
|
96
|
+
deactivate() {
|
|
97
|
+
_BeforeCacheReset.#subscribers.delete(this);
|
|
98
|
+
if (_BeforeCacheReset.#subscribers.size > 0) return;
|
|
99
|
+
document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// src/utils/microtask_coalescer.ts
|
|
104
|
+
var MicrotaskCoalescer = class {
|
|
105
|
+
#run;
|
|
106
|
+
#queued = false;
|
|
107
|
+
#active = false;
|
|
108
|
+
#generation = 0;
|
|
109
|
+
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
110
|
+
constructor(run) {
|
|
111
|
+
this.#run = run;
|
|
112
|
+
}
|
|
113
|
+
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
114
|
+
activate() {
|
|
115
|
+
this.#active = true;
|
|
116
|
+
}
|
|
117
|
+
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
118
|
+
cancel() {
|
|
119
|
+
this.#active = false;
|
|
120
|
+
this.#queued = false;
|
|
121
|
+
this.#generation += 1;
|
|
122
|
+
}
|
|
123
|
+
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
124
|
+
schedule() {
|
|
125
|
+
if (!this.#active || this.#queued) return;
|
|
126
|
+
this.#queued = true;
|
|
127
|
+
const generation = this.#generation;
|
|
128
|
+
queueMicrotask(() => {
|
|
129
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
130
|
+
this.#queued = false;
|
|
131
|
+
this.#run();
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
22
136
|
// src/controllers/form_field_controller.ts
|
|
137
|
+
var OBSERVED_ATTRIBUTES = ["hidden", "id"];
|
|
23
138
|
var FormFieldController = class _FormFieldController extends Controller {
|
|
24
139
|
static targets = ["control", "description", "error"];
|
|
25
140
|
static values = {
|
|
@@ -29,31 +144,85 @@ var FormFieldController = class _FormFieldController extends Controller {
|
|
|
29
144
|
static events = ["validate"];
|
|
30
145
|
/** Root attribute (CSS hook) reflecting the invalid state. */
|
|
31
146
|
static #INVALID_ATTR = "data-stimeo--form-field-invalid";
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
147
|
+
/** Collapses one target/morph batch into one silent ARIA reconciliation. */
|
|
148
|
+
#reconcile = new MicrotaskCoalescer(() => this.#reconcileDom());
|
|
149
|
+
/** ARIA ownership is scoped to the current singular control target. */
|
|
150
|
+
#beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
|
|
151
|
+
#ariaDescribedBy = new AttributeLease("aria-describedby");
|
|
152
|
+
#ariaErrorMessage = new AttributeLease("aria-errormessage");
|
|
153
|
+
#ariaInvalid = new AttributeLease("aria-invalid");
|
|
154
|
+
/** Watches retained target ids, error visibility, and error content. */
|
|
155
|
+
#observer = new MutationObserver((records) => {
|
|
156
|
+
if (records.some((record) => this.#isRelevantMutation(record))) {
|
|
157
|
+
this.#reconcile.schedule();
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
#activeControl = null;
|
|
36
161
|
#baseDescribedBy = [];
|
|
37
|
-
|
|
162
|
+
#explicitInvalid = false;
|
|
163
|
+
#initialized = false;
|
|
164
|
+
/** Wires the initial graph and starts retained-target reconciliation. */
|
|
38
165
|
connect() {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
166
|
+
this.#reconcile.activate();
|
|
167
|
+
this.#ensureAssociationIds();
|
|
168
|
+
this.#beforeCache.activate();
|
|
169
|
+
if (!this.#initialized) {
|
|
170
|
+
this.#explicitInvalid = this.element.hasAttribute(_FormFieldController.#INVALID_ATTR) && this.#shownErrors().length === 0;
|
|
171
|
+
this.#initialized = true;
|
|
44
172
|
}
|
|
45
|
-
this.#
|
|
46
|
-
this.#
|
|
173
|
+
this.#reconcileDom();
|
|
174
|
+
this.#observer.observe(this.element, {
|
|
175
|
+
attributes: true,
|
|
176
|
+
attributeFilter: OBSERVED_ATTRIBUTES,
|
|
177
|
+
characterData: true,
|
|
178
|
+
childList: true,
|
|
179
|
+
subtree: true
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/** Releases observers, queued work, and ARIA borrowed on the current control. */
|
|
183
|
+
disconnect() {
|
|
184
|
+
this.#reconcile.cancel();
|
|
185
|
+
this.#observer.disconnect();
|
|
186
|
+
this.#beforeCache.deactivate();
|
|
187
|
+
this.#activeControl && this.#releaseControl(this.#activeControl);
|
|
188
|
+
}
|
|
189
|
+
/** Reconciles a control inserted or replaced at runtime. */
|
|
190
|
+
controlTargetConnected() {
|
|
191
|
+
this.#reconcile.schedule();
|
|
192
|
+
}
|
|
193
|
+
/** Schedules restoration of authored ARIA when a control leaves this field. */
|
|
194
|
+
controlTargetDisconnected() {
|
|
195
|
+
this.#reconcile.schedule();
|
|
196
|
+
}
|
|
197
|
+
/** Reconciles a description inserted or replaced at runtime. */
|
|
198
|
+
descriptionTargetConnected() {
|
|
199
|
+
this.#reconcile.schedule();
|
|
200
|
+
}
|
|
201
|
+
/** Removes a departed description from the control's association graph. */
|
|
202
|
+
descriptionTargetDisconnected() {
|
|
203
|
+
this.#reconcile.schedule();
|
|
204
|
+
}
|
|
205
|
+
/** Reconciles an error region inserted or replaced at runtime. */
|
|
206
|
+
errorTargetConnected() {
|
|
207
|
+
this.#reconcile.schedule();
|
|
208
|
+
}
|
|
209
|
+
/** Removes a departed error from invalid state and the association graph. */
|
|
210
|
+
errorTargetDisconnected() {
|
|
211
|
+
this.#reconcile.schedule();
|
|
47
212
|
}
|
|
48
213
|
/**
|
|
49
214
|
* Marks the field invalid and shows the error message. Bound via `data-action`
|
|
50
215
|
* (`#setError`) or callable directly.
|
|
51
216
|
*
|
|
217
|
+
* A non-empty shown message is announced exactly once through the shared
|
|
218
|
+
* assertive announcer. Initial/server reconciliation is deliberately silent.
|
|
219
|
+
*
|
|
52
220
|
* @param arg - Either the message string, or the action event whose
|
|
53
221
|
* `data-stimeo--form-field-message-param` supplies it. When no message is
|
|
54
222
|
* resolvable, any already-populated error targets are simply (re)shown.
|
|
223
|
+
* @param options - Programmatic overrides. Stimulus actions use the Values.
|
|
55
224
|
*/
|
|
56
|
-
setError(arg) {
|
|
225
|
+
setError(arg, options = {}) {
|
|
57
226
|
const message = this.#resolveMessage(arg);
|
|
58
227
|
if (message !== null && this.hasErrorTarget) {
|
|
59
228
|
this.errorTargets[0]?.replaceChildren(document.createTextNode(message));
|
|
@@ -61,70 +230,86 @@ var FormFieldController = class _FormFieldController extends Controller {
|
|
|
61
230
|
for (const error of this.errorTargets) {
|
|
62
231
|
error.hidden = (error.textContent ?? "").trim() === "";
|
|
63
232
|
}
|
|
64
|
-
this.#
|
|
65
|
-
this
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
233
|
+
this.#explicitInvalid = true;
|
|
234
|
+
this.#reconcileDom();
|
|
235
|
+
const shownMessage = this.#shownMessage();
|
|
236
|
+
const detail = { valid: false, message: shownMessage };
|
|
237
|
+
this.dispatch("validate", { detail });
|
|
238
|
+
announce(shownMessage, { assertive: true });
|
|
239
|
+
if (options.focus ?? this.focusOnErrorValue) this.#activeControl?.focus();
|
|
69
240
|
}
|
|
70
241
|
/**
|
|
71
242
|
* Clears the error: empties and hides every error target and marks the field
|
|
72
243
|
* valid. Bound via `data-action` (`#clearError`) or callable directly.
|
|
244
|
+
*
|
|
245
|
+
* Clearing is silent: the visual/ARIA state and validation event are sufficient,
|
|
246
|
+
* while success wording remains an explicit consumer announcement.
|
|
73
247
|
*/
|
|
74
248
|
clearError() {
|
|
75
249
|
for (const error of this.errorTargets) {
|
|
76
250
|
error.replaceChildren();
|
|
77
251
|
error.hidden = true;
|
|
78
252
|
}
|
|
79
|
-
this.#
|
|
80
|
-
this
|
|
253
|
+
this.#explicitInvalid = false;
|
|
254
|
+
this.#reconcileDom();
|
|
255
|
+
const detail = { valid: true, message: "" };
|
|
256
|
+
this.dispatch("validate", { detail });
|
|
81
257
|
}
|
|
82
258
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* change (and survives Turbo morphing).
|
|
86
|
-
*
|
|
87
|
-
* @param force - When `true`, the field is marked invalid regardless of whether
|
|
88
|
-
* a (visible, non-empty) error region exists. {@link setError} passes this so
|
|
89
|
-
* the invalid state holds even with no error target; derivation from the DOM
|
|
90
|
-
* (connect / {@link clearError}) leaves it `false`.
|
|
259
|
+
* Rebuilds ids and derived ARIA from the settled target graph without reporting
|
|
260
|
+
* a user validation action.
|
|
91
261
|
*/
|
|
92
|
-
#
|
|
262
|
+
#reconcileDom() {
|
|
263
|
+
this.#ensureAssociationIds();
|
|
264
|
+
this.#adoptCurrentControl();
|
|
93
265
|
const shown = this.#shownErrors();
|
|
94
|
-
const invalid =
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
266
|
+
const invalid = this.#explicitInvalid || shown.length > 0;
|
|
267
|
+
this.element.toggleAttribute(_FormFieldController.#INVALID_ATTR, invalid);
|
|
268
|
+
const control = this.#activeControl;
|
|
269
|
+
if (!control) return;
|
|
270
|
+
this.#ariaInvalid.write(control, invalid ? "true" : "false");
|
|
271
|
+
const primaryErrorId = shown[0]?.id ?? null;
|
|
272
|
+
this.#ariaErrorMessage.write(control, primaryErrorId);
|
|
273
|
+
const associationIds = this.#orderedElements([...this.descriptionTargets, ...shown]).map(
|
|
274
|
+
(element) => element.id
|
|
275
|
+
);
|
|
276
|
+
const describedBy = this.#uniqueTokens([...this.#baseDescribedBy, ...associationIds]);
|
|
277
|
+
this.#ariaDescribedBy.write(control, describedBy.length > 0 ? describedBy.join(" ") : null);
|
|
278
|
+
}
|
|
279
|
+
/** Assigns stable ids before either capture or reflection uses them. */
|
|
280
|
+
#ensureAssociationIds() {
|
|
281
|
+
for (const description of this.descriptionTargets) {
|
|
282
|
+
ensureId(description, "stimeo--form-field-desc");
|
|
99
283
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
control.setAttribute("aria-invalid", invalid ? "true" : "false");
|
|
103
|
-
const errorIds = shown.map((error) => error.id);
|
|
104
|
-
const primaryErrorId = errorIds[0];
|
|
105
|
-
if (primaryErrorId) {
|
|
106
|
-
control.setAttribute("aria-errormessage", primaryErrorId);
|
|
107
|
-
} else {
|
|
108
|
-
control.removeAttribute("aria-errormessage");
|
|
284
|
+
for (const error of this.errorTargets) {
|
|
285
|
+
ensureId(error, "stimeo--form-field-error");
|
|
109
286
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
287
|
+
}
|
|
288
|
+
/** Switches ARIA ownership when the singular current control target changes. */
|
|
289
|
+
#adoptCurrentControl() {
|
|
290
|
+
const next = this.hasControlTarget ? this.controlTarget : null;
|
|
291
|
+
if (next === this.#activeControl) return;
|
|
292
|
+
this.#activeControl && this.#releaseControl(this.#activeControl);
|
|
293
|
+
this.#activeControl = next;
|
|
294
|
+
this.#baseDescribedBy = next ? this.#externalDescribedByTokens(next) : [];
|
|
295
|
+
}
|
|
296
|
+
/** Restores one departed control without overwriting later consumer edits. */
|
|
297
|
+
#releaseControl(control) {
|
|
298
|
+
this.#ariaDescribedBy.return(control);
|
|
299
|
+
this.#ariaErrorMessage.return(control);
|
|
300
|
+
this.#ariaInvalid.return(control);
|
|
301
|
+
if (control === this.#activeControl) {
|
|
302
|
+
this.#activeControl = null;
|
|
303
|
+
this.#baseDescribedBy = [];
|
|
119
304
|
}
|
|
120
305
|
}
|
|
121
|
-
/** Error targets currently visible and non-empty. */
|
|
306
|
+
/** Error targets currently visible and non-empty, in document order. */
|
|
122
307
|
#shownErrors() {
|
|
123
|
-
return this
|
|
124
|
-
(error) => !error.hidden && (error.textContent ?? "").trim() !== ""
|
|
308
|
+
return this.#orderedElements(
|
|
309
|
+
this.errorTargets.filter((error) => !error.hidden && (error.textContent ?? "").trim() !== "")
|
|
125
310
|
);
|
|
126
311
|
}
|
|
127
|
-
/** Text of the first shown error, for
|
|
312
|
+
/** Text of the first shown error, for validation detail and announcement. */
|
|
128
313
|
#shownMessage() {
|
|
129
314
|
return (this.#shownErrors()[0]?.textContent ?? "").trim();
|
|
130
315
|
}
|
|
@@ -135,17 +320,50 @@ var FormFieldController = class _FormFieldController extends Controller {
|
|
|
135
320
|
return typeof message === "string" ? message : null;
|
|
136
321
|
}
|
|
137
322
|
/**
|
|
138
|
-
* Tokens
|
|
139
|
-
*
|
|
323
|
+
* Tokens authored on a newly adopted control, excluding ids this controller
|
|
324
|
+
* owns through its current description/error targets.
|
|
140
325
|
*/
|
|
141
|
-
#externalDescribedByTokens() {
|
|
142
|
-
if (!this.hasControlTarget) return [];
|
|
326
|
+
#externalDescribedByTokens(control) {
|
|
143
327
|
const owned = /* @__PURE__ */ new Set([
|
|
144
328
|
...this.descriptionTargets.map((description) => description.id),
|
|
145
329
|
...this.errorTargets.map((error) => error.id)
|
|
146
330
|
]);
|
|
147
|
-
const existing =
|
|
148
|
-
return
|
|
331
|
+
const existing = control.getAttribute("aria-describedby") ?? "";
|
|
332
|
+
return this.#uniqueTokens(
|
|
333
|
+
existing.split(/\s+/).filter((token) => token.length > 0 && !owned.has(token))
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
/** Deduplicates ARIA tokens without disturbing their first occurrence. */
|
|
337
|
+
#uniqueTokens(tokens) {
|
|
338
|
+
const seen = /* @__PURE__ */ new Set();
|
|
339
|
+
return tokens.filter((token) => {
|
|
340
|
+
if (token.length === 0 || seen.has(token)) return false;
|
|
341
|
+
seen.add(token);
|
|
342
|
+
return true;
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
/** Returns unique current targets in DOM order before serializing IDREF lists. */
|
|
346
|
+
#orderedElements(elements) {
|
|
347
|
+
const candidates = new Set(elements);
|
|
348
|
+
return [this.element, ...this.element.querySelectorAll("*")].filter(
|
|
349
|
+
(element) => candidates.has(element)
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
/** Whether one retained-node mutation can alter this controller's derived state. */
|
|
353
|
+
#isRelevantMutation(record) {
|
|
354
|
+
const target = record.target;
|
|
355
|
+
if (record.type === "attributes") {
|
|
356
|
+
if (record.attributeName === "hidden")
|
|
357
|
+
return this.errorTargets.includes(target);
|
|
358
|
+
return record.attributeName === "id" && [...this.descriptionTargets, ...this.errorTargets].includes(target);
|
|
359
|
+
}
|
|
360
|
+
return this.errorTargets.some((error) => error === target || error.contains(target));
|
|
361
|
+
}
|
|
362
|
+
/** Returns borrowed control ARIA before Turbo snapshots the page. */
|
|
363
|
+
#rewindForCache() {
|
|
364
|
+
this.#ariaDescribedBy.returnAll();
|
|
365
|
+
this.#ariaErrorMessage.returnAll();
|
|
366
|
+
this.#ariaInvalid.returnAll();
|
|
149
367
|
}
|
|
150
368
|
};
|
|
151
369
|
|