stimeo-ui 0.16.0 → 0.17.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 +31 -0
- data/README.md +8 -1
- data/dist/cable/index.js +363 -436
- data/dist/controllers/accordion_controller.js +166 -55
- data/dist/controllers/alert_dialog_controller.js +862 -221
- data/dist/controllers/announcer_controller.js +135 -198
- data/dist/controllers/aspect_ratio_controller.js +0 -10
- data/dist/controllers/auto_submit_controller.js +271 -127
- data/dist/controllers/avatar_controller.js +171 -105
- data/dist/controllers/breadcrumb_controller.js +0 -130
- data/dist/controllers/bulk_select_controller.js +262 -52
- data/dist/controllers/calendar_controller.js +175 -209
- data/dist/controllers/carousel_controller.js +461 -399
- data/dist/controllers/character_counter_controller.js +246 -156
- data/dist/controllers/checkbox_controller.js +32 -74
- data/dist/controllers/clipboard_controller.js +168 -147
- data/dist/controllers/collapsible_controller.js +301 -120
- data/dist/controllers/color_picker_controller.js +149 -139
- data/dist/controllers/combobox_controller.js +360 -133
- data/dist/controllers/command_palette_controller.js +970 -358
- data/dist/controllers/conditional_fields_controller.js +166 -124
- data/dist/controllers/confirm_controller.js +939 -248
- data/dist/controllers/context_menu_controller.js +256 -128
- data/dist/controllers/count_up_controller.js +73 -21
- data/dist/controllers/countdown_controller.js +183 -148
- data/dist/controllers/currency_input_controller.js +134 -158
- data/dist/controllers/data_grid_controller.js +114 -186
- data/dist/controllers/date_range_picker_controller.js +288 -206
- data/dist/controllers/dialog_controller.js +858 -223
- data/dist/controllers/direct_upload_controller.js +73 -153
- data/dist/controllers/dirty_form_controller.js +14 -48
- data/dist/controllers/dismissible_controller.js +175 -21
- data/dist/controllers/drawer_controller.js +933 -341
- data/dist/controllers/dropdown_controller.js +312 -82
- data/dist/controllers/editable_controller.js +158 -68
- data/dist/controllers/empty_state_controller.js +67 -53
- data/dist/controllers/file_dropzone_controller.js +311 -207
- data/dist/controllers/filter_controller.js +67 -39
- data/dist/controllers/flash_controller.js +172 -314
- data/dist/controllers/focus_controller.js +654 -240
- data/dist/controllers/form_field_controller.js +147 -132
- data/dist/controllers/form_validation_controller.js +13 -86
- data/dist/controllers/frame_loading_controller.js +264 -213
- data/dist/controllers/highlight_controller.js +107 -75
- data/dist/controllers/hover_card_controller.js +309 -118
- data/dist/controllers/idle_controller.js +267 -101
- data/dist/controllers/input_mask_controller.js +71 -72
- data/dist/controllers/intersection_controller.js +147 -106
- data/dist/controllers/lazy_frame_controller.js +9 -65
- data/dist/controllers/listbox_controller.js +306 -204
- data/dist/controllers/local_time_controller.js +54 -64
- data/dist/controllers/masonry_controller.js +70 -93
- data/dist/controllers/menu_controller.js +214 -156
- data/dist/controllers/menubar_controller.js +170 -329
- data/dist/controllers/meter_controller.js +151 -67
- data/dist/controllers/multi_select_controller.js +593 -358
- data/dist/controllers/navigation_menu_controller.js +124 -232
- data/dist/controllers/nested_form_controller.js +352 -182
- data/dist/controllers/network_status_controller.js +242 -54
- data/dist/controllers/number_input_controller.js +320 -263
- data/dist/controllers/optimistic_controller.js +16 -76
- data/dist/controllers/otp_controller.js +198 -193
- data/dist/controllers/overflow_indicator_controller.js +116 -113
- data/dist/controllers/overflow_menu_controller.js +343 -283
- data/dist/controllers/pagination_controller.js +295 -119
- data/dist/controllers/password_reveal_controller.js +389 -121
- data/dist/controllers/password_strength_controller.js +99 -153
- data/dist/controllers/persist_controller.js +237 -103
- data/dist/controllers/pointer_drag_controller.js +185 -165
- data/dist/controllers/popover_controller.js +236 -97
- data/dist/controllers/portal_controller.js +209 -76
- data/dist/controllers/preview_guard_controller.js +232 -116
- data/dist/controllers/progress_controller.js +125 -56
- data/dist/controllers/radio_group_controller.js +117 -137
- data/dist/controllers/range_slider_controller.js +192 -105
- data/dist/controllers/rating_controller.js +418 -207
- data/dist/controllers/read_more_controller.js +134 -63
- data/dist/controllers/reading_progress_controller.js +147 -135
- data/dist/controllers/relative_time_controller.js +145 -110
- data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
- data/dist/controllers/resizable_controller.js +255 -139
- data/dist/controllers/roving_controller.js +39 -84
- data/dist/controllers/scroll_area_controller.js +428 -225
- data/dist/controllers/scroll_restore_controller.js +0 -53
- data/dist/controllers/scroll_visibility_controller.js +395 -145
- data/dist/controllers/scrollspy_controller.js +181 -242
- data/dist/controllers/separator_controller.js +267 -140
- data/dist/controllers/sidebar_controller.js +1023 -386
- data/dist/controllers/skeleton_controller.js +248 -97
- data/dist/controllers/slider_controller.js +159 -79
- data/dist/controllers/smart_sticky_header_controller.js +162 -63
- data/dist/controllers/sortable_controller.js +0 -112
- data/dist/controllers/spinner_controller.js +277 -169
- data/dist/controllers/step_indicator_controller.js +99 -46
- data/dist/controllers/stepper_controller.js +114 -73
- data/dist/controllers/stick_to_bottom_controller.js +262 -138
- data/dist/controllers/sticky_observer_controller.js +214 -49
- data/dist/controllers/submit_once_controller.js +294 -205
- data/dist/controllers/switch_controller.js +33 -60
- data/dist/controllers/tabs_controller.js +35 -29
- data/dist/controllers/tags_input_controller.js +136 -145
- data/dist/controllers/textarea_autosize_controller.js +71 -58
- data/dist/controllers/theme_controller.js +130 -112
- data/dist/controllers/time_picker_controller.js +146 -80
- data/dist/controllers/toast_controller.js +186 -297
- data/dist/controllers/toggle_group_controller.js +95 -136
- data/dist/controllers/toolbar_controller.js +68 -98
- data/dist/controllers/tooltip_controller.js +158 -124
- data/dist/controllers/transition_controller.js +123 -119
- data/dist/controllers/tree_view_controller.js +150 -345
- data/dist/index.js +6370 -9346
- data/dist/positioning/index.js +148 -47
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +3 -3
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
|
|
4
|
+
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
5
|
+
function stateReasonFor(event) {
|
|
6
|
+
if (!event) return "api";
|
|
7
|
+
if (FOCUS_EVENTS.has(event.type)) return "focus";
|
|
8
|
+
if (POINTER_EVENTS.has(event.type)) return "pointer";
|
|
9
|
+
return "user";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function actionSource(source) {
|
|
13
|
+
if (source instanceof HTMLElement) {
|
|
14
|
+
return { event: null, host: source, origin: source, reason: "api" };
|
|
15
|
+
}
|
|
16
|
+
const event = source ?? null;
|
|
17
|
+
return {
|
|
18
|
+
event,
|
|
19
|
+
host: event?.currentTarget instanceof Element ? event.currentTarget : null,
|
|
20
|
+
origin: event?.target instanceof Element ? event.target : null,
|
|
21
|
+
reason: stateReasonFor(event)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
4
24
|
|
|
5
|
-
// src/utils/logical_scroll.ts
|
|
6
25
|
function isRtl(element) {
|
|
7
26
|
return window.getComputedStyle(element).direction === "rtl";
|
|
8
27
|
}
|
|
9
28
|
|
|
10
|
-
// src/utils/arrow_step.ts
|
|
11
29
|
function logicalArrowStep(key, element) {
|
|
12
30
|
if (key === "ArrowDown") return 1;
|
|
13
31
|
if (key === "ArrowUp") return -1;
|
|
@@ -20,7 +38,6 @@ function isReservedArrowChord(event, allow = []) {
|
|
|
20
38
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
21
39
|
}
|
|
22
40
|
|
|
23
|
-
// src/utils/event_owner.ts
|
|
24
41
|
function ownerIndex(candidates, node) {
|
|
25
42
|
if (!(node instanceof Node)) return -1;
|
|
26
43
|
return candidates.findIndex((candidate) => candidate.contains(node));
|
|
@@ -29,7 +46,6 @@ function ownerOf(candidates, node) {
|
|
|
29
46
|
return candidates[ownerIndex(candidates, node)] ?? null;
|
|
30
47
|
}
|
|
31
48
|
|
|
32
|
-
// src/utils/field_mirror.ts
|
|
33
49
|
function writeFields(container, values, { name, form = "" }) {
|
|
34
50
|
const current = [...container.children];
|
|
35
51
|
const submits = current.length === values.length && current.every(
|
|
@@ -52,7 +68,6 @@ function commitField(target) {
|
|
|
52
68
|
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
53
69
|
}
|
|
54
70
|
|
|
55
|
-
// src/utils/focus_candidate.ts
|
|
56
71
|
function inheritsFieldsetDisabled(control) {
|
|
57
72
|
let fieldset = control.closest("fieldset[disabled]");
|
|
58
73
|
while (fieldset) {
|
|
@@ -63,7 +78,6 @@ function inheritsFieldsetDisabled(control) {
|
|
|
63
78
|
return false;
|
|
64
79
|
}
|
|
65
80
|
|
|
66
|
-
// src/utils/interactive_host.ts
|
|
67
81
|
var INTERACTIVE_HOST_SELECTOR = "button, input, select, textarea, label, a[href], area[href], summary, details, audio[controls], video[controls], iframe, object, embed";
|
|
68
82
|
function isInteractiveHost(element) {
|
|
69
83
|
if (element.matches(INTERACTIVE_HOST_SELECTOR)) return true;
|
|
@@ -80,27 +94,33 @@ function isInteractiveHost(element) {
|
|
|
80
94
|
return false;
|
|
81
95
|
}
|
|
82
96
|
|
|
83
|
-
|
|
97
|
+
var ListenerSet = class {
|
|
98
|
+
#abort = new AbortController();
|
|
99
|
+
add(target, type, handler, options) {
|
|
100
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
101
|
+
}
|
|
102
|
+
dispose() {
|
|
103
|
+
this.#abort.abort();
|
|
104
|
+
this.#abort = new AbortController();
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
84
108
|
var MicrotaskCoalescer = class {
|
|
85
109
|
#run;
|
|
86
110
|
#queued = false;
|
|
87
111
|
#active = false;
|
|
88
112
|
#generation = 0;
|
|
89
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
90
113
|
constructor(run) {
|
|
91
114
|
this.#run = run;
|
|
92
115
|
}
|
|
93
|
-
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
94
116
|
activate() {
|
|
95
117
|
this.#active = true;
|
|
96
118
|
}
|
|
97
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
98
119
|
cancel() {
|
|
99
120
|
this.#active = false;
|
|
100
121
|
this.#queued = false;
|
|
101
122
|
this.#generation += 1;
|
|
102
123
|
}
|
|
103
|
-
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
104
124
|
schedule() {
|
|
105
125
|
if (!this.#active || this.#queued) return;
|
|
106
126
|
this.#queued = true;
|
|
@@ -113,31 +133,50 @@ var MicrotaskCoalescer = class {
|
|
|
113
133
|
}
|
|
114
134
|
};
|
|
115
135
|
|
|
116
|
-
|
|
136
|
+
var MorphRenderWatcher = class {
|
|
137
|
+
#listeners = new ListenerSet();
|
|
138
|
+
#pass;
|
|
139
|
+
#element = null;
|
|
140
|
+
#onMorph = () => this.#pass.schedule();
|
|
141
|
+
constructor(run) {
|
|
142
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
143
|
+
}
|
|
144
|
+
observe(element) {
|
|
145
|
+
if (this.#element === element) return;
|
|
146
|
+
this.disconnect();
|
|
147
|
+
this.#element = element;
|
|
148
|
+
this.#pass.activate();
|
|
149
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
150
|
+
}
|
|
151
|
+
schedule() {
|
|
152
|
+
this.#pass.schedule();
|
|
153
|
+
}
|
|
154
|
+
disconnect() {
|
|
155
|
+
this.#listeners.dispose();
|
|
156
|
+
this.#pass.cancel();
|
|
157
|
+
this.#element = null;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
var MoveCounter = class {
|
|
162
|
+
#sequence = 0;
|
|
163
|
+
record() {
|
|
164
|
+
this.#sequence += 1;
|
|
165
|
+
return this.#sequence;
|
|
166
|
+
}
|
|
167
|
+
isLatest(token) {
|
|
168
|
+
return token === this.#sequence;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
117
172
|
var RovingTabindex = class {
|
|
118
|
-
/** Returns the current ordered item elements; called on every operation. */
|
|
119
173
|
#getItems;
|
|
120
|
-
/**
|
|
121
|
-
* @param getItems - Returns the current ordered item elements. Called on every
|
|
122
|
-
* operation so the live target list is always used.
|
|
123
|
-
*/
|
|
124
174
|
constructor(getItems) {
|
|
125
175
|
this.#getItems = getItems;
|
|
126
176
|
}
|
|
127
|
-
/** Index of the currently tabbable item (`tabindex="0"`), or `-1` if none. */
|
|
128
177
|
get activeIndex() {
|
|
129
178
|
return this.#getItems().findIndex((item) => item.tabIndex === 0);
|
|
130
179
|
}
|
|
131
|
-
/**
|
|
132
|
-
* Makes exactly the item at `index` tabbable (`tabindex="0"`) and removes every
|
|
133
|
-
* other item from the Tab sequence (`tabindex="-1"`). An out-of-range `index`
|
|
134
|
-
* (e.g. `-1`) leaves all items at `-1`, which a controller can use to express
|
|
135
|
-
* "nothing is currently tabbable".
|
|
136
|
-
*
|
|
137
|
-
* @param index - Position of the item to make tabbable.
|
|
138
|
-
* @param options - Pass `{ focus: true }` to also move DOM focus to that item,
|
|
139
|
-
* and `items` to reuse an event-scoped collection snapshot.
|
|
140
|
-
*/
|
|
141
180
|
setActive(index, options = {}) {
|
|
142
181
|
const { focus = false } = options;
|
|
143
182
|
const items = options.items ?? this.#getItems();
|
|
@@ -153,7 +192,10 @@ function rovingMove(current, length, delta, wrap) {
|
|
|
153
192
|
return (next + length) % length;
|
|
154
193
|
}
|
|
155
194
|
|
|
156
|
-
|
|
195
|
+
function targetSelector(identifier, name) {
|
|
196
|
+
return `[data-${identifier}-target~="${name}"]`;
|
|
197
|
+
}
|
|
198
|
+
|
|
157
199
|
var ITEM_ATTRIBUTES = [
|
|
158
200
|
"aria-pressed",
|
|
159
201
|
"data-value",
|
|
@@ -166,6 +208,7 @@ var ITEM_ATTRIBUTES = [
|
|
|
166
208
|
];
|
|
167
209
|
var hasModifier = (event) => event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
|
|
168
210
|
var ToggleGroupController = class extends Controller {
|
|
211
|
+
#moves = new MoveCounter();
|
|
169
212
|
static targets = ["item", "fields"];
|
|
170
213
|
static values = {
|
|
171
214
|
mode: { type: String, default: "multiple" },
|
|
@@ -177,27 +220,13 @@ var ToggleGroupController = class extends Controller {
|
|
|
177
220
|
#roving = new RovingTabindex(() => this.#managedTargets);
|
|
178
221
|
#handledEvents = /* @__PURE__ */ new WeakSet();
|
|
179
222
|
#managedItems = /* @__PURE__ */ new Set();
|
|
180
|
-
#originalTabindex = /* @__PURE__ */ new
|
|
223
|
+
#originalTabindex = /* @__PURE__ */ new WeakMap();
|
|
181
224
|
#ownedPressed = /* @__PURE__ */ new Set();
|
|
182
225
|
#internalPressedValues = /* @__PURE__ */ new Map();
|
|
183
|
-
|
|
184
|
-
#pass = new MicrotaskCoalescer(() => this.#reconcileDom());
|
|
226
|
+
#pass = new MorphRenderWatcher(() => this.#reconcileDom());
|
|
185
227
|
#observer = null;
|
|
186
228
|
#connected = false;
|
|
187
|
-
/** The pressed set last published: read on connect, then committed or reported. */
|
|
188
229
|
#committedValues = [];
|
|
189
|
-
/**
|
|
190
|
-
* How many presses the user has committed, so a press whose reports are still
|
|
191
|
-
* going out can tell that a listener has committed a newer one meanwhile.
|
|
192
|
-
*/
|
|
193
|
-
#presses = 0;
|
|
194
|
-
/**
|
|
195
|
-
* Normalizes state and establishes one roving entry point. A single authored
|
|
196
|
-
* `tabindex="0"` survives reconnect; otherwise the first pressed, navigable
|
|
197
|
-
* item wins, falling back to the first navigable item. The pressed set it
|
|
198
|
-
* settles on is the one a later move is measured from; connecting reports
|
|
199
|
-
* nothing.
|
|
200
|
-
*/
|
|
201
230
|
connect() {
|
|
202
231
|
const currentItems = new Set(this.itemTargets);
|
|
203
232
|
for (const item of this.#managedItems) {
|
|
@@ -219,16 +248,12 @@ var ToggleGroupController = class extends Controller {
|
|
|
219
248
|
this.element.addEventListener("focusin", this.#onFocusin);
|
|
220
249
|
this.#connected = true;
|
|
221
250
|
this.#internalPressedValues.clear();
|
|
222
|
-
this.#pass.
|
|
251
|
+
this.#pass.observe(this.element);
|
|
223
252
|
this.#observeMutations();
|
|
224
253
|
}
|
|
225
|
-
/**
|
|
226
|
-
* Releases every listener, the observer, and a pass still queued, while
|
|
227
|
-
* retaining DOM state for Turbo cache/reconnect.
|
|
228
|
-
*/
|
|
229
254
|
disconnect() {
|
|
230
255
|
this.#connected = false;
|
|
231
|
-
this.#pass.
|
|
256
|
+
this.#pass.disconnect();
|
|
232
257
|
this.element.removeEventListener("click", this.#onClickCapture, true);
|
|
233
258
|
this.element.removeEventListener("keydown", this.#onKeydownCapture, true);
|
|
234
259
|
this.element.removeEventListener("click", this.#onClick);
|
|
@@ -236,60 +261,47 @@ var ToggleGroupController = class extends Controller {
|
|
|
236
261
|
this.element.removeEventListener("focusin", this.#onFocusin);
|
|
237
262
|
this.#observer?.disconnect();
|
|
238
263
|
this.#observer = null;
|
|
239
|
-
this.#internalPressedValues.clear();
|
|
240
264
|
}
|
|
241
|
-
/** Drops a newly connected item from the Tab sequence before the batch is reconciled. */
|
|
242
265
|
itemTargetConnected(item) {
|
|
243
266
|
if (!this.#connected) return;
|
|
244
267
|
this.#reconcileHost(item, true);
|
|
245
268
|
this.#pass.schedule();
|
|
246
269
|
}
|
|
247
|
-
/** Restores attributes owned only while an element is an item, then reconciles the batch. */
|
|
248
270
|
itemTargetDisconnected(item) {
|
|
249
271
|
if (!this.#connected) return;
|
|
250
272
|
this.#releaseItem(item);
|
|
251
273
|
this.#pass.schedule();
|
|
252
274
|
}
|
|
253
|
-
/** Reconciles the single-selection invariant when the Stimulus Value changes. */
|
|
254
275
|
modeValueChanged() {
|
|
255
276
|
this.#pass.schedule();
|
|
256
277
|
}
|
|
257
|
-
/** Rebuilds the submitted fields when the public name changes at runtime. */
|
|
258
278
|
nameValueChanged() {
|
|
259
279
|
this.#pass.schedule();
|
|
260
280
|
}
|
|
261
|
-
/** Repoints the submitted fields when the owning form changes at runtime. */
|
|
262
281
|
formValueChanged() {
|
|
263
282
|
this.#pass.schedule();
|
|
264
283
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
*/
|
|
269
|
-
toggle(event) {
|
|
270
|
-
if (event.defaultPrevented) return;
|
|
271
|
-
const item = event.currentTarget;
|
|
284
|
+
toggle(source) {
|
|
285
|
+
const { event, host, origin, reason } = actionSource(source);
|
|
286
|
+
const item = host?.closest(targetSelector(this.identifier, "item"));
|
|
272
287
|
if (!item || !this.itemTargets.includes(item)) return;
|
|
273
|
-
this
|
|
274
|
-
|
|
288
|
+
if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
|
|
289
|
+
return;
|
|
290
|
+
if (event?.defaultPrevented) return;
|
|
291
|
+
if (event) this.#handledEvents.add(event);
|
|
292
|
+
this.#toggleItem(item, reason);
|
|
275
293
|
}
|
|
276
|
-
/**
|
|
277
|
-
* Moves focus or activates the action's item. Per-item action wiring is
|
|
278
|
-
* optional because keydown is also delegated from the group.
|
|
279
|
-
*/
|
|
280
294
|
onKeydown(event) {
|
|
281
295
|
const item = event.currentTarget;
|
|
282
296
|
if (!item || !this.itemTargets.includes(item)) return;
|
|
283
297
|
this.#handleKeydown(event, item);
|
|
284
298
|
}
|
|
285
|
-
/** Blocks disabled activation before per-item or consumer event handlers run. */
|
|
286
299
|
#onClickCapture = (event) => {
|
|
287
300
|
const item = this.#itemForEventTarget(event.target);
|
|
288
301
|
if (!item || !this.#isSupportedHost(item) || !this.#isActivationDisabled(item)) return;
|
|
289
302
|
event.preventDefault();
|
|
290
303
|
event.stopImmediatePropagation();
|
|
291
304
|
};
|
|
292
|
-
/** Blocks disabled keyboard activation while leaving navigation keys available. */
|
|
293
305
|
#onKeydownCapture = (event) => {
|
|
294
306
|
if (event.key !== " " && event.key !== "Enter") return;
|
|
295
307
|
const item = this.#itemForEventTarget(event.target);
|
|
@@ -297,24 +309,20 @@ var ToggleGroupController = class extends Controller {
|
|
|
297
309
|
event.preventDefault();
|
|
298
310
|
event.stopImmediatePropagation();
|
|
299
311
|
};
|
|
300
|
-
/** Delegated click path for static and runtime-added items without actions. */
|
|
301
312
|
#onClick = (event) => {
|
|
302
313
|
if (this.#handledEvents.delete(event) || event.defaultPrevented) return;
|
|
303
314
|
const item = this.#itemForEventTarget(event.target);
|
|
304
315
|
if (item) this.#toggleItem(item);
|
|
305
316
|
};
|
|
306
|
-
/** Delegated keydown path for static and runtime-added items without actions. */
|
|
307
317
|
#onKeydown = (event) => {
|
|
308
318
|
const item = this.#itemForEventTarget(event.target);
|
|
309
319
|
if (item) this.#handleKeydown(event, item);
|
|
310
320
|
};
|
|
311
|
-
/** Keeps programmatic and pointer focus as the group's next Tab entry point. */
|
|
312
321
|
#onFocusin = (event) => {
|
|
313
322
|
const item = this.#itemForEventTarget(event.target);
|
|
314
323
|
if (!item || !this.#isNavigable(item)) return;
|
|
315
324
|
this.#setActive(item);
|
|
316
325
|
};
|
|
317
|
-
/** Applies the APG navigation and activation key map to one supported item. */
|
|
318
326
|
#handleKeydown(event, item) {
|
|
319
327
|
if (event.defaultPrevented || !this.#isSupportedHost(item)) return;
|
|
320
328
|
if (event.isComposing) return;
|
|
@@ -353,18 +361,7 @@ var ToggleGroupController = class extends Controller {
|
|
|
353
361
|
event.preventDefault();
|
|
354
362
|
if (destination) this.#setActive(destination, true);
|
|
355
363
|
}
|
|
356
|
-
|
|
357
|
-
* Applies one user toggle and dispatches the documented change detail. The set
|
|
358
|
-
* it leaves is taken as published before either report goes out, so a
|
|
359
|
-
* listener that presses again or writes `aria-pressed` is measured from it. A
|
|
360
|
-
* listener of the native `change` that presses again has had that newer press
|
|
361
|
-
* reported, so this press's `change` would describe a set that is gone and is
|
|
362
|
-
* not dispatched.
|
|
363
|
-
*
|
|
364
|
-
* @stimeoRuntimeOnly `mode` is the rule this one press follows; the lasting single-press
|
|
365
|
-
* normalisation is a render root of its own.
|
|
366
|
-
*/
|
|
367
|
-
#toggleItem(item) {
|
|
364
|
+
#toggleItem(item, reason = "user") {
|
|
368
365
|
if (!this.#isSupportedHost(item) || this.#isActivationDisabled(item)) return;
|
|
369
366
|
if (!this.#managedItems.has(item)) this.#reconcileHost(item, true);
|
|
370
367
|
const willPress = !this.#isPressed(item);
|
|
@@ -376,20 +373,20 @@ var ToggleGroupController = class extends Controller {
|
|
|
376
373
|
}
|
|
377
374
|
this.#setActive(item);
|
|
378
375
|
const values = this.#pressedValues();
|
|
376
|
+
const previous = this.#committedValues;
|
|
377
|
+
const changed = values.length !== previous.length || values.some((value, index) => value !== previous[index]);
|
|
379
378
|
this.#committedValues = values;
|
|
380
|
-
|
|
381
|
-
|
|
379
|
+
if (!changed) {
|
|
380
|
+
this.#mirrorFields(false);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const token = this.#moves.record();
|
|
382
384
|
this.#mirrorFields(true);
|
|
383
|
-
if (
|
|
385
|
+
if (!this.#moves.isLatest(token)) return;
|
|
384
386
|
this.dispatch("change", {
|
|
385
|
-
detail: { value: this.#itemValue(item), pressed: willPress, values: [...values] }
|
|
387
|
+
detail: { value: this.#itemValue(item), pressed: willPress, values: [...values], reason }
|
|
386
388
|
});
|
|
387
389
|
}
|
|
388
|
-
/**
|
|
389
|
-
* Mirrors the pressed values into the optional fields container.
|
|
390
|
-
*
|
|
391
|
-
* @stimeoRenderRoot
|
|
392
|
-
*/
|
|
393
390
|
#mirrorFields(notify) {
|
|
394
391
|
if (!this.hasFieldsTarget) return;
|
|
395
392
|
const options = { name: this.nameValue, form: this.formValue };
|
|
@@ -397,13 +394,6 @@ var ToggleGroupController = class extends Controller {
|
|
|
397
394
|
commitField(this.fieldsTarget);
|
|
398
395
|
}
|
|
399
396
|
}
|
|
400
|
-
/**
|
|
401
|
-
* Reconciles supported hosts, pressed state, the roving invariant, and the
|
|
402
|
-
* fields after the page changed the group, then reports a pressed set that
|
|
403
|
-
* moved. Records still queued are read first, so an author's write is not lost
|
|
404
|
-
* when observation is suspended for this pass's own writes; observation resumes
|
|
405
|
-
* before the report, so an edit a listener makes is seen by the next pass.
|
|
406
|
-
*/
|
|
407
397
|
#reconcileDom() {
|
|
408
398
|
const observer = this.#observer;
|
|
409
399
|
if (observer) {
|
|
@@ -418,7 +408,6 @@ var ToggleGroupController = class extends Controller {
|
|
|
418
408
|
if (observer) this.#observeWith(observer);
|
|
419
409
|
this.#reportMove();
|
|
420
410
|
}
|
|
421
|
-
/** Reports a pressed set that moved since the one this group last published. */
|
|
422
411
|
#reportMove() {
|
|
423
412
|
const values = this.#pressedValues();
|
|
424
413
|
const previous = this.#committedValues;
|
|
@@ -428,7 +417,6 @@ var ToggleGroupController = class extends Controller {
|
|
|
428
417
|
this.#committedValues = values;
|
|
429
418
|
this.dispatch("reconcile", { detail: { values: [...values] } });
|
|
430
419
|
}
|
|
431
|
-
/** Begins or ends ownership according to the item's current host semantics. */
|
|
432
420
|
#reconcileHost(item, dropFromTabSequence) {
|
|
433
421
|
if (!this.#isSupportedHost(item)) {
|
|
434
422
|
this.#releaseItem(item);
|
|
@@ -441,17 +429,13 @@ var ToggleGroupController = class extends Controller {
|
|
|
441
429
|
}
|
|
442
430
|
this.#normalizePressed(item);
|
|
443
431
|
}
|
|
444
|
-
/** Restores only defaults owned because an element belonged to this group. */
|
|
445
432
|
#releaseItem(item) {
|
|
446
433
|
if (!this.#managedItems.delete(item)) return;
|
|
447
434
|
const original = this.#originalTabindex.get(item);
|
|
448
435
|
if (original === null) item.removeAttribute("tabindex");
|
|
449
436
|
else if (original !== void 0) item.setAttribute("tabindex", original);
|
|
450
|
-
this.#originalTabindex.delete(item);
|
|
451
437
|
if (this.#ownedPressed.delete(item)) item.removeAttribute("aria-pressed");
|
|
452
|
-
this.#internalPressedValues.delete(item);
|
|
453
438
|
}
|
|
454
|
-
/** Supplies a missing pressed state and normalizes invalid ARIA tokens. */
|
|
455
439
|
#normalizePressed(item) {
|
|
456
440
|
const value = item.getAttribute("aria-pressed");
|
|
457
441
|
if (value === null) {
|
|
@@ -462,13 +446,7 @@ var ToggleGroupController = class extends Controller {
|
|
|
462
446
|
item.setAttribute("aria-pressed", "false");
|
|
463
447
|
}
|
|
464
448
|
}
|
|
465
|
-
/**
|
|
466
|
-
* Makes the first DOM-ordered pressed item the sole pressed item in single mode.
|
|
467
|
-
*
|
|
468
|
-
* @stimeoRenderRoot
|
|
469
|
-
*/
|
|
470
449
|
#normalizeSelection() {
|
|
471
|
-
for (const item of this.#managedTargets) this.#normalizePressed(item);
|
|
472
450
|
if (this.modeValue !== "single") return;
|
|
473
451
|
let found = false;
|
|
474
452
|
for (const item of this.#managedTargets) {
|
|
@@ -477,7 +455,6 @@ var ToggleGroupController = class extends Controller {
|
|
|
477
455
|
else this.#setPressed(item, false);
|
|
478
456
|
}
|
|
479
457
|
}
|
|
480
|
-
/** Keeps one navigable Tab stop, preferring current DOM state after initial connect. */
|
|
481
458
|
#ensureTabStop(initialPreference) {
|
|
482
459
|
const navigable = this.#navigableItems;
|
|
483
460
|
let active;
|
|
@@ -493,7 +470,6 @@ var ToggleGroupController = class extends Controller {
|
|
|
493
470
|
active ??= navigable[0];
|
|
494
471
|
this.#setActive(active ?? null);
|
|
495
472
|
}
|
|
496
|
-
/** Moves through the full item order until a navigable destination is found. */
|
|
497
473
|
#nextNavigable(fromIndex, delta) {
|
|
498
474
|
const items = this.#managedTargets;
|
|
499
475
|
let index = fromIndex;
|
|
@@ -504,31 +480,25 @@ var ToggleGroupController = class extends Controller {
|
|
|
504
480
|
}
|
|
505
481
|
return void 0;
|
|
506
482
|
}
|
|
507
|
-
/** Assigns the single roving Tab stop and optionally moves DOM focus. */
|
|
508
483
|
#setActive(item, focus = false) {
|
|
509
484
|
this.#roving.setActive(this.#managedTargets.indexOf(item), { focus });
|
|
510
485
|
}
|
|
511
|
-
/** Finds this group's item containing an event target, excluding nested groups. */
|
|
512
486
|
#itemForEventTarget(target) {
|
|
513
487
|
if (!this.#ownsEventTarget(target)) return null;
|
|
514
488
|
return ownerOf(this.itemTargets, target);
|
|
515
489
|
}
|
|
516
|
-
/** Whether the closest Toggle Group scope around a target is this instance. */
|
|
517
490
|
#ownsEventTarget(target) {
|
|
518
491
|
return target instanceof Element && target.closest(`[data-controller~="${this.identifier}"]`) === this.element;
|
|
519
492
|
}
|
|
520
|
-
/** Hosts whose activation model can be owned without conflicting native behavior. */
|
|
521
493
|
#isSupportedHost(item) {
|
|
522
494
|
if (item instanceof HTMLButtonElement) return item.type === "button";
|
|
523
495
|
return !isInteractiveHost(item);
|
|
524
496
|
}
|
|
525
|
-
/** Items eligible for roving focus (ARIA-disabled deliberately remains eligible). */
|
|
526
497
|
#isNavigable(item) {
|
|
527
498
|
if (!this.#isSupportedHost(item) || this.#isHidden(item)) return false;
|
|
528
499
|
if (!(item instanceof HTMLButtonElement)) return true;
|
|
529
500
|
return !item.disabled && !inheritsFieldsetDisabled(item);
|
|
530
501
|
}
|
|
531
|
-
/** Whether hidden applies on the path from an item up to the group root. */
|
|
532
502
|
#isHidden(item) {
|
|
533
503
|
let current = item;
|
|
534
504
|
while (current && current !== this.element) {
|
|
@@ -537,7 +507,6 @@ var ToggleGroupController = class extends Controller {
|
|
|
537
507
|
}
|
|
538
508
|
return false;
|
|
539
509
|
}
|
|
540
|
-
/** Whether ARIA, visibility, or native HTML semantics suppress activation. */
|
|
541
510
|
#isActivationDisabled(item) {
|
|
542
511
|
if (!this.#isNavigable(item)) return true;
|
|
543
512
|
let current = item;
|
|
@@ -547,7 +516,6 @@ var ToggleGroupController = class extends Controller {
|
|
|
547
516
|
}
|
|
548
517
|
return false;
|
|
549
518
|
}
|
|
550
|
-
/** Releases default ownership when a Turbo morph authors `aria-pressed`. */
|
|
551
519
|
#releaseAuthoredPressed(records) {
|
|
552
520
|
for (const record of records) {
|
|
553
521
|
if (record.attributeName !== "aria-pressed") continue;
|
|
@@ -559,7 +527,6 @@ var ToggleGroupController = class extends Controller {
|
|
|
559
527
|
}
|
|
560
528
|
this.#internalPressedValues.clear();
|
|
561
529
|
}
|
|
562
|
-
/** Watches target membership, retained item hosts/state, and inherited host/fieldset state. */
|
|
563
530
|
#observeMutations() {
|
|
564
531
|
const observer = new MutationObserver((records) => {
|
|
565
532
|
this.#releaseAuthoredPressed(records);
|
|
@@ -568,7 +535,6 @@ var ToggleGroupController = class extends Controller {
|
|
|
568
535
|
this.#observer = observer;
|
|
569
536
|
this.#observeWith(observer);
|
|
570
537
|
}
|
|
571
|
-
/** Registers every root and ancestor observation on one observer instance. */
|
|
572
538
|
#observeWith(observer) {
|
|
573
539
|
observer.observe(this.element, {
|
|
574
540
|
subtree: true,
|
|
@@ -585,34 +551,27 @@ var ToggleGroupController = class extends Controller {
|
|
|
585
551
|
ancestor = ancestor.parentElement;
|
|
586
552
|
}
|
|
587
553
|
}
|
|
588
|
-
/** Supported targets currently managed by the roving primitive. */
|
|
589
554
|
get #managedTargets() {
|
|
590
555
|
return this.itemTargets.filter((item) => this.#managedItems.has(item));
|
|
591
556
|
}
|
|
592
|
-
/** Managed items eligible for the roving Tab stop. */
|
|
593
557
|
get #navigableItems() {
|
|
594
558
|
return this.#managedTargets.filter((item) => this.#isNavigable(item));
|
|
595
559
|
}
|
|
596
|
-
/** Whether an item is currently pressed. */
|
|
597
560
|
#isPressed(item) {
|
|
598
561
|
return item.getAttribute("aria-pressed") === "true";
|
|
599
562
|
}
|
|
600
|
-
/** Reflects pressed state while distinguishing controller writes from authored morphs. */
|
|
601
563
|
#setPressed(item, pressed) {
|
|
602
564
|
const value = pressed ? "true" : "false";
|
|
603
565
|
this.#internalPressedValues.set(item, value);
|
|
604
566
|
item.setAttribute("aria-pressed", value);
|
|
605
567
|
}
|
|
606
|
-
/** The DOM-ordered values of every currently pressed managed item. */
|
|
607
568
|
#pressedValues() {
|
|
608
569
|
return this.#managedTargets.filter((item) => this.#isPressed(item)).map((item) => this.#itemValue(item));
|
|
609
570
|
}
|
|
610
|
-
/** An item's value (`data-value`, defaulting to empty). */
|
|
611
571
|
#itemValue(item) {
|
|
612
572
|
return item.getAttribute("data-value") ?? "";
|
|
613
573
|
}
|
|
614
574
|
};
|
|
615
575
|
|
|
616
576
|
export { ToggleGroupController };
|
|
617
|
-
//# sourceMappingURL=toggle_group_controller.js.map
|
|
618
577
|
//# sourceMappingURL=toggle_group_controller.js.map
|