stimeo-ui 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +178 -0
- data/README.md +120 -0
- data/dist/cable/index.js +63 -15
- data/dist/controllers/accordion_controller.js +98 -9
- data/dist/controllers/announcer_controller.js +8 -1
- data/dist/controllers/auto_submit_controller.js +43 -2
- data/dist/controllers/avatar_controller.js +1 -1
- data/dist/controllers/breadcrumb_controller.js +9 -8
- data/dist/controllers/bulk_select_controller.js +7 -6
- data/dist/controllers/calendar_controller.js +323 -123
- data/dist/controllers/carousel_controller.js +263 -38
- data/dist/controllers/character_counter_controller.js +40 -2
- data/dist/controllers/checkbox_controller.js +54 -10
- 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 +56 -11
- 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 +205 -58
- data/dist/controllers/data_grid_controller.js +195 -29
- data/dist/controllers/date_range_picker_controller.js +139 -27
- data/dist/controllers/dialog_controller.js +35 -8
- data/dist/controllers/direct_upload_controller.js +22 -4
- data/dist/controllers/dirty_form_controller.js +14 -1
- 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 +305 -46
- 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 +16 -2
- data/dist/controllers/highlight_controller.js +19 -2
- 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/masonry_controller.js +0 -2
- 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 +14 -1
- data/dist/controllers/otp_controller.js +198 -55
- data/dist/controllers/overflow_indicator_controller.js +84 -13
- data/dist/controllers/overflow_menu_controller.js +112 -41
- 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 +69 -2
- data/dist/controllers/resizable_controller.js +82 -22
- data/dist/controllers/scroll_area_controller.js +5 -1
- data/dist/controllers/scroll_visibility_controller.js +0 -1
- data/dist/controllers/scrollspy_controller.js +6 -0
- 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 +11 -1
- 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 +75 -5
- data/dist/controllers/submit_once_controller.js +16 -3
- 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/theme_controller.js +64 -14
- data/dist/controllers/time_picker_controller.js +23 -8
- data/dist/controllers/toast_controller.js +337 -54
- 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 +4588 -1535
- data/dist/positioning/index.js +2 -0
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
|
@@ -89,6 +89,47 @@ function prefersReducedMotion() {
|
|
|
89
89
|
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
// src/utils/state_regions.ts
|
|
93
|
+
var StateRegions = class {
|
|
94
|
+
#whenTrue;
|
|
95
|
+
#whenFalse;
|
|
96
|
+
#taken = /* @__PURE__ */ new WeakSet();
|
|
97
|
+
constructor(options) {
|
|
98
|
+
this.#whenTrue = options.whenTrue;
|
|
99
|
+
this.#whenFalse = options.whenFalse ?? null;
|
|
100
|
+
}
|
|
101
|
+
/** Shows the regions inside `host` that belong to `isTrue` and hides the others. */
|
|
102
|
+
reflect(host, isTrue) {
|
|
103
|
+
const shown = this.#inside(host, this.#whenTrue());
|
|
104
|
+
if (!this.#whenFalse) {
|
|
105
|
+
for (const region of shown) this.#write(region, !isTrue);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const hiddenSide = this.#inside(host, this.#whenFalse());
|
|
109
|
+
if (shown.length === 0 || hiddenSide.length === 0) {
|
|
110
|
+
for (const region of shown) this.#give(region);
|
|
111
|
+
for (const region of hiddenSide) this.#give(region);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
for (const region of shown) this.#write(region, !isTrue);
|
|
115
|
+
for (const region of hiddenSide) this.#write(region, isTrue);
|
|
116
|
+
}
|
|
117
|
+
/** Writes `hidden` where it moves, noting which regions it takes out of view. */
|
|
118
|
+
#write(region, hidden) {
|
|
119
|
+
if (hidden) this.#taken.add(region);
|
|
120
|
+
else this.#taken.delete(region);
|
|
121
|
+
if (region.hidden !== hidden) region.hidden = hidden;
|
|
122
|
+
}
|
|
123
|
+
/** Returns a region this instance hid; one it never hid keeps what it carries. */
|
|
124
|
+
#give(region) {
|
|
125
|
+
if (this.#taken.delete(region)) region.hidden = false;
|
|
126
|
+
}
|
|
127
|
+
/** The declared regions that sit within `host`, which may be the host itself. */
|
|
128
|
+
#inside(host, regions) {
|
|
129
|
+
return regions.filter((region) => host.contains(region));
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
92
133
|
// src/controllers/stick_to_bottom_controller.ts
|
|
93
134
|
var DEFAULT_THRESHOLD = 80;
|
|
94
135
|
var countElements = (nodes) => {
|
|
@@ -97,7 +138,7 @@ var countElements = (nodes) => {
|
|
|
97
138
|
return n;
|
|
98
139
|
};
|
|
99
140
|
var StickToBottomController = class extends Controller {
|
|
100
|
-
static targets = ["content"];
|
|
141
|
+
static targets = ["content", "hasNew"];
|
|
101
142
|
static values = {
|
|
102
143
|
threshold: { type: Number, default: DEFAULT_THRESHOLD },
|
|
103
144
|
behavior: { type: String, default: "auto" },
|
|
@@ -113,11 +154,14 @@ var StickToBottomController = class extends Controller {
|
|
|
113
154
|
#awaitingLayout = false;
|
|
114
155
|
#connected = false;
|
|
115
156
|
#pinned = false;
|
|
157
|
+
#hasNew = false;
|
|
158
|
+
/** Owns `hidden` on the regions declared for the has-new state. */
|
|
159
|
+
#hasNewRegion = new StateRegions({ whenTrue: () => this.hasNewTargets });
|
|
116
160
|
#onScroll = () => this.#updatePinned();
|
|
117
161
|
connect() {
|
|
118
162
|
this.#connected = true;
|
|
119
163
|
if (this.pinOnConnectValue && this.#measurable()) this.#scrollToBottom("instant");
|
|
120
|
-
this
|
|
164
|
+
this.#setHasNew(false);
|
|
121
165
|
this.#pinned = this.#isPinned();
|
|
122
166
|
this.#reflectPinned();
|
|
123
167
|
this.element.addEventListener("scroll", this.#onScroll, { passive: true });
|
|
@@ -138,6 +182,19 @@ var StickToBottomController = class extends Controller {
|
|
|
138
182
|
contentTargetDisconnected() {
|
|
139
183
|
this.#syncWatched();
|
|
140
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Settles a has-new region that arrived at runtime on the side the flag is currently
|
|
187
|
+
* on, the way a region present at connect is settled by the connect-time normalization.
|
|
188
|
+
*
|
|
189
|
+
* Stimulus attaches targets before `connect()`, and a container that leaves the document
|
|
190
|
+
* and comes back keeps its controller, so in that window the flag still reads the
|
|
191
|
+
* arrival of the connection that ended. A region attached there is left to the
|
|
192
|
+
* connect-time normalization, which drops the flag and settles the region in one write.
|
|
193
|
+
*/
|
|
194
|
+
hasNewTargetConnected() {
|
|
195
|
+
if (!this.#connected) return;
|
|
196
|
+
this.#hasNewRegion.reflect(this.element, this.#hasNew);
|
|
197
|
+
}
|
|
141
198
|
/**
|
|
142
199
|
* Re-derives pinned when the distance that counts as the bottom is changed at runtime
|
|
143
200
|
* (a morph that swaps the attribute on a retained element).
|
|
@@ -159,7 +216,7 @@ var StickToBottomController = class extends Controller {
|
|
|
159
216
|
*/
|
|
160
217
|
scrollToBottom() {
|
|
161
218
|
this.#scrollToBottom();
|
|
162
|
-
this
|
|
219
|
+
this.#setHasNew(false);
|
|
163
220
|
this.#updatePinned();
|
|
164
221
|
}
|
|
165
222
|
/** Follows appended children while pinned; otherwise flags new content. */
|
|
@@ -170,7 +227,7 @@ var StickToBottomController = class extends Controller {
|
|
|
170
227
|
if (this.#pinned) {
|
|
171
228
|
this.#scrollToBottom();
|
|
172
229
|
} else {
|
|
173
|
-
this
|
|
230
|
+
this.#setHasNew(true);
|
|
174
231
|
this.dispatch("new", { detail: { count: added } });
|
|
175
232
|
}
|
|
176
233
|
}
|
|
@@ -190,11 +247,24 @@ var StickToBottomController = class extends Controller {
|
|
|
190
247
|
#reflectPinned() {
|
|
191
248
|
if (this.#pinned) {
|
|
192
249
|
this.element.setAttribute("data-pinned", "true");
|
|
193
|
-
this
|
|
250
|
+
this.#setHasNew(false);
|
|
194
251
|
} else {
|
|
195
252
|
this.element.removeAttribute("data-pinned");
|
|
196
253
|
}
|
|
197
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* Mirrors whether content has arrived that the container did not follow onto the state
|
|
257
|
+
* hook and onto the regions declared for that state.
|
|
258
|
+
*/
|
|
259
|
+
#setHasNew(hasNew) {
|
|
260
|
+
this.#hasNew = hasNew;
|
|
261
|
+
if (hasNew) {
|
|
262
|
+
this.element.setAttribute("data-has-new", "true");
|
|
263
|
+
} else {
|
|
264
|
+
this.element.removeAttribute("data-has-new");
|
|
265
|
+
}
|
|
266
|
+
this.#hasNewRegion.reflect(this.element, hasNew);
|
|
267
|
+
}
|
|
198
268
|
/** Whether the container currently sits within `threshold` of its bottom. */
|
|
199
269
|
#isPinned() {
|
|
200
270
|
if (!this.#measurable()) return false;
|
|
@@ -331,7 +331,12 @@ var SubmitOnceController = class extends Controller {
|
|
|
331
331
|
if (!session || !this.#isNativeSubmitControl(target)) return;
|
|
332
332
|
this.#enterControl(session, target);
|
|
333
333
|
}
|
|
334
|
-
/**
|
|
334
|
+
/**
|
|
335
|
+
* Ends one session, restores owned state, and reports the observable completion.
|
|
336
|
+
*
|
|
337
|
+
* @stimeoRuntimeOnly `announceReadyText` words the one announcement of this finished submission
|
|
338
|
+
* and `restoreFocus` decides where its focus returns.
|
|
339
|
+
*/
|
|
335
340
|
#complete(form, reason, success) {
|
|
336
341
|
const session = this.#sessionFor(form);
|
|
337
342
|
if (!session) return;
|
|
@@ -353,7 +358,12 @@ var SubmitOnceController = class extends Controller {
|
|
|
353
358
|
this.#disabled.write(control, "");
|
|
354
359
|
this.#controlBusy.write(control, "true");
|
|
355
360
|
}
|
|
356
|
-
/**
|
|
361
|
+
/**
|
|
362
|
+
* Swaps only the triggering control's safe label channel.
|
|
363
|
+
*
|
|
364
|
+
* @stimeoRuntimeOnly `busyLabel` is the label this one submission shows until it ends; the idle
|
|
365
|
+
* label comes back with the state it returns.
|
|
366
|
+
*/
|
|
357
367
|
#enterLabel(session) {
|
|
358
368
|
const control = session.submitter;
|
|
359
369
|
if (!control) return;
|
|
@@ -440,7 +450,10 @@ var SubmitOnceController = class extends Controller {
|
|
|
440
450
|
}
|
|
441
451
|
this.#sessions.clear();
|
|
442
452
|
}
|
|
443
|
-
/**
|
|
453
|
+
/**
|
|
454
|
+
* Keeps Turbo's cached snapshot idle without `end`, announcements, or focus
|
|
455
|
+
* moves; the abandoned forms are reported once as `reconcile`.
|
|
456
|
+
*/
|
|
444
457
|
#rewindForCache() {
|
|
445
458
|
const forms = [...this.#sessions.keys()];
|
|
446
459
|
this.#abandonSessions();
|
|
@@ -9,6 +9,16 @@ function setDefaultAttribute(element, name, value) {
|
|
|
9
9
|
return true;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
// src/utils/field_mirror.ts
|
|
13
|
+
function writeField(field, value) {
|
|
14
|
+
if (field.value === value) return false;
|
|
15
|
+
field.value = value;
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
function commitField(target) {
|
|
19
|
+
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
20
|
+
}
|
|
21
|
+
|
|
12
22
|
// src/utils/focus_candidate.ts
|
|
13
23
|
function inheritsFieldsetDisabled(control) {
|
|
14
24
|
let fieldset = control.closest("fieldset[disabled]");
|
|
@@ -37,6 +47,39 @@ function isInteractiveHost(element) {
|
|
|
37
47
|
return false;
|
|
38
48
|
}
|
|
39
49
|
|
|
50
|
+
// src/utils/microtask_coalescer.ts
|
|
51
|
+
var MicrotaskCoalescer = class {
|
|
52
|
+
#run;
|
|
53
|
+
#queued = false;
|
|
54
|
+
#active = false;
|
|
55
|
+
#generation = 0;
|
|
56
|
+
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
57
|
+
constructor(run) {
|
|
58
|
+
this.#run = run;
|
|
59
|
+
}
|
|
60
|
+
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
61
|
+
activate() {
|
|
62
|
+
this.#active = true;
|
|
63
|
+
}
|
|
64
|
+
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
65
|
+
cancel() {
|
|
66
|
+
this.#active = false;
|
|
67
|
+
this.#queued = false;
|
|
68
|
+
this.#generation += 1;
|
|
69
|
+
}
|
|
70
|
+
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
71
|
+
schedule() {
|
|
72
|
+
if (!this.#active || this.#queued) return;
|
|
73
|
+
this.#queued = true;
|
|
74
|
+
const generation = this.#generation;
|
|
75
|
+
queueMicrotask(() => {
|
|
76
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
77
|
+
this.#queued = false;
|
|
78
|
+
this.#run();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
40
83
|
// src/controllers/switch_controller.ts
|
|
41
84
|
var OBSERVED_ATTRIBUTES = [
|
|
42
85
|
"role",
|
|
@@ -49,17 +92,20 @@ var OBSERVED_ATTRIBUTES = [
|
|
|
49
92
|
];
|
|
50
93
|
var OBSERVED_ANCESTOR_ATTRIBUTES = ["contenteditable"];
|
|
51
94
|
var SwitchController = class extends Controller {
|
|
95
|
+
static targets = ["field"];
|
|
52
96
|
static actions = ["onKeydown", "toggle"];
|
|
53
|
-
static events = ["
|
|
97
|
+
static events = ["change", "reconcile"];
|
|
54
98
|
/** Defaults this instance introduced and may therefore remove safely. */
|
|
55
99
|
#ownedDefaults = /* @__PURE__ */ new Set();
|
|
56
100
|
/** Controller writes that must not be mistaken for authored morph changes. */
|
|
57
101
|
#internalAttributeValues = /* @__PURE__ */ new Map();
|
|
102
|
+
/** One pass per batch of retained-element changes and field arrivals. */
|
|
103
|
+
#pass = new MicrotaskCoalescer(() => this.#reconcile());
|
|
104
|
+
/** The checked state last published: read on connect, then committed or reported. */
|
|
105
|
+
#committedChecked = false;
|
|
58
106
|
#attributeObserver = new MutationObserver((records) => {
|
|
59
107
|
this.#releaseAuthoredDefaults(records);
|
|
60
|
-
this.#
|
|
61
|
-
this.#reconcileDefaults();
|
|
62
|
-
this.#observeAttributes();
|
|
108
|
+
this.#pass.schedule();
|
|
63
109
|
});
|
|
64
110
|
/** Blocks disabled pointer/native-key activation before consumer click handlers. */
|
|
65
111
|
#onClickCapture = (event) => {
|
|
@@ -67,14 +113,25 @@ var SwitchController = class extends Controller {
|
|
|
67
113
|
event.preventDefault();
|
|
68
114
|
event.stopImmediatePropagation();
|
|
69
115
|
};
|
|
70
|
-
/**
|
|
116
|
+
/**
|
|
117
|
+
* Ensures the switch exposes a role and is keyboard-reachable, and takes the
|
|
118
|
+
* state it finds as the one a later move is measured from, reporting nothing.
|
|
119
|
+
*/
|
|
71
120
|
connect() {
|
|
72
121
|
this.#reconcileDefaults();
|
|
122
|
+
this.#mirrorField(false);
|
|
123
|
+
this.#committedChecked = this.#checked;
|
|
73
124
|
this.element.addEventListener("click", this.#onClickCapture, true);
|
|
125
|
+
this.#pass.activate();
|
|
74
126
|
this.#observeAttributes();
|
|
75
127
|
}
|
|
76
|
-
/**
|
|
128
|
+
/** Fills a form field inserted or replaced at runtime with the current state. */
|
|
129
|
+
fieldTargetConnected() {
|
|
130
|
+
this.#pass.schedule();
|
|
131
|
+
}
|
|
132
|
+
/** Releases the click guard, the attribute observer, and a pass still queued. */
|
|
77
133
|
disconnect() {
|
|
134
|
+
this.#pass.cancel();
|
|
78
135
|
this.element.removeEventListener("click", this.#onClickCapture, true);
|
|
79
136
|
this.#attributeObserver.disconnect();
|
|
80
137
|
this.#internalAttributeValues.clear();
|
|
@@ -82,7 +139,7 @@ var SwitchController = class extends Controller {
|
|
|
82
139
|
/** Toggles the checked state. Bound via `data-action` (click). */
|
|
83
140
|
toggle() {
|
|
84
141
|
if (!this.#isSupportedHost || this.#isActivationDisabled) return;
|
|
85
|
-
this.#
|
|
142
|
+
this.#commit(!this.#checked);
|
|
86
143
|
}
|
|
87
144
|
/**
|
|
88
145
|
* Activates the switch on Space/Enter for non-native hosts and suppresses key
|
|
@@ -107,6 +164,28 @@ var SwitchController = class extends Controller {
|
|
|
107
164
|
if (event.repeat) return;
|
|
108
165
|
this.toggle();
|
|
109
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Re-derives the defaults and the field after the page changed the host, then
|
|
169
|
+
* reports a checked state that moved. Records still queued are read first, so
|
|
170
|
+
* an author's write is not lost when observation is suspended for this pass's
|
|
171
|
+
* own writes; observation resumes before the report, so an edit a listener
|
|
172
|
+
* makes is seen by the next pass.
|
|
173
|
+
*/
|
|
174
|
+
#reconcile() {
|
|
175
|
+
this.#releaseAuthoredDefaults(this.#attributeObserver.takeRecords());
|
|
176
|
+
this.#attributeObserver.disconnect();
|
|
177
|
+
this.#reconcileDefaults();
|
|
178
|
+
this.#mirrorField(false);
|
|
179
|
+
this.#observeAttributes();
|
|
180
|
+
this.#reportMove();
|
|
181
|
+
}
|
|
182
|
+
/** Reports a checked state that moved since the one this switch last published. */
|
|
183
|
+
#reportMove() {
|
|
184
|
+
const checked = this.#checked;
|
|
185
|
+
if (checked === this.#committedChecked) return;
|
|
186
|
+
this.#committedChecked = checked;
|
|
187
|
+
this.dispatch("reconcile", { detail: { checked } });
|
|
188
|
+
}
|
|
110
189
|
/** Re-adds only missing defaults, preserving every authored attribute value. */
|
|
111
190
|
#reconcileDefaults() {
|
|
112
191
|
if (!this.#isSupportedHost) {
|
|
@@ -119,6 +198,12 @@ var SwitchController = class extends Controller {
|
|
|
119
198
|
this.#setOwnedDefault("tabindex", "0");
|
|
120
199
|
}
|
|
121
200
|
}
|
|
201
|
+
/** Mirrors the checked state into the optional form field. */
|
|
202
|
+
#mirrorField(notify) {
|
|
203
|
+
if (!this.hasFieldTarget) return;
|
|
204
|
+
const moved = writeField(this.fieldTarget, this.#checked ? "true" : "false");
|
|
205
|
+
if (moved && notify) commitField(this.fieldTarget);
|
|
206
|
+
}
|
|
122
207
|
/** Records a newly introduced default without claiming authored markup. */
|
|
123
208
|
#setOwnedDefault(name, value) {
|
|
124
209
|
if (setDefaultAttribute(this.element, name, value)) this.#ownedDefaults.add(name);
|
|
@@ -181,12 +266,18 @@ var SwitchController = class extends Controller {
|
|
|
181
266
|
get #checked() {
|
|
182
267
|
return this.element.getAttribute("aria-checked") === "true";
|
|
183
268
|
}
|
|
184
|
-
/**
|
|
185
|
-
|
|
269
|
+
/**
|
|
270
|
+
* Applies one toggle the user made. The state is taken as published before
|
|
271
|
+
* either report goes out, so a listener that toggles again or writes the
|
|
272
|
+
* attribute is measured from it.
|
|
273
|
+
*/
|
|
274
|
+
#commit(value) {
|
|
186
275
|
const reflected = value ? "true" : "false";
|
|
187
276
|
this.#internalAttributeValues.set("aria-checked", reflected);
|
|
188
277
|
this.element.setAttribute("aria-checked", reflected);
|
|
189
|
-
this
|
|
278
|
+
this.#committedChecked = value;
|
|
279
|
+
this.#mirrorField(true);
|
|
280
|
+
this.dispatch("change", { detail: { checked: value } });
|
|
190
281
|
}
|
|
191
282
|
};
|
|
192
283
|
|
|
@@ -17,6 +17,9 @@ function isReservedArrowChord(event, allow = []) {
|
|
|
17
17
|
var TabsController = class extends Controller {
|
|
18
18
|
static targets = ["tab", "panel", "list"];
|
|
19
19
|
static actions = ["onKeydown", "select"];
|
|
20
|
+
static events = ["change"];
|
|
21
|
+
/** Whether selection moves are reported: set once `connect()` settled the baseline. */
|
|
22
|
+
#reporting = false;
|
|
20
23
|
/**
|
|
21
24
|
* Selects the initially active tab: the pre-selected one, else the first.
|
|
22
25
|
*
|
|
@@ -30,6 +33,14 @@ var TabsController = class extends Controller {
|
|
|
30
33
|
(tab) => tab.getAttribute("aria-selected") === "true"
|
|
31
34
|
);
|
|
32
35
|
this.#selectIndex(preselected === -1 ? 0 : preselected, { focus: false });
|
|
36
|
+
this.#reporting = true;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Stops reporting until the next `connect()` has settled its baseline, so that
|
|
40
|
+
* normalization stays silent.
|
|
41
|
+
*/
|
|
42
|
+
disconnect() {
|
|
43
|
+
this.#reporting = false;
|
|
33
44
|
}
|
|
34
45
|
/** Selects the clicked tab. Bound via `data-action` (click). */
|
|
35
46
|
select(event) {
|
|
@@ -69,7 +80,9 @@ var TabsController = class extends Controller {
|
|
|
69
80
|
* `tabindex`, and panel visibility. Optionally moves focus to the new tab.
|
|
70
81
|
*/
|
|
71
82
|
#selectIndex(index, { focus }) {
|
|
72
|
-
this.tabTargets
|
|
83
|
+
const tabs = this.tabTargets;
|
|
84
|
+
const previous = tabs.findIndex((tab) => tab.getAttribute("aria-selected") === "true");
|
|
85
|
+
tabs.forEach((tab, i) => {
|
|
73
86
|
const selected = i === index;
|
|
74
87
|
tab.setAttribute("aria-selected", selected ? "true" : "false");
|
|
75
88
|
tab.tabIndex = selected ? 0 : -1;
|
|
@@ -77,7 +90,13 @@ var TabsController = class extends Controller {
|
|
|
77
90
|
this.panelTargets.forEach((panel, i) => {
|
|
78
91
|
panel.hidden = i !== index;
|
|
79
92
|
});
|
|
80
|
-
if (
|
|
93
|
+
if (previous !== index && this.#reporting) {
|
|
94
|
+
this.dispatch("change", {
|
|
95
|
+
detail: { index, total: tabs.length, previous },
|
|
96
|
+
cancelable: false
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (focus && tabs[index]?.getAttribute("aria-selected") === "true") tabs[index]?.focus();
|
|
81
100
|
}
|
|
82
101
|
};
|
|
83
102
|
|