stimeo-ui 0.7.0 → 0.9.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.
- package/CHANGELOG.md +151 -0
- package/dist/controllers/auto_submit_controller.d.ts +14 -3
- package/dist/controllers/auto_submit_controller.js +94 -16
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.d.ts +62 -15
- package/dist/controllers/bulk_select_controller.js +139 -28
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +81 -47
- package/dist/controllers/carousel_controller.js +451 -100
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.d.ts +48 -15
- package/dist/controllers/clipboard_controller.js +102 -20
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +37 -6
- package/dist/controllers/color_picker_controller.js +180 -43
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +39 -5
- package/dist/controllers/currency_input_controller.js +305 -74
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +25 -9
- package/dist/controllers/data_grid_controller.js +150 -24
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +3 -1
- package/dist/controllers/direct_upload_controller.js +12 -2
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/editable_controller.d.ts +34 -9
- package/dist/controllers/editable_controller.js +83 -30
- package/dist/controllers/editable_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +123 -29
- package/dist/controllers/file_dropzone_controller.js +386 -63
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/filter_controller.d.ts +15 -3
- package/dist/controllers/filter_controller.js +32 -1
- package/dist/controllers/filter_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +3 -1
- package/dist/controllers/flash_controller.js +3 -1
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +4 -2
- package/dist/controllers/frame_loading_controller.js +2 -1
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.d.ts +42 -16
- package/dist/controllers/input_mask_controller.js +251 -76
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +34 -5
- package/dist/controllers/masonry_controller.js +129 -17
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.d.ts +51 -14
- package/dist/controllers/nested_form_controller.js +450 -42
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/otp_controller.d.ts +64 -25
- package/dist/controllers/otp_controller.js +485 -114
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/reset_before_cache_controller.d.ts +25 -2
- package/dist/controllers/reset_before_cache_controller.js +51 -5
- package/dist/controllers/reset_before_cache_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.d.ts +23 -7
- package/dist/controllers/resizable_controller.js +128 -55
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +3 -2
- package/dist/controllers/spinner_controller.js +7 -5
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +4 -1
- package/dist/controllers/submit_once_controller.js +3 -1
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.d.ts +30 -10
- package/dist/controllers/textarea_autosize_controller.js +131 -3
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/index.js +2764 -923
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +8 -1
- package/dist/inspector/cli.js +5 -1
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +5 -1
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +24 -24
- package/dist/inspector/manifest.json +193 -30
- package/package.json +2 -2
|
@@ -8,6 +8,39 @@ function isReservedArrowChord(event, allow = []) {
|
|
|
8
8
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
// src/utils/microtask_coalescer.ts
|
|
12
|
+
var MicrotaskCoalescer = class {
|
|
13
|
+
#run;
|
|
14
|
+
#queued = false;
|
|
15
|
+
#active = false;
|
|
16
|
+
#generation = 0;
|
|
17
|
+
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
18
|
+
constructor(run) {
|
|
19
|
+
this.#run = run;
|
|
20
|
+
}
|
|
21
|
+
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
22
|
+
activate() {
|
|
23
|
+
this.#active = true;
|
|
24
|
+
}
|
|
25
|
+
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
26
|
+
cancel() {
|
|
27
|
+
this.#active = false;
|
|
28
|
+
this.#queued = false;
|
|
29
|
+
this.#generation += 1;
|
|
30
|
+
}
|
|
31
|
+
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
32
|
+
schedule() {
|
|
33
|
+
if (!this.#active || this.#queued) return;
|
|
34
|
+
this.#queued = true;
|
|
35
|
+
const generation = this.#generation;
|
|
36
|
+
queueMicrotask(() => {
|
|
37
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
38
|
+
this.#queued = false;
|
|
39
|
+
this.#run();
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
11
44
|
// src/utils/before_cache_reset.ts
|
|
12
45
|
var BeforeCacheReset = class _BeforeCacheReset {
|
|
13
46
|
/** Every subscribed instance, iterated by the one shared document listener. */
|
|
@@ -69,26 +102,34 @@ var TabindexLoan = class {
|
|
|
69
102
|
};
|
|
70
103
|
|
|
71
104
|
// src/controllers/resizable_controller.ts
|
|
105
|
+
var DEFAULT_MIN = 0;
|
|
106
|
+
var DEFAULT_MAX = 100;
|
|
107
|
+
var DEFAULT_VALUE = 50;
|
|
108
|
+
var DEFAULT_STEP = 1;
|
|
72
109
|
var ResizableController = class extends Controller {
|
|
73
110
|
static targets = ["primary", "secondary", "separator"];
|
|
74
111
|
static values = {
|
|
75
|
-
min: { type: Number, default:
|
|
76
|
-
max: { type: Number, default:
|
|
77
|
-
step: { type: Number, default:
|
|
78
|
-
value: { type: Number, default:
|
|
112
|
+
min: { type: Number, default: DEFAULT_MIN },
|
|
113
|
+
max: { type: Number, default: DEFAULT_MAX },
|
|
114
|
+
step: { type: Number, default: DEFAULT_STEP },
|
|
115
|
+
value: { type: Number, default: DEFAULT_VALUE }
|
|
79
116
|
};
|
|
80
117
|
static actions = ["onKeydown", "onPointerDown", "toggle"];
|
|
81
118
|
static events = ["change"];
|
|
82
|
-
/**
|
|
83
|
-
#valueBeforeCollapse =
|
|
119
|
+
/** Where the divider sat before the current collapse; `null` when unknown. */
|
|
120
|
+
#valueBeforeCollapse = null;
|
|
84
121
|
/** Aborts in-progress pointer-drag listeners when the drag ends or on teardown. */
|
|
85
122
|
#dragAbort = null;
|
|
86
123
|
/** `tabindex` lent to a pane so `F6` can put focus on it; panes carry none. */
|
|
87
124
|
#paneTabindex = new TabindexLoan();
|
|
88
125
|
/** Releases the pane cycle listener bound in {@link connect}. */
|
|
89
126
|
#cycleAbort = null;
|
|
127
|
+
/** Folds the range and position inputs of one morph batch into a single paint. */
|
|
128
|
+
#repaint = new MicrotaskCoalescer(() => this.#render());
|
|
90
129
|
connect() {
|
|
91
|
-
this
|
|
130
|
+
this.element.removeAttribute("data-dragging");
|
|
131
|
+
this.#repaint.activate();
|
|
132
|
+
this.#render();
|
|
92
133
|
this.#cycleAbort = new AbortController();
|
|
93
134
|
this.element.addEventListener("keydown", this.#onCycleKeydown, {
|
|
94
135
|
signal: this.#cycleAbort.signal
|
|
@@ -100,17 +141,19 @@ var ResizableController = class extends Controller {
|
|
|
100
141
|
this.#dragAbort = null;
|
|
101
142
|
this.#cycleAbort?.abort();
|
|
102
143
|
this.#cycleAbort = null;
|
|
144
|
+
this.#repaint.cancel();
|
|
103
145
|
this.#paneTabindex.returnAll();
|
|
146
|
+
this.element.removeAttribute("data-dragging");
|
|
104
147
|
}
|
|
105
148
|
/** Moves focus to the next pane on `F6`, wrapping; entering at the first one. */
|
|
106
149
|
#onCycleKeydown = (event) => {
|
|
107
150
|
if (event.key !== "F6") return;
|
|
151
|
+
if (event.isComposing) return;
|
|
108
152
|
if (event.defaultPrevented) return;
|
|
109
153
|
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
|
|
110
154
|
const panes = [];
|
|
111
155
|
if (this.hasPrimaryTarget) panes.push(this.primaryTarget);
|
|
112
156
|
if (this.hasSecondaryTarget) panes.push(this.secondaryTarget);
|
|
113
|
-
if (panes.length === 0) return;
|
|
114
157
|
const target = event.target;
|
|
115
158
|
const current = panes.findIndex((pane) => target instanceof Node && pane.contains(target));
|
|
116
159
|
const next = panes[(current + 1) % panes.length];
|
|
@@ -119,12 +162,21 @@ var ResizableController = class extends Controller {
|
|
|
119
162
|
this.#paneTabindex.lend(next);
|
|
120
163
|
next.focus();
|
|
121
164
|
};
|
|
122
|
-
/**
|
|
123
|
-
* Stimulus lifecycle callback when the valueValue changes.
|
|
124
|
-
* Keeps CSS fractions and ARIA status completely aligned.
|
|
125
|
-
*/
|
|
165
|
+
/** Re-renders when application code (or a Turbo morph) changes `value` at runtime. */
|
|
126
166
|
valueValueChanged() {
|
|
127
|
-
this.#
|
|
167
|
+
this.#repaint.schedule();
|
|
168
|
+
}
|
|
169
|
+
/** Re-renders when application code (or a Turbo morph) changes `min` at runtime. */
|
|
170
|
+
minValueChanged() {
|
|
171
|
+
this.#repaint.schedule();
|
|
172
|
+
}
|
|
173
|
+
/** Re-renders when application code (or a Turbo morph) changes `max` at runtime. */
|
|
174
|
+
maxValueChanged() {
|
|
175
|
+
this.#repaint.schedule();
|
|
176
|
+
}
|
|
177
|
+
/** Re-publishes range and position onto a separator swapped in after connect. */
|
|
178
|
+
separatorTargetConnected() {
|
|
179
|
+
this.#repaint.schedule();
|
|
128
180
|
}
|
|
129
181
|
/** Starts active pointer drag tracking and locks capture. */
|
|
130
182
|
onPointerDown(event) {
|
|
@@ -145,44 +197,44 @@ var ResizableController = class extends Controller {
|
|
|
145
197
|
onKeydown(event) {
|
|
146
198
|
if (isReservedArrowChord(event)) return;
|
|
147
199
|
if (!this.hasSeparatorTarget) return;
|
|
148
|
-
const
|
|
149
|
-
const isVertical =
|
|
200
|
+
const { min, max } = this.#range;
|
|
201
|
+
const isVertical = this.#isVertical;
|
|
150
202
|
let handled = true;
|
|
151
|
-
let nextValue = this
|
|
203
|
+
let nextValue = this.#position;
|
|
152
204
|
switch (event.key) {
|
|
153
205
|
case "ArrowLeft":
|
|
154
206
|
if (isVertical) {
|
|
155
|
-
nextValue -= this
|
|
207
|
+
nextValue -= this.#step;
|
|
156
208
|
} else {
|
|
157
209
|
handled = false;
|
|
158
210
|
}
|
|
159
211
|
break;
|
|
160
212
|
case "ArrowRight":
|
|
161
213
|
if (isVertical) {
|
|
162
|
-
nextValue += this
|
|
214
|
+
nextValue += this.#step;
|
|
163
215
|
} else {
|
|
164
216
|
handled = false;
|
|
165
217
|
}
|
|
166
218
|
break;
|
|
167
219
|
case "ArrowUp":
|
|
168
220
|
if (!isVertical) {
|
|
169
|
-
nextValue -= this
|
|
221
|
+
nextValue -= this.#step;
|
|
170
222
|
} else {
|
|
171
223
|
handled = false;
|
|
172
224
|
}
|
|
173
225
|
break;
|
|
174
226
|
case "ArrowDown":
|
|
175
227
|
if (!isVertical) {
|
|
176
|
-
nextValue += this
|
|
228
|
+
nextValue += this.#step;
|
|
177
229
|
} else {
|
|
178
230
|
handled = false;
|
|
179
231
|
}
|
|
180
232
|
break;
|
|
181
233
|
case "Home":
|
|
182
|
-
nextValue =
|
|
234
|
+
nextValue = min;
|
|
183
235
|
break;
|
|
184
236
|
case "End":
|
|
185
|
-
nextValue =
|
|
237
|
+
nextValue = max;
|
|
186
238
|
break;
|
|
187
239
|
case "Enter":
|
|
188
240
|
event.preventDefault();
|
|
@@ -194,65 +246,86 @@ var ResizableController = class extends Controller {
|
|
|
194
246
|
}
|
|
195
247
|
if (handled) {
|
|
196
248
|
event.preventDefault();
|
|
197
|
-
this.valueValue = Math.max(
|
|
198
|
-
this.#
|
|
249
|
+
this.valueValue = Math.max(min, Math.min(nextValue, max));
|
|
250
|
+
this.#render();
|
|
199
251
|
this.#dispatchChange();
|
|
200
252
|
}
|
|
201
253
|
}
|
|
202
|
-
/** Double-click or Enter to collapse
|
|
254
|
+
/** Double-click or Enter to collapse the primary pane, or put it back. */
|
|
203
255
|
toggle() {
|
|
204
|
-
const
|
|
205
|
-
if (this
|
|
206
|
-
this.#valueBeforeCollapse = this
|
|
207
|
-
this.valueValue =
|
|
256
|
+
const { min, max } = this.#range;
|
|
257
|
+
if (this.#position > min) {
|
|
258
|
+
this.#valueBeforeCollapse = this.#position;
|
|
259
|
+
this.valueValue = min;
|
|
208
260
|
} else {
|
|
209
|
-
this.valueValue = this.#valueBeforeCollapse
|
|
261
|
+
this.valueValue = this.#valueBeforeCollapse ?? max;
|
|
262
|
+
this.#valueBeforeCollapse = null;
|
|
210
263
|
}
|
|
211
|
-
this.#
|
|
264
|
+
this.#render();
|
|
212
265
|
this.#dispatchChange();
|
|
213
266
|
}
|
|
214
267
|
#onPointerMove = (event) => {
|
|
215
268
|
if (!this.hasSeparatorTarget) return;
|
|
216
269
|
const rect = this.element.getBoundingClientRect();
|
|
217
|
-
const
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
} else {
|
|
223
|
-
fraction = (event.clientY - rect.top) / rect.height;
|
|
224
|
-
}
|
|
225
|
-
fraction = Math.max(0, Math.min(fraction, 1));
|
|
226
|
-
const percent = Math.round(fraction * 100);
|
|
227
|
-
this.valueValue = Math.max(this.minValue, Math.min(percent, this.maxValue));
|
|
228
|
-
this.#clampAndSync();
|
|
270
|
+
const raw = this.#isVertical ? (event.clientX - rect.left) / rect.width : (event.clientY - rect.top) / rect.height;
|
|
271
|
+
const fraction = Number.isFinite(raw) ? Math.max(0, Math.min(raw, 1)) : 0;
|
|
272
|
+
const { min, max } = this.#range;
|
|
273
|
+
this.valueValue = Math.max(min, Math.min(Math.round(fraction * 100), max));
|
|
274
|
+
this.#render();
|
|
229
275
|
};
|
|
230
276
|
#onPointerUp = (event) => {
|
|
231
|
-
if (!this.hasSeparatorTarget) return;
|
|
232
|
-
const separator = this.separatorTarget;
|
|
233
|
-
separator.releasePointerCapture(event.pointerId);
|
|
234
277
|
this.element.removeAttribute("data-dragging");
|
|
235
278
|
this.#dragAbort?.abort();
|
|
236
279
|
this.#dragAbort = null;
|
|
280
|
+
if (this.hasSeparatorTarget) {
|
|
281
|
+
this.separatorTarget.releasePointerCapture(event.pointerId);
|
|
282
|
+
}
|
|
237
283
|
this.#dispatchChange();
|
|
238
284
|
};
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
285
|
+
/**
|
|
286
|
+
* Publishes the position: the fraction consumer CSS multiplies a pane by, and
|
|
287
|
+
* the range assistive tech reads off the separator. The clamped position is
|
|
288
|
+
* written back to the Value so it, ARIA, CSS, and dispatched events agree.
|
|
289
|
+
*
|
|
290
|
+
* @stimeoRenderRoot
|
|
291
|
+
*/
|
|
292
|
+
#render() {
|
|
293
|
+
const { min, max } = this.#range;
|
|
294
|
+
const position = this.#position;
|
|
295
|
+
if (this.valueValue !== position) {
|
|
296
|
+
this.valueValue = position;
|
|
243
297
|
}
|
|
244
|
-
|
|
245
|
-
this.element.style.setProperty("--stimeo--resizable-fraction", String(fraction));
|
|
298
|
+
this.element.style.setProperty("--stimeo--resizable-fraction", String(position / 100));
|
|
246
299
|
if (this.hasSeparatorTarget) {
|
|
247
|
-
this.separatorTarget.setAttribute("aria-valuenow", String(
|
|
248
|
-
this.separatorTarget.setAttribute("aria-valuemin", String(
|
|
249
|
-
this.separatorTarget.setAttribute("aria-valuemax", String(
|
|
300
|
+
this.separatorTarget.setAttribute("aria-valuenow", String(position));
|
|
301
|
+
this.separatorTarget.setAttribute("aria-valuemin", String(min));
|
|
302
|
+
this.separatorTarget.setAttribute("aria-valuemax", String(max));
|
|
250
303
|
}
|
|
251
304
|
}
|
|
252
305
|
#dispatchChange() {
|
|
253
306
|
const fraction = this.valueValue / 100;
|
|
254
307
|
this.dispatch("change", { detail: { value: this.valueValue, fraction } });
|
|
255
308
|
}
|
|
309
|
+
/** The declared range after validation; an unreadable bound uses its default. */
|
|
310
|
+
get #range() {
|
|
311
|
+
const min = Number.isFinite(this.minValue) ? this.minValue : DEFAULT_MIN;
|
|
312
|
+
const max = Number.isFinite(this.maxValue) ? this.maxValue : DEFAULT_MAX;
|
|
313
|
+
return { min, max: Math.max(min, max) };
|
|
314
|
+
}
|
|
315
|
+
/** The declared position, clamped into the validated range. */
|
|
316
|
+
get #position() {
|
|
317
|
+
const { min, max } = this.#range;
|
|
318
|
+
const value = Number.isFinite(this.valueValue) ? this.valueValue : DEFAULT_VALUE;
|
|
319
|
+
return Math.max(min, Math.min(value, max));
|
|
320
|
+
}
|
|
321
|
+
/** The keyboard increment; a non-positive or unreadable one uses the default. */
|
|
322
|
+
get #step() {
|
|
323
|
+
return Number.isFinite(this.stepValue) && this.stepValue > 0 ? this.stepValue : DEFAULT_STEP;
|
|
324
|
+
}
|
|
325
|
+
/** Whether the divider runs vertically; a separator that does not say is horizontal. */
|
|
326
|
+
get #isVertical() {
|
|
327
|
+
return this.hasSeparatorTarget && this.separatorTarget.getAttribute("aria-orientation") === "vertical";
|
|
328
|
+
}
|
|
256
329
|
};
|
|
257
330
|
|
|
258
331
|
export { ResizableController };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/arrow_step.ts","../../src/utils/before_cache_reset.ts","../../src/utils/tabindex_loan.ts","../../src/controllers/resizable_controller.ts"],"names":[],"mappings":"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","file":"resizable_controller.js","sourcesContent":["import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n","/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way (the `MicrotaskCoalescer` split).\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","import { BeforeCacheReset } from \"./before_cache_reset\";\n\n/**\n * Shared bookkeeping for a `tabindex` a controller lends an element temporarily.\n *\n * A controller that must move focus somewhere the author never made focusable\n * (a landmark root, a scroll destination) reaches for the same trick: add a\n * `tabindex` just-in-time and hand it back once it is no longer needed. The\n * borrow is the easy half; the return is what the two conditions below are for.\n *\n * **Returning needs two conditions, not one.** Owning the borrow is not enough:\n * the attribute must also still hold the value this instance wrote. A consumer\n * that changed it afterwards — `tabindex=\"0\"` to make the root its own Tab stop\n * — owns it now, and removing it there silently discards authored markup. The\n * bookkeeping is dropped either way, since the loan is over regardless of who\n * ends up owning the value.\n *\n * **Never borrow over an existing value.** An element that already carries a\n * `tabindex` is the author's to control, so there is nothing to lend and nothing\n * to return.\n *\n * Every live loan also owns a shared `turbo:before-cache` subscription. The loan\n * is returned before Turbo can copy it into a snapshot, so consumers get cache\n * safety without duplicating a lifecycle hook; `returnAll()` removes the\n * subscription again as soon as no loan remains.\n *\n * The registry is keyed by element, so a controller borrowing on a single\n * element (`this.element`) and one borrowing across a changing set of targets\n * use the same API — the single-element case is a set of one. It holds no\n * opinion about *when* to borrow or where focus goes next; that stays in the\n * controller.\n *\n * **The API is deliberately two methods.** This file's own doc block is dropped\n * from `dist`, but every member comment is inlined into **each** consumer entry\n * (`tsup` builds with `splitting: false`), so rationale belongs here, only the\n * contract belongs on the members, and every method no consumer calls is still\n * paid for once per consumer entry.\n *\n * @example\n * ```ts\n * readonly #tabindex = new TabindexLoan();\n *\n * #rescueFocus() {\n * this.#tabindex.lend(this.element);\n * this.element.focus();\n * }\n *\n * disconnect() {\n * this.#tabindex.returnAll();\n * }\n * ```\n */\nexport class TabindexLoan<T extends HTMLElement = HTMLElement> {\n readonly #value: string;\n readonly #lent = new Set<T>();\n /** Returns live loans before Turbo can copy them into its page snapshot. */\n readonly #beforeCache = new BeforeCacheReset(() => this.returnAll());\n\n /**\n * @param value - the `tabindex` to lend. `\"-1\"` (the default) is\n * programmatically focusable but not a Tab stop; `\"0\"` is a real Tab stop,\n * which a scroll region with no focusable content of its own needs.\n */\n constructor(value: string = \"-1\") {\n this.#value = value;\n }\n\n /** Lends `element` the value; no-ops when it already carries a `tabindex`. */\n lend(element: T): void {\n if (element.hasAttribute(\"tabindex\")) return;\n element.setAttribute(\"tabindex\", this.#value);\n this.#lent.add(element);\n // Subscribe only while a real loan exists. Keeping this guarantee here means\n // every consumer is Turbo-safe without another lifecycle hook to remember.\n this.#beforeCache.activate();\n }\n\n /** Takes back every loan whose value is still the one that was lent. */\n returnAll(): void {\n for (const element of this.#lent) {\n if (element.getAttribute(\"tabindex\") === this.#value) element.removeAttribute(\"tabindex\");\n }\n this.#lent.clear();\n this.#beforeCache.deactivate();\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord } from \"../utils/arrow_step\";\nimport { TabindexLoan } from \"../utils/tabindex_loan\";\n\n/**\n * Headless, highly accessible Window Splitter / Resizable panes logic.\n *\n * Markup contract (identifier: `stimeo--resizable`):\n * <div data-controller=\"stimeo--resizable\"\n * data-stimeo--resizable-min-value=\"20\"\n * data-stimeo--resizable-max-value=\"80\"\n * data-stimeo--resizable-value-value=\"50\">\n * <div data-stimeo--resizable-target=\"primary\" id=\"pane-1\">Pane A</div>\n * <div role=\"separator\" tabindex=\"0\" aria-orientation=\"vertical\"\n * aria-controls=\"pane-1\" aria-valuemin=\"20\" aria-valuemax=\"80\"\n * aria-valuenow=\"50\" aria-label=\"Resize splitter\"\n * data-stimeo--resizable-target=\"separator\"\n * data-action=\"pointerdown->stimeo--resizable#onPointerDown\n * keydown->stimeo--resizable#onKeydown\"></div>\n * <div data-stimeo--resizable-target=\"secondary\">Pane B</div>\n * </div>\n *\n * Implements the WAI-ARIA APG **Window Splitter** pattern:\n * - Robust dragging track using W3C `setPointerCapture` to ensure uninterrupted dragging\n * even when pointers quickly stray outside the separator area.\n * - Auto-clamped flex-percentage updates with bounds safety.\n * - Smooth step keyboard adjustments for Arrow keys based on vertical/horizontal orientation.\n * - Root-level CSS custom property `--stimeo--resizable-fraction` (0..1) driving\n * presentation styles.\n * - `F6` cycles focus through the panes, which the pattern lists as optional.\n *\n * `change` dispatches `{ value: number, fraction: number }`.\n *\n * @remarks\n * Behavior only. The controller adjusts the CSS custom property on the root element,\n * updates ARIA values on the separator, and emits `stimeo--resizable:change` events.\n */\nexport class ResizableController extends Controller<HTMLElement> {\n static override targets = [\"primary\", \"secondary\", \"separator\"];\n static override values = {\n min: { type: Number, default: 0 },\n max: { type: Number, default: 100 },\n step: { type: Number, default: 1 },\n value: { type: Number, default: 50 },\n };\n static actions = [\"onKeydown\", \"onPointerDown\", \"toggle\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly primaryTarget: HTMLElement;\n declare readonly secondaryTarget: HTMLElement;\n declare readonly separatorTarget: HTMLElement;\n declare readonly hasPrimaryTarget: boolean;\n declare readonly hasSecondaryTarget: boolean;\n declare readonly hasSeparatorTarget: boolean;\n\n declare minValue: number;\n declare maxValue: number;\n declare stepValue: number;\n declare valueValue: number;\n\n /** The value held before the current collapse, restored when toggling back. */\n #valueBeforeCollapse = 50;\n\n /** Aborts in-progress pointer-drag listeners when the drag ends or on teardown. */\n #dragAbort: AbortController | null = null;\n\n /** `tabindex` lent to a pane so `F6` can put focus on it; panes carry none. */\n readonly #paneTabindex = new TabindexLoan();\n\n /** Releases the pane cycle listener bound in {@link connect}. */\n #cycleAbort: AbortController | null = null;\n\n override connect(): void {\n this.#clampAndSync();\n // Bound on the root rather than the panes so the cycle continues once focus\n // has left the separator — the panes hold consumer markup and the next F6\n // arrives from inside one of them.\n this.#cycleAbort = new AbortController();\n this.element.addEventListener(\"keydown\", this.#onCycleKeydown, {\n signal: this.#cycleAbort.signal,\n });\n }\n\n /** Cancels any active pointer drag so listeners never leak past disconnect. */\n override disconnect(): void {\n this.#dragAbort?.abort();\n this.#dragAbort = null;\n this.#cycleAbort?.abort();\n this.#cycleAbort = null;\n this.#paneTabindex.returnAll();\n }\n\n /** Moves focus to the next pane on `F6`, wrapping; entering at the first one. */\n readonly #onCycleKeydown = (event: KeyboardEvent): void => {\n if (event.key !== \"F6\") return;\n // A descendant widget that already claimed the key must not ALSO cycle the\n // panes; this listener sees consumer markup, so composition depends on it.\n if (event.defaultPrevented) return;\n // The pattern binds a bare F6 only. A chorded one is the browser's or the\n // OS's (Ctrl+F6 cycles frames in several browsers).\n if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;\n\n const panes: HTMLElement[] = [];\n if (this.hasPrimaryTarget) panes.push(this.primaryTarget);\n if (this.hasSecondaryTarget) panes.push(this.secondaryTarget);\n if (panes.length === 0) return;\n\n const target = event.target as Node | null;\n const current = panes.findIndex((pane) => target instanceof Node && pane.contains(target));\n const next = panes[(current + 1) % panes.length];\n if (!next) return;\n\n event.preventDefault();\n this.#paneTabindex.lend(next);\n next.focus();\n };\n\n /**\n * Stimulus lifecycle callback when the valueValue changes.\n * Keeps CSS fractions and ARIA status completely aligned.\n */\n valueValueChanged(): void {\n this.#clampAndSync();\n }\n\n /** Starts active pointer drag tracking and locks capture. */\n onPointerDown(event: PointerEvent): void {\n if (!this.hasSeparatorTarget || event.button !== 0) return;\n\n event.preventDefault();\n\n const separator = this.separatorTarget;\n separator.setPointerCapture(event.pointerId);\n // preventDefault() above suppresses the implicit focus, so move focus\n // explicitly — otherwise keyboard (arrow) adjustments never reach the\n // separator after a pointer interaction (WCAG 2.1.1).\n separator.focus();\n\n this.element.setAttribute(\"data-dragging\", \"true\");\n\n this.#dragAbort?.abort();\n const abort = new AbortController();\n this.#dragAbort = abort;\n separator.addEventListener(\"pointermove\", this.#onPointerMove, { signal: abort.signal });\n separator.addEventListener(\"pointerup\", this.#onPointerUp, { signal: abort.signal });\n separator.addEventListener(\"pointercancel\", this.#onPointerUp, { signal: abort.signal });\n }\n\n /** Keydown adjustments for ArrowUp/Down/Left/Right and Home/End. */\n onKeydown(event: KeyboardEvent): void {\n if (isReservedArrowChord(event)) return;\n if (!this.hasSeparatorTarget) return;\n\n const orientation = this.separatorTarget.getAttribute(\"aria-orientation\") || \"vertical\";\n const isVertical = orientation === \"vertical\";\n\n let handled = true;\n let nextValue = this.valueValue;\n\n switch (event.key) {\n case \"ArrowLeft\":\n if (isVertical) {\n nextValue -= this.stepValue;\n } else {\n handled = false;\n }\n break;\n case \"ArrowRight\":\n if (isVertical) {\n nextValue += this.stepValue;\n } else {\n handled = false;\n }\n break;\n case \"ArrowUp\":\n if (!isVertical) {\n nextValue -= this.stepValue;\n } else {\n handled = false;\n }\n break;\n case \"ArrowDown\":\n if (!isVertical) {\n nextValue += this.stepValue;\n } else {\n handled = false;\n }\n break;\n case \"Home\":\n nextValue = this.minValue;\n break;\n case \"End\":\n nextValue = this.maxValue;\n break;\n case \"Enter\":\n event.preventDefault();\n this.toggle();\n return;\n default:\n handled = false;\n break;\n }\n\n if (handled) {\n event.preventDefault();\n this.valueValue = Math.max(this.minValue, Math.min(nextValue, this.maxValue));\n this.#clampAndSync();\n this.#dispatchChange();\n }\n }\n\n /** Double-click or Enter to collapse/restore pane to min/max levels. */\n toggle(): void {\n const threshold = this.minValue + (this.maxValue - this.minValue) / 2;\n if (this.valueValue > this.minValue) {\n this.#valueBeforeCollapse = this.valueValue;\n this.valueValue = this.minValue;\n } else {\n this.valueValue =\n this.#valueBeforeCollapse >= threshold ? this.#valueBeforeCollapse : this.maxValue;\n }\n this.#clampAndSync();\n this.#dispatchChange();\n }\n\n readonly #onPointerMove = (event: PointerEvent): void => {\n if (!this.hasSeparatorTarget) return;\n\n const rect = this.element.getBoundingClientRect();\n const orientation = this.separatorTarget.getAttribute(\"aria-orientation\") || \"vertical\";\n const isVertical = orientation === \"vertical\";\n\n let fraction = 0.5;\n\n if (isVertical) {\n fraction = (event.clientX - rect.left) / rect.width;\n } else {\n fraction = (event.clientY - rect.top) / rect.height;\n }\n\n // Restrict within absolute boundaries\n fraction = Math.max(0, Math.min(fraction, 1));\n const percent = Math.round(fraction * 100);\n\n this.valueValue = Math.max(this.minValue, Math.min(percent, this.maxValue));\n this.#clampAndSync();\n };\n\n readonly #onPointerUp = (event: PointerEvent): void => {\n if (!this.hasSeparatorTarget) return;\n\n const separator = this.separatorTarget;\n separator.releasePointerCapture(event.pointerId);\n\n this.element.removeAttribute(\"data-dragging\");\n\n this.#dragAbort?.abort();\n this.#dragAbort = null;\n\n this.#dispatchChange();\n };\n\n #clampAndSync(): void {\n const clamped = Math.max(this.minValue, Math.min(this.valueValue, this.maxValue));\n\n // Keep the stored value in range so valueValue, ARIA, CSS, and dispatched\n // events never diverge (e.g. an initial value above max). Writing back\n // re-enters valueValueChanged once, but the value is already clamped there\n // so it does not recurse.\n if (this.valueValue !== clamped) {\n this.valueValue = clamped;\n }\n\n // Expose local fraction to consumer CSS custom property\n const fraction = clamped / 100;\n this.element.style.setProperty(\"--stimeo--resizable-fraction\", String(fraction));\n\n if (this.hasSeparatorTarget) {\n this.separatorTarget.setAttribute(\"aria-valuenow\", String(clamped));\n this.separatorTarget.setAttribute(\"aria-valuemin\", String(this.minValue));\n this.separatorTarget.setAttribute(\"aria-valuemax\", String(this.maxValue));\n }\n }\n\n #dispatchChange(): void {\n const fraction = this.valueValue / 100;\n this.dispatch(\"change\", { detail: { value: this.valueValue, fraction } });\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/arrow_step.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/before_cache_reset.ts","../../src/utils/tabindex_loan.ts","../../src/controllers/resizable_controller.ts"],"names":[],"mappings":"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","file":"resizable_controller.js","sourcesContent":["import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n","/**\n * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way (the `MicrotaskCoalescer` split).\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","import { BeforeCacheReset } from \"./before_cache_reset\";\n\n/**\n * Shared bookkeeping for a `tabindex` a controller lends an element temporarily.\n *\n * A controller that must move focus somewhere the author never made focusable\n * (a landmark root, a scroll destination) reaches for the same trick: add a\n * `tabindex` just-in-time and hand it back once it is no longer needed. The\n * borrow is the easy half; the return is what the two conditions below are for.\n *\n * **Returning needs two conditions, not one.** Owning the borrow is not enough:\n * the attribute must also still hold the value this instance wrote. A consumer\n * that changed it afterwards — `tabindex=\"0\"` to make the root its own Tab stop\n * — owns it now, and removing it there silently discards authored markup. The\n * bookkeeping is dropped either way, since the loan is over regardless of who\n * ends up owning the value.\n *\n * **Never borrow over an existing value.** An element that already carries a\n * `tabindex` is the author's to control, so there is nothing to lend and nothing\n * to return.\n *\n * Every live loan also owns a shared `turbo:before-cache` subscription. The loan\n * is returned before Turbo can copy it into a snapshot, so consumers get cache\n * safety without duplicating a lifecycle hook; `returnAll()` removes the\n * subscription again as soon as no loan remains.\n *\n * The registry is keyed by element, so a controller borrowing on a single\n * element (`this.element`) and one borrowing across a changing set of targets\n * use the same API — the single-element case is a set of one. It holds no\n * opinion about *when* to borrow or where focus goes next; that stays in the\n * controller.\n *\n * **The API is deliberately two methods.** This file's own doc block is dropped\n * from `dist`, but every member comment is inlined into **each** consumer entry\n * (`tsup` builds with `splitting: false`), so rationale belongs here, only the\n * contract belongs on the members, and every method no consumer calls is still\n * paid for once per consumer entry.\n *\n * @example\n * ```ts\n * readonly #tabindex = new TabindexLoan();\n *\n * #rescueFocus() {\n * this.#tabindex.lend(this.element);\n * this.element.focus();\n * }\n *\n * disconnect() {\n * this.#tabindex.returnAll();\n * }\n * ```\n */\nexport class TabindexLoan<T extends HTMLElement = HTMLElement> {\n readonly #value: string;\n readonly #lent = new Set<T>();\n /** Returns live loans before Turbo can copy them into its page snapshot. */\n readonly #beforeCache = new BeforeCacheReset(() => this.returnAll());\n\n /**\n * @param value - the `tabindex` to lend. `\"-1\"` (the default) is\n * programmatically focusable but not a Tab stop; `\"0\"` is a real Tab stop,\n * which a scroll region with no focusable content of its own needs.\n */\n constructor(value: string = \"-1\") {\n this.#value = value;\n }\n\n /** Lends `element` the value; no-ops when it already carries a `tabindex`. */\n lend(element: T): void {\n if (element.hasAttribute(\"tabindex\")) return;\n element.setAttribute(\"tabindex\", this.#value);\n this.#lent.add(element);\n // Subscribe only while a real loan exists. Keeping this guarantee here means\n // every consumer is Turbo-safe without another lifecycle hook to remember.\n this.#beforeCache.activate();\n }\n\n /** Takes back every loan whose value is still the one that was lent. */\n returnAll(): void {\n for (const element of this.#lent) {\n if (element.getAttribute(\"tabindex\") === this.#value) element.removeAttribute(\"tabindex\");\n }\n this.#lent.clear();\n this.#beforeCache.deactivate();\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord } from \"../utils/arrow_step\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\nimport { TabindexLoan } from \"../utils/tabindex_loan\";\n\n/** Value defaults, reused when a declaration cannot be read as a percentage. */\nconst DEFAULT_MIN = 0;\nconst DEFAULT_MAX = 100;\nconst DEFAULT_VALUE = 50;\nconst DEFAULT_STEP = 1;\n\n/**\n * Headless, highly accessible Window Splitter / Resizable panes logic.\n *\n * Markup contract (identifier: `stimeo--resizable`):\n * <div data-controller=\"stimeo--resizable\"\n * data-stimeo--resizable-min-value=\"20\"\n * data-stimeo--resizable-max-value=\"80\"\n * data-stimeo--resizable-step-value=\"1\"\n * data-stimeo--resizable-value-value=\"50\">\n * <div data-stimeo--resizable-target=\"primary\" id=\"pane-1\">Pane A</div>\n * <div role=\"separator\" tabindex=\"0\" aria-orientation=\"vertical\"\n * aria-controls=\"pane-1\" aria-valuemin=\"20\" aria-valuemax=\"80\"\n * aria-valuenow=\"50\" aria-label=\"Resize splitter\"\n * data-stimeo--resizable-target=\"separator\"\n * data-action=\"pointerdown->stimeo--resizable#onPointerDown\n * keydown->stimeo--resizable#onKeydown\"></div>\n * <div data-stimeo--resizable-target=\"secondary\">Pane B</div>\n * </div>\n *\n * Implements the WAI-ARIA APG **Window Splitter** pattern:\n * - Robust dragging track using W3C `setPointerCapture` to ensure uninterrupted dragging\n * even when pointers quickly stray outside the separator area.\n * - Arrow keys move the divider by `step` percent, along the axis\n * `aria-orientation` names; `Home` / `End` jump to the ends of the range.\n * - `Enter` collapses the primary pane to the minimum and puts it back where it\n * was, which is what the pattern means by restoring a previous position.\n * - Root-level CSS custom property `--stimeo--resizable-fraction` (0..1) driving\n * presentation styles.\n * - `F6` cycles focus through the panes, which the pattern lists as optional.\n *\n * `change` dispatches `{ value: number, fraction: number }`.\n *\n * @remarks\n * Behavior only. The controller adjusts the CSS custom property on the root element,\n * updates ARIA values on the separator, and emits `stimeo--resizable:change` events.\n *\n * Both the range and the position are percentages. A bound that is not a finite\n * number falls back to its Value default, a maximum below the minimum collapses\n * onto that minimum, and a step that is not a positive finite number falls back\n * to `1` — so an unreadable declaration narrows what the widget can do without\n * ever publishing `NaN` to CSS or to assistive tech.\n *\n * `aria-orientation` is the author's, and its absence means `horizontal` — the\n * value ARIA gives a separator that does not say. Reading any other default here\n * would make the announced axis and the answering arrow keys disagree.\n */\nexport class ResizableController extends Controller<HTMLElement> {\n static override targets = [\"primary\", \"secondary\", \"separator\"];\n static override values = {\n min: { type: Number, default: DEFAULT_MIN },\n max: { type: Number, default: DEFAULT_MAX },\n step: { type: Number, default: DEFAULT_STEP },\n value: { type: Number, default: DEFAULT_VALUE },\n };\n static actions = [\"onKeydown\", \"onPointerDown\", \"toggle\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly primaryTarget: HTMLElement;\n declare readonly secondaryTarget: HTMLElement;\n declare readonly separatorTarget: HTMLElement;\n declare readonly hasPrimaryTarget: boolean;\n declare readonly hasSecondaryTarget: boolean;\n declare readonly hasSeparatorTarget: boolean;\n\n declare minValue: number;\n declare maxValue: number;\n declare stepValue: number;\n declare valueValue: number;\n\n /** Where the divider sat before the current collapse; `null` when unknown. */\n #valueBeforeCollapse: number | null = null;\n\n /** Aborts in-progress pointer-drag listeners when the drag ends or on teardown. */\n #dragAbort: AbortController | null = null;\n\n /** `tabindex` lent to a pane so `F6` can put focus on it; panes carry none. */\n readonly #paneTabindex = new TabindexLoan();\n\n /** Releases the pane cycle listener bound in {@link connect}. */\n #cycleAbort: AbortController | null = null;\n\n /** Folds the range and position inputs of one morph batch into a single paint. */\n readonly #repaint = new MicrotaskCoalescer(() => this.#render());\n\n override connect(): void {\n // A drag cannot outlive a navigation, so a hook captured mid-drag is stale on\n // arrival. Clearing is unconditional here because disconnect() aborts the\n // drag, leaving no session that an in-page move could carry over.\n this.element.removeAttribute(\"data-dragging\");\n this.#repaint.activate();\n // Stimulus fires the value callbacks before connect, where the coalescer\n // ignores them, so the first paint has to be asked for directly.\n this.#render();\n // Bound on the root rather than the panes so the cycle continues once focus\n // has left the separator — the panes hold consumer markup and the next F6\n // arrives from inside one of them.\n this.#cycleAbort = new AbortController();\n this.element.addEventListener(\"keydown\", this.#onCycleKeydown, {\n signal: this.#cycleAbort.signal,\n });\n }\n\n /** Cancels any active pointer drag so listeners never leak past disconnect. */\n override disconnect(): void {\n this.#dragAbort?.abort();\n this.#dragAbort = null;\n this.#cycleAbort?.abort();\n this.#cycleAbort = null;\n this.#repaint.cancel();\n this.#paneTabindex.returnAll();\n this.element.removeAttribute(\"data-dragging\");\n }\n\n /** Moves focus to the next pane on `F6`, wrapping; entering at the first one. */\n readonly #onCycleKeydown = (event: KeyboardEvent): void => {\n if (event.key !== \"F6\") return;\n // Japanese input methods bind F6 to a conversion, so a press that belongs to\n // a composition steers that conversion and must not move focus away from it.\n if (event.isComposing) return;\n // A descendant widget that already claimed the key must not ALSO cycle the\n // panes; this listener sees consumer markup, so composition depends on it.\n if (event.defaultPrevented) return;\n // The pattern binds a bare F6 only. A chorded one is the browser's or the\n // OS's (Ctrl+F6 cycles frames in several browsers).\n if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;\n\n const panes: HTMLElement[] = [];\n if (this.hasPrimaryTarget) panes.push(this.primaryTarget);\n if (this.hasSecondaryTarget) panes.push(this.secondaryTarget);\n\n const target = event.target as Node | null;\n const current = panes.findIndex((pane) => target instanceof Node && pane.contains(target));\n // With no panes the modulo is NaN and the read is undefined, which is the\n // same \"nowhere to go\" this guard answers for a cycle that has ends.\n const next = panes[(current + 1) % panes.length];\n if (!next) return;\n\n event.preventDefault();\n this.#paneTabindex.lend(next);\n next.focus();\n };\n\n /** Re-renders when application code (or a Turbo morph) changes `value` at runtime. */\n valueValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /** Re-renders when application code (or a Turbo morph) changes `min` at runtime. */\n minValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /** Re-renders when application code (or a Turbo morph) changes `max` at runtime. */\n maxValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /** Re-publishes range and position onto a separator swapped in after connect. */\n separatorTargetConnected(): void {\n this.#repaint.schedule();\n }\n\n /** Starts active pointer drag tracking and locks capture. */\n onPointerDown(event: PointerEvent): void {\n if (!this.hasSeparatorTarget || event.button !== 0) return;\n\n event.preventDefault();\n\n const separator = this.separatorTarget;\n separator.setPointerCapture(event.pointerId);\n // preventDefault() above suppresses the implicit focus, so move focus\n // explicitly — otherwise keyboard (arrow) adjustments never reach the\n // separator after a pointer interaction (WCAG 2.1.1).\n separator.focus();\n\n this.element.setAttribute(\"data-dragging\", \"true\");\n\n this.#dragAbort?.abort();\n const abort = new AbortController();\n this.#dragAbort = abort;\n separator.addEventListener(\"pointermove\", this.#onPointerMove, { signal: abort.signal });\n separator.addEventListener(\"pointerup\", this.#onPointerUp, { signal: abort.signal });\n separator.addEventListener(\"pointercancel\", this.#onPointerUp, { signal: abort.signal });\n }\n\n /** Keydown adjustments for ArrowUp/Down/Left/Right and Home/End. */\n onKeydown(event: KeyboardEvent): void {\n if (isReservedArrowChord(event)) return;\n if (!this.hasSeparatorTarget) return;\n\n const { min, max } = this.#range;\n const isVertical = this.#isVertical;\n\n let handled = true;\n let nextValue = this.#position;\n\n switch (event.key) {\n case \"ArrowLeft\":\n if (isVertical) {\n nextValue -= this.#step;\n } else {\n handled = false;\n }\n break;\n case \"ArrowRight\":\n if (isVertical) {\n nextValue += this.#step;\n } else {\n handled = false;\n }\n break;\n case \"ArrowUp\":\n if (!isVertical) {\n nextValue -= this.#step;\n } else {\n handled = false;\n }\n break;\n case \"ArrowDown\":\n if (!isVertical) {\n nextValue += this.#step;\n } else {\n handled = false;\n }\n break;\n case \"Home\":\n nextValue = min;\n break;\n case \"End\":\n nextValue = max;\n break;\n case \"Enter\":\n event.preventDefault();\n this.toggle();\n return;\n default:\n handled = false;\n break;\n }\n\n if (handled) {\n event.preventDefault();\n this.valueValue = Math.max(min, Math.min(nextValue, max));\n this.#render();\n this.#dispatchChange();\n }\n }\n\n /** Double-click or Enter to collapse the primary pane, or put it back. */\n toggle(): void {\n const { min, max } = this.#range;\n if (this.#position > min) {\n this.#valueBeforeCollapse = this.#position;\n this.valueValue = min;\n } else {\n // With nothing remembered — a controller that connected to an already\n // collapsed pane — there is no previous position, so open the pane fully.\n this.valueValue = this.#valueBeforeCollapse ?? max;\n this.#valueBeforeCollapse = null;\n }\n this.#render();\n this.#dispatchChange();\n }\n\n readonly #onPointerMove = (event: PointerEvent): void => {\n if (!this.hasSeparatorTarget) return;\n\n const rect = this.element.getBoundingClientRect();\n const raw = this.#isVertical\n ? (event.clientX - rect.left) / rect.width\n : (event.clientY - rect.top) / rect.height;\n\n // A container with no extent divides by zero; treat that as the near end\n // rather than letting it reach the published fraction.\n const fraction = Number.isFinite(raw) ? Math.max(0, Math.min(raw, 1)) : 0;\n const { min, max } = this.#range;\n\n this.valueValue = Math.max(min, Math.min(Math.round(fraction * 100), max));\n this.#render();\n };\n\n readonly #onPointerUp = (event: PointerEvent): void => {\n // Ending the drag must not depend on the separator still being there: a\n // target removed mid-drag would otherwise strand the hook and the listeners.\n this.element.removeAttribute(\"data-dragging\");\n this.#dragAbort?.abort();\n this.#dragAbort = null;\n\n if (this.hasSeparatorTarget) {\n this.separatorTarget.releasePointerCapture(event.pointerId);\n }\n\n this.#dispatchChange();\n };\n\n /**\n * Publishes the position: the fraction consumer CSS multiplies a pane by, and\n * the range assistive tech reads off the separator. The clamped position is\n * written back to the Value so it, ARIA, CSS, and dispatched events agree.\n *\n * @stimeoRenderRoot\n */\n #render(): void {\n const { min, max } = this.#range;\n const position = this.#position;\n\n if (this.valueValue !== position) {\n this.valueValue = position;\n }\n\n this.element.style.setProperty(\"--stimeo--resizable-fraction\", String(position / 100));\n\n if (this.hasSeparatorTarget) {\n this.separatorTarget.setAttribute(\"aria-valuenow\", String(position));\n this.separatorTarget.setAttribute(\"aria-valuemin\", String(min));\n this.separatorTarget.setAttribute(\"aria-valuemax\", String(max));\n }\n }\n\n #dispatchChange(): void {\n const fraction = this.valueValue / 100;\n this.dispatch(\"change\", { detail: { value: this.valueValue, fraction } });\n }\n\n /** The declared range after validation; an unreadable bound uses its default. */\n get #range(): { readonly min: number; readonly max: number } {\n const min = Number.isFinite(this.minValue) ? this.minValue : DEFAULT_MIN;\n const max = Number.isFinite(this.maxValue) ? this.maxValue : DEFAULT_MAX;\n // A maximum below the minimum describes no range at all; collapsing it onto\n // the minimum keeps the position inside something that can be announced.\n return { min, max: Math.max(min, max) };\n }\n\n /** The declared position, clamped into the validated range. */\n get #position(): number {\n const { min, max } = this.#range;\n const value = Number.isFinite(this.valueValue) ? this.valueValue : DEFAULT_VALUE;\n return Math.max(min, Math.min(value, max));\n }\n\n /** The keyboard increment; a non-positive or unreadable one uses the default. */\n get #step(): number {\n return Number.isFinite(this.stepValue) && this.stepValue > 0 ? this.stepValue : DEFAULT_STEP;\n }\n\n /** Whether the divider runs vertically; a separator that does not say is horizontal. */\n get #isVertical(): boolean {\n return (\n this.hasSeparatorTarget &&\n this.separatorTarget.getAttribute(\"aria-orientation\") === \"vertical\"\n );\n }\n}\n"]}
|
|
@@ -36,7 +36,8 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
36
36
|
* - `stimeo--spinner:timeout` — `timeout` elapsed with the load still running;
|
|
37
37
|
* the controller then ends it exactly as `stop` would, so a `hide` follows.
|
|
38
38
|
*
|
|
39
|
-
* `hide`, `show` and `
|
|
39
|
+
* `hide`, `show`, `timeout`, and `reconcile` dispatch `{}`. The last of those
|
|
40
|
+
* reports that the Turbo cache rewind returned a running cycle to idle.
|
|
40
41
|
*
|
|
41
42
|
* @remarks
|
|
42
43
|
* Behavior only — the visual spinner is the consumer's, alongside the text and
|
|
@@ -70,7 +71,7 @@ declare class SpinnerController extends Controller<HTMLElement> {
|
|
|
70
71
|
};
|
|
71
72
|
};
|
|
72
73
|
static actions: readonly ["start", "stop"];
|
|
73
|
-
static events: readonly ["hide", "show", "timeout"];
|
|
74
|
+
static events: readonly ["hide", "show", "timeout", "reconcile"];
|
|
74
75
|
readonly indicatorTarget: HTMLElement;
|
|
75
76
|
readonly regionTarget: HTMLElement;
|
|
76
77
|
readonly messageTarget: HTMLElement;
|
|
@@ -222,7 +222,7 @@ var SpinnerController = class extends Controller {
|
|
|
222
222
|
timeout: { type: Number, default: 0 }
|
|
223
223
|
};
|
|
224
224
|
static actions = ["start", "stop"];
|
|
225
|
-
static events = ["hide", "show", "timeout"];
|
|
225
|
+
static events = ["hide", "show", "timeout", "reconcile"];
|
|
226
226
|
#timers = new SafeTimeout();
|
|
227
227
|
#floor = new MinDurationFloor(this.#timers);
|
|
228
228
|
#gate = new DetachGate();
|
|
@@ -326,16 +326,18 @@ var SpinnerController = class extends Controller {
|
|
|
326
326
|
* Returns the loading state to idle for the snapshot Turbo is about to take,
|
|
327
327
|
* so a page reached with the Back button is not restored mid-load with a
|
|
328
328
|
* spinner nothing can stop. State only: `data-state`, the indicator's `hidden`,
|
|
329
|
-
* and `aria-busy`. No `hide` is dispatched —
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
* leaves the running cycle intact.
|
|
329
|
+
* and `aria-busy`. No `hide` is dispatched — that would claim the load finished.
|
|
330
|
+
* The rewind reports itself as `reconcile` instead, so a consumer painting from
|
|
331
|
+
* `show` can drop it before the snapshot is taken. The live page keeps its
|
|
332
|
+
* timers, so a navigation that never completes leaves the running cycle intact.
|
|
333
333
|
*/
|
|
334
334
|
#rewindForCache() {
|
|
335
|
+
const running = this.element.getAttribute("data-state") !== "idle";
|
|
335
336
|
this.#cancelTimeout();
|
|
336
337
|
this.#setBusy(false);
|
|
337
338
|
if (this.hasIndicatorTarget) this.indicatorTarget.hidden = true;
|
|
338
339
|
this.element.setAttribute("data-state", "idle");
|
|
340
|
+
if (running) this.dispatch("reconcile", { detail: {} });
|
|
339
341
|
}
|
|
340
342
|
/** Reflects busy state onto the controlled region (if present). */
|
|
341
343
|
#setBusy(busy) {
|