stimeo-ui 0.2.0 → 0.3.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 +163 -0
- data/dist/controllers/accordion_controller.js +10 -0
- data/dist/controllers/alert_dialog_controller.js +318 -0
- data/dist/controllers/breadcrumb_controller.js +225 -13
- data/dist/controllers/calendar_controller.js +89 -22
- data/dist/controllers/carousel_controller.js +313 -0
- data/dist/controllers/clipboard_controller.js +144 -0
- data/dist/controllers/collapsible_controller.js +327 -0
- data/dist/controllers/color_picker_controller.js +252 -0
- data/dist/controllers/combobox_controller.js +162 -23
- data/dist/controllers/command_palette_controller.js +194 -17
- data/dist/controllers/context_menu_controller.js +32 -10
- data/dist/controllers/count_up_controller.js +8 -1
- data/dist/controllers/currency_input_controller.js +147 -0
- data/dist/controllers/data_grid_controller.js +246 -0
- data/dist/controllers/date_range_picker_controller.js +441 -0
- data/dist/controllers/dismissible_controller.js +117 -0
- data/dist/controllers/drawer_controller.js +630 -0
- data/dist/controllers/editable_controller.js +169 -0
- data/dist/controllers/file_dropzone_controller.js +165 -0
- data/dist/controllers/filter_controller.js +86 -0
- data/dist/controllers/flash_controller.js +36 -5
- data/dist/controllers/form_validation_controller.js +1 -1
- data/dist/controllers/highlight_controller.js +6 -4
- data/dist/controllers/intersection_controller.js +67 -19
- data/dist/controllers/lazy_frame_controller.js +54 -11
- data/dist/controllers/listbox_controller.js +257 -53
- data/dist/controllers/local_time_controller.js +2 -2
- data/dist/controllers/masonry_controller.js +142 -0
- data/dist/controllers/menu_controller.js +104 -17
- data/dist/controllers/menubar_controller.js +785 -0
- data/dist/controllers/multi_select_controller.js +755 -0
- data/dist/controllers/navigation_menu_controller.js +511 -0
- data/dist/controllers/number_input_controller.js +7 -0
- data/dist/controllers/otp_controller.js +18 -1
- data/dist/controllers/overflow_indicator_controller.js +246 -27
- data/dist/controllers/overflow_menu_controller.js +381 -57
- data/dist/controllers/pagination_controller.js +163 -32
- data/dist/controllers/password_reveal_controller.js +117 -0
- data/dist/controllers/persist_controller.js +6 -6
- data/dist/controllers/pointer_drag_controller.js +9 -1
- data/dist/controllers/popover_controller.js +2 -2
- data/dist/controllers/radio_group_controller.js +22 -3
- data/dist/controllers/range_slider_controller.js +192 -0
- data/dist/controllers/rating_controller.js +16 -2
- data/dist/controllers/read_more_controller.js +238 -0
- data/dist/controllers/resizable_controller.js +65 -1
- data/dist/controllers/roving_controller.js +17 -2
- data/dist/controllers/scroll_area_controller.js +101 -14
- data/dist/controllers/scroll_restore_controller.js +93 -0
- data/dist/controllers/scroll_visibility_controller.js +40 -6
- data/dist/controllers/scrollspy_controller.js +369 -74
- data/dist/controllers/separator_controller.js +96 -0
- data/dist/controllers/sidebar_controller.js +761 -0
- data/dist/controllers/skeleton_controller.js +1 -1
- data/dist/controllers/slider_controller.js +32 -6
- data/dist/controllers/sortable_controller.js +34 -3
- data/dist/controllers/spinner_controller.js +1 -1
- data/dist/controllers/stepper_controller.js +28 -12
- data/dist/controllers/stick_to_bottom_controller.js +9 -4
- data/dist/controllers/sticky_observer_controller.js +109 -20
- data/dist/controllers/switch_controller.js +1 -0
- data/dist/controllers/tabs_controller.js +26 -3
- data/dist/controllers/tags_input_controller.js +295 -0
- data/dist/controllers/theme_controller.js +42 -13
- data/dist/controllers/time_picker_controller.js +231 -0
- data/dist/controllers/toast_controller.js +40 -14
- data/dist/controllers/toggle_group_controller.js +23 -2
- data/dist/controllers/toolbar_controller.js +230 -31
- data/dist/controllers/transition_controller.js +153 -38
- data/dist/controllers/tree_view_controller.js +691 -0
- data/dist/index.js +4256 -915
- data/lib/stimeo/ui/version.rb +2 -3
- metadata +28 -2
|
@@ -2,6 +2,17 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/toggle_group_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/logical_scroll.ts
|
|
6
|
+
function isRtl(element) {
|
|
7
|
+
return window.getComputedStyle(element).direction === "rtl";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// src/utils/arrow_step.ts
|
|
11
|
+
function isReservedArrowChord(event, allow = []) {
|
|
12
|
+
if (!event.key.startsWith("Arrow")) return false;
|
|
13
|
+
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
14
|
+
}
|
|
15
|
+
|
|
5
16
|
// src/utils/roving_tabindex.ts
|
|
6
17
|
var RovingTabindex = class {
|
|
7
18
|
/** Returns the current ordered item elements; called on every operation. */
|
|
@@ -60,17 +71,27 @@ var ToggleGroupController = class extends Controller {
|
|
|
60
71
|
}
|
|
61
72
|
/** Arrow/Home/End move focus only; Space/Enter toggle non-button hosts. */
|
|
62
73
|
onKeydown(event) {
|
|
74
|
+
if (event.defaultPrevented) return;
|
|
75
|
+
if (isReservedArrowChord(event)) return;
|
|
63
76
|
const current = this.itemTargets.indexOf(event.currentTarget);
|
|
64
77
|
if (current === -1) return;
|
|
65
78
|
let next = null;
|
|
79
|
+
const rtl = isRtl(this.element);
|
|
80
|
+
const horizontalStep = (key) => rtl ? key === "ArrowRight" ? -1 : 1 : key === "ArrowRight" ? 1 : -1;
|
|
66
81
|
switch (event.key) {
|
|
67
82
|
case "ArrowRight":
|
|
68
83
|
case "ArrowDown":
|
|
69
|
-
next = rovingMove(
|
|
84
|
+
next = rovingMove(
|
|
85
|
+
current,
|
|
86
|
+
this.itemTargets.length,
|
|
87
|
+
event.key === "ArrowDown" ? 1 : horizontalStep(event.key));
|
|
70
88
|
break;
|
|
71
89
|
case "ArrowLeft":
|
|
72
90
|
case "ArrowUp":
|
|
73
|
-
next = rovingMove(
|
|
91
|
+
next = rovingMove(
|
|
92
|
+
current,
|
|
93
|
+
this.itemTargets.length,
|
|
94
|
+
event.key === "ArrowUp" ? -1 : horizontalStep(event.key));
|
|
74
95
|
break;
|
|
75
96
|
case "Home":
|
|
76
97
|
next = 0;
|
|
@@ -2,6 +2,75 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/toolbar_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/logical_scroll.ts
|
|
6
|
+
function isRtl(element) {
|
|
7
|
+
return window.getComputedStyle(element).direction === "rtl";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// src/utils/arrow_step.ts
|
|
11
|
+
function isReservedArrowChord(event, allow = []) {
|
|
12
|
+
if (!event.key.startsWith("Arrow")) return false;
|
|
13
|
+
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// src/utils/composition_tracker.ts
|
|
17
|
+
var CompositionTracker = class {
|
|
18
|
+
#observedTargets = /* @__PURE__ */ new Set();
|
|
19
|
+
#activeTargets = /* @__PURE__ */ new Set();
|
|
20
|
+
#onStart;
|
|
21
|
+
#onEnd;
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
this.#onStart = options.onStart;
|
|
24
|
+
this.#onEnd = options.onEnd;
|
|
25
|
+
}
|
|
26
|
+
/** Starts lifecycle tracking for `target`; repeated calls are idempotent. */
|
|
27
|
+
observe(target) {
|
|
28
|
+
if (this.#observedTargets.has(target)) return;
|
|
29
|
+
target.addEventListener("compositionstart", this.#handleStart);
|
|
30
|
+
target.addEventListener("compositionend", this.#handleEnd);
|
|
31
|
+
this.#observedTargets.add(target);
|
|
32
|
+
}
|
|
33
|
+
/** Stops tracking one target and clears any active composition it owned. */
|
|
34
|
+
unobserve(target) {
|
|
35
|
+
if (!this.#observedTargets.delete(target)) return;
|
|
36
|
+
target.removeEventListener("compositionstart", this.#handleStart);
|
|
37
|
+
target.removeEventListener("compositionend", this.#handleEnd);
|
|
38
|
+
this.#activeTargets.delete(target);
|
|
39
|
+
}
|
|
40
|
+
/** Releases every listener and clears state so reconnect starts cleanly. */
|
|
41
|
+
disconnect() {
|
|
42
|
+
for (const target of this.#observedTargets) {
|
|
43
|
+
target.removeEventListener("compositionstart", this.#handleStart);
|
|
44
|
+
target.removeEventListener("compositionend", this.#handleEnd);
|
|
45
|
+
}
|
|
46
|
+
this.#observedTargets.clear();
|
|
47
|
+
this.#activeTargets.clear();
|
|
48
|
+
}
|
|
49
|
+
/** True when lifecycle tracking or the current event reports composition. */
|
|
50
|
+
isComposing(event) {
|
|
51
|
+
return this.#activeTargets.size > 0 || event?.isComposing === true;
|
|
52
|
+
}
|
|
53
|
+
#handleStart = (event) => {
|
|
54
|
+
if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
|
|
55
|
+
this.#onStart?.(event);
|
|
56
|
+
};
|
|
57
|
+
#handleEnd = (event) => {
|
|
58
|
+
if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
|
|
59
|
+
this.#onEnd?.(event);
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// src/utils/focus_candidate.ts
|
|
64
|
+
function inheritsFieldsetDisabled(control) {
|
|
65
|
+
let fieldset = control.closest("fieldset[disabled]");
|
|
66
|
+
while (fieldset) {
|
|
67
|
+
const legend = Array.from(fieldset.children).find((child) => child.tagName === "LEGEND");
|
|
68
|
+
if (!legend?.contains(control)) return true;
|
|
69
|
+
fieldset = fieldset.parentElement?.closest("fieldset[disabled]") ?? null;
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
5
74
|
// src/utils/roving_tabindex.ts
|
|
6
75
|
var RovingTabindex = class {
|
|
7
76
|
/** Returns the current ordered item elements; called on every operation. */
|
|
@@ -42,6 +111,7 @@ function rovingMove(current, length, delta, wrap) {
|
|
|
42
111
|
}
|
|
43
112
|
|
|
44
113
|
// src/controllers/toolbar_controller.ts
|
|
114
|
+
var STATE_ATTRIBUTES = ["disabled", "hidden"];
|
|
45
115
|
var ToolbarController = class extends Controller {
|
|
46
116
|
static targets = ["control"];
|
|
47
117
|
static values = {
|
|
@@ -50,61 +120,190 @@ var ToolbarController = class extends Controller {
|
|
|
50
120
|
};
|
|
51
121
|
static actions = ["onKeydown"];
|
|
52
122
|
#roving = new RovingTabindex(() => this.controlTargets);
|
|
123
|
+
#composition = new CompositionTracker();
|
|
124
|
+
#observer = null;
|
|
53
125
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
126
|
+
* Live between `connect()` and `disconnect()`. Stimulus reports the *initial*
|
|
127
|
+
* targets before `connect()` and re-reports them as disconnected after
|
|
128
|
+
* `disconnect()`; gating on this keeps the target callbacks from clobbering
|
|
129
|
+
* the authored Tab stop on mount and from resurrecting one after teardown.
|
|
58
130
|
*/
|
|
131
|
+
#connected = false;
|
|
59
132
|
connect() {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
133
|
+
this.#ensureTabStop();
|
|
134
|
+
this.element.addEventListener("keydown", this.#onKeydown);
|
|
135
|
+
this.element.addEventListener("focusin", this.#onFocusin);
|
|
136
|
+
this.#composition.observe(this.element);
|
|
137
|
+
if (typeof MutationObserver !== "undefined") {
|
|
138
|
+
const observer = new MutationObserver(() => this.#ensureTabStop());
|
|
139
|
+
observer.observe(this.element, {
|
|
140
|
+
subtree: true,
|
|
141
|
+
childList: true,
|
|
142
|
+
attributes: true,
|
|
143
|
+
attributeFilter: STATE_ATTRIBUTES
|
|
144
|
+
});
|
|
145
|
+
for (let fieldset = this.element.parentElement?.closest("fieldset") ?? null; fieldset; fieldset = fieldset.parentElement?.closest("fieldset") ?? null) {
|
|
146
|
+
observer.observe(fieldset, { attributes: true, attributeFilter: ["disabled"] });
|
|
147
|
+
}
|
|
148
|
+
this.#observer = observer;
|
|
65
149
|
}
|
|
66
|
-
|
|
67
|
-
|
|
150
|
+
this.#connected = true;
|
|
151
|
+
}
|
|
152
|
+
disconnect() {
|
|
153
|
+
this.#connected = false;
|
|
154
|
+
this.element.removeEventListener("keydown", this.#onKeydown);
|
|
155
|
+
this.element.removeEventListener("focusin", this.#onFocusin);
|
|
156
|
+
this.#composition.disconnect();
|
|
157
|
+
this.#observer?.disconnect();
|
|
158
|
+
this.#observer = null;
|
|
68
159
|
}
|
|
69
|
-
/**
|
|
160
|
+
/**
|
|
161
|
+
* A control added at runtime is dropped out of the Tab sequence first — a
|
|
162
|
+
* fresh `<button>` is tabbable by default, which would make the group two Tab
|
|
163
|
+
* stops — and then the lone stop is re-established.
|
|
164
|
+
*/
|
|
165
|
+
controlTargetConnected(control) {
|
|
166
|
+
if (!this.#connected) return;
|
|
167
|
+
control.tabIndex = -1;
|
|
168
|
+
this.#ensureTabStop();
|
|
169
|
+
}
|
|
170
|
+
/** Removing the control that held the Tab stop must not orphan the group. */
|
|
171
|
+
controlTargetDisconnected() {
|
|
172
|
+
if (!this.#connected) return;
|
|
173
|
+
this.#ensureTabStop();
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Arrow/Home/End move focus and the single tab stop.
|
|
177
|
+
*
|
|
178
|
+
* Binding this per control with `data-action` is **optional** — the same
|
|
179
|
+
* handling runs from the container's delegated listener. It stays a declared
|
|
180
|
+
* action so per-control wiring keeps working; wiring both does not
|
|
181
|
+
* double-move, because the delegated pass then yields on `defaultPrevented`.
|
|
182
|
+
*/
|
|
70
183
|
onKeydown(event) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
184
|
+
this.#handleKeydown(event);
|
|
185
|
+
}
|
|
186
|
+
/** Delegated counterpart of {@link onKeydown}; bound on the container. */
|
|
187
|
+
#onKeydown = (event) => {
|
|
188
|
+
this.#handleKeydown(event);
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* Syncs the Tab stop to a control focused by other means (click, programmatic
|
|
192
|
+
* `focus()`) so Tab re-entry returns there. A non-navigable control is ignored:
|
|
193
|
+
* the lone Tab stop must never sit on something the user cannot operate.
|
|
194
|
+
*/
|
|
195
|
+
#onFocusin = (event) => {
|
|
196
|
+
const index = this.#indexOf(event.target);
|
|
197
|
+
const control = index === -1 ? void 0 : this.controlTargets[index];
|
|
198
|
+
if (!control || !this.#isNavigable(control)) return;
|
|
199
|
+
this.#roving.setActive(index);
|
|
200
|
+
};
|
|
201
|
+
#handleKeydown(event) {
|
|
202
|
+
if (event.defaultPrevented) return;
|
|
203
|
+
if (isReservedArrowChord(event)) return;
|
|
204
|
+
if (this.#composition.isComposing(event)) return;
|
|
205
|
+
this.#ensureTabStop();
|
|
206
|
+
const from = this.#indexOf(event.target);
|
|
207
|
+
if (from === -1) return;
|
|
76
208
|
const vertical = this.orientationValue === "vertical";
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
|
|
209
|
+
const rtl = !vertical && isRtl(this.element);
|
|
210
|
+
const forwardKey = vertical ? "ArrowDown" : rtl ? "ArrowLeft" : "ArrowRight";
|
|
211
|
+
const backwardKey = vertical ? "ArrowUp" : rtl ? "ArrowRight" : "ArrowLeft";
|
|
212
|
+
let target;
|
|
80
213
|
if (event.key === forwardKey) {
|
|
81
|
-
|
|
214
|
+
target = this.#nextNavigable(from, 1);
|
|
82
215
|
} else if (event.key === backwardKey) {
|
|
83
|
-
|
|
216
|
+
target = this.#nextNavigable(from, -1);
|
|
84
217
|
} else if (event.key === "Home") {
|
|
85
|
-
|
|
218
|
+
target = this.#navigableControls[0];
|
|
86
219
|
} else if (event.key === "End") {
|
|
87
|
-
|
|
220
|
+
const navigable = this.#navigableControls;
|
|
221
|
+
target = navigable[navigable.length - 1];
|
|
88
222
|
} else {
|
|
89
223
|
return;
|
|
90
224
|
}
|
|
91
225
|
event.preventDefault();
|
|
92
|
-
const target = navigable[next];
|
|
93
226
|
if (target) this.#roving.setActive(this.controlTargets.indexOf(target), { focus: true });
|
|
94
227
|
}
|
|
95
|
-
/**
|
|
228
|
+
/**
|
|
229
|
+
* Re-establishes the single Tab stop: keep the current one while it is still
|
|
230
|
+
* navigable, else hand it to the first navigable control. `-1` (no Tab stop at
|
|
231
|
+
* all) is reached only when every control is unavailable, and is recovered
|
|
232
|
+
* from as soon as one becomes navigable again. Idempotent by construction, so
|
|
233
|
+
* connect, the target callbacks, the observer, and keydown can all call it.
|
|
234
|
+
*/
|
|
235
|
+
#ensureTabStop() {
|
|
236
|
+
const active = this.#roving.activeIndex;
|
|
237
|
+
const activeEl = active === -1 ? null : this.controlTargets[active];
|
|
238
|
+
if (activeEl && this.#isNavigable(activeEl)) {
|
|
239
|
+
this.#roving.setActive(active);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const first = this.#navigableControls[0];
|
|
243
|
+
this.#roving.setActive(first ? this.controlTargets.indexOf(first) : -1);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* First navigable control strictly in the `delta` direction from `fromIndex`,
|
|
247
|
+
* scanning the **full** control list rather than the navigable subset so a
|
|
248
|
+
* non-navigable origin is never a dead end. When the direction yields nothing
|
|
249
|
+
* the origin keeps focus; when the origin itself is not navigable the first
|
|
250
|
+
* navigable control is used instead, so an arrow key always escapes.
|
|
251
|
+
*/
|
|
252
|
+
#nextNavigable(fromIndex, delta) {
|
|
253
|
+
const controls = this.controlTargets;
|
|
254
|
+
const length = controls.length;
|
|
255
|
+
const wrap = this.wrapValue ? "wrap" : "clamp";
|
|
256
|
+
let index = fromIndex;
|
|
257
|
+
for (let step = 0; step < length; step++) {
|
|
258
|
+
const next = rovingMove(index, length, delta, wrap);
|
|
259
|
+
if (next === index) break;
|
|
260
|
+
index = next;
|
|
261
|
+
const candidate = controls[index];
|
|
262
|
+
if (candidate && this.#isNavigable(candidate)) return candidate;
|
|
263
|
+
}
|
|
264
|
+
const origin = controls[fromIndex];
|
|
265
|
+
return origin && this.#isNavigable(origin) ? origin : this.#navigableControls[0];
|
|
266
|
+
}
|
|
267
|
+
/** Index in `controlTargets` of the control owning `target` (it or a descendant). */
|
|
268
|
+
#indexOf(target) {
|
|
269
|
+
const node = target;
|
|
270
|
+
if (!node) return -1;
|
|
271
|
+
return this.controlTargets.findIndex((control) => control === node || control.contains(node));
|
|
272
|
+
}
|
|
273
|
+
/** Controls eligible for the roving tab stop (excludes native disabled / hidden). */
|
|
96
274
|
get #navigableControls() {
|
|
97
275
|
return this.controlTargets.filter((control) => this.#isNavigable(control));
|
|
98
276
|
}
|
|
99
277
|
/**
|
|
100
|
-
* A control can hold the tab stop unless it is `hidden
|
|
101
|
-
*
|
|
278
|
+
* A control can hold the tab stop unless it is `hidden` or a disabled form
|
|
279
|
+
* control. `aria-disabled` remains navigable per the APG; activation suppression
|
|
280
|
+
* belongs to the control itself or its owning behavior. Native disabledness is
|
|
281
|
+
* read from the `disabled` property, narrowed by an `in` check rather than
|
|
282
|
+
* asserted since `<a>` and `<div role="button">` are legitimate controls that
|
|
283
|
+
* are unaffected by a disabled fieldset. CSS-only visibility cannot be detected
|
|
102
284
|
* headlessly and stays the consumer's responsibility.
|
|
103
285
|
*/
|
|
104
286
|
#isNavigable(control) {
|
|
105
|
-
if (control
|
|
106
|
-
if (
|
|
107
|
-
|
|
287
|
+
if (this.#isHidden(control)) return false;
|
|
288
|
+
if (!("disabled" in control)) return true;
|
|
289
|
+
if (control.disabled === true) return false;
|
|
290
|
+
return !inheritsFieldsetDisabled(control);
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Whether `control`, or anything between it and the toolbar root, is `hidden`.
|
|
294
|
+
*
|
|
295
|
+
* Reading only the control's own attribute lets an invisible control hold the
|
|
296
|
+
* single Tab stop, which takes the *whole* toolbar out of the Tab sequence — an
|
|
297
|
+
* ordinary `hidden` wrapper is enough. The walk stops at the root: a toolbar
|
|
298
|
+
* inside a `hidden` region is already out of the page's Tab order.
|
|
299
|
+
*/
|
|
300
|
+
#isHidden(control) {
|
|
301
|
+
let node = control;
|
|
302
|
+
while (node && node !== this.element) {
|
|
303
|
+
if (node.hasAttribute("hidden")) return true;
|
|
304
|
+
node = node.parentElement;
|
|
305
|
+
}
|
|
306
|
+
return false;
|
|
108
307
|
}
|
|
109
308
|
};
|
|
110
309
|
|
|
@@ -2,6 +2,11 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/transition_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/reduced_motion.ts
|
|
6
|
+
function prefersReducedMotion() {
|
|
7
|
+
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
// src/utils/safe_timeout.ts
|
|
6
11
|
var TimerRegistry = class {
|
|
7
12
|
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
@@ -55,14 +60,149 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
55
60
|
}
|
|
56
61
|
};
|
|
57
62
|
|
|
63
|
+
// src/utils/transition_completion.ts
|
|
64
|
+
function timeMs(value) {
|
|
65
|
+
const trimmed = value.trim();
|
|
66
|
+
const amount = Number.parseFloat(trimmed);
|
|
67
|
+
if (!Number.isFinite(amount)) return 0;
|
|
68
|
+
if (trimmed.endsWith("ms")) return amount;
|
|
69
|
+
if (trimmed.endsWith("s")) return amount * 1e3;
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
function cssList(value) {
|
|
73
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
74
|
+
}
|
|
75
|
+
function transitionTimings(style) {
|
|
76
|
+
const properties = cssList(style.transitionProperty);
|
|
77
|
+
const durations = cssList(style.transitionDuration).map(timeMs);
|
|
78
|
+
const delays = cssList(style.transitionDelay).map(timeMs);
|
|
79
|
+
const effectiveProperties = properties.length > 0 ? properties : Array.from({ length: Math.max(durations.length, delays.length, 1) }, () => "all");
|
|
80
|
+
const effectiveDurations = durations.length > 0 ? durations : [0];
|
|
81
|
+
const effectiveDelays = delays.length > 0 ? delays : [0];
|
|
82
|
+
return effectiveProperties.filter((property) => property !== "none").map((property, index) => ({
|
|
83
|
+
property,
|
|
84
|
+
totalMs: Math.max(
|
|
85
|
+
0,
|
|
86
|
+
(effectiveDurations[index % effectiveDurations.length] ?? 0) + (effectiveDelays[index % effectiveDelays.length] ?? 0)
|
|
87
|
+
)
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
function maxTotalMs(timings) {
|
|
91
|
+
return timings.reduce((max, { totalMs }) => Math.max(max, totalMs), 0);
|
|
92
|
+
}
|
|
93
|
+
var TransitionCompletion = class {
|
|
94
|
+
#timers = new SafeTimeout();
|
|
95
|
+
#element = null;
|
|
96
|
+
#complete = null;
|
|
97
|
+
#pendingProperties = null;
|
|
98
|
+
#deadline = 0;
|
|
99
|
+
/**
|
|
100
|
+
* Replaces any prior wait and invokes `complete` synchronously for a 0ms
|
|
101
|
+
* transition (including when `getComputedStyle` is unavailable).
|
|
102
|
+
*
|
|
103
|
+
* With a positive `options.timeoutMs` the synchronous fast-path is skipped and
|
|
104
|
+
* the override replaces the auto-computed fallback (see {@link TransitionWaitOptions}).
|
|
105
|
+
*/
|
|
106
|
+
wait(element, complete, options = {}) {
|
|
107
|
+
this.cancel();
|
|
108
|
+
const requested = options.timeoutMs ?? 0;
|
|
109
|
+
const override = Number.isFinite(requested) && requested > 0 ? requested : 0;
|
|
110
|
+
const timings = typeof window.getComputedStyle === "function" ? transitionTimings(window.getComputedStyle(element)) : [];
|
|
111
|
+
const maximum = maxTotalMs(timings);
|
|
112
|
+
if (maximum <= 0 && override <= 0) {
|
|
113
|
+
complete();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.#element = element;
|
|
117
|
+
this.#complete = complete;
|
|
118
|
+
this.#deadline = Date.now() + maximum;
|
|
119
|
+
const activeProperties = this.#activeTransitionProperties(element);
|
|
120
|
+
this.#pendingProperties = activeProperties.size > 0 ? activeProperties : this.#explicitPendingProperties(timings);
|
|
121
|
+
element.addEventListener("transitionend", this.#onTerminal);
|
|
122
|
+
element.addEventListener("transitioncancel", this.#onTerminal);
|
|
123
|
+
this.#timers.set(() => this.#finish(), override > 0 ? override : maximum + 50);
|
|
124
|
+
}
|
|
125
|
+
/** Cancels the pending wait without invoking its completion callback. */
|
|
126
|
+
cancel() {
|
|
127
|
+
this.#complete = null;
|
|
128
|
+
this.#teardown();
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Handles terminal events from the observed element only.
|
|
132
|
+
*
|
|
133
|
+
* For explicit property lists, every declared positive-time property must
|
|
134
|
+
* settle. For `all`, no reliable property set exists, so an event can finish
|
|
135
|
+
* only after the computed maximum time; the safety timeout owns the usual path.
|
|
136
|
+
*/
|
|
137
|
+
#onTerminal = (event) => {
|
|
138
|
+
if (event.target !== this.#element) return;
|
|
139
|
+
const transitionEvent = event;
|
|
140
|
+
if (transitionEvent.pseudoElement) return;
|
|
141
|
+
if (this.#pendingProperties) {
|
|
142
|
+
const propertyName = transitionEvent.propertyName;
|
|
143
|
+
const active = this.#activeTransitionProperties(this.#element);
|
|
144
|
+
if (active.has(propertyName)) return;
|
|
145
|
+
if (!this.#pendingProperties.delete(propertyName)) return;
|
|
146
|
+
if (this.#pendingProperties.size > 0) return;
|
|
147
|
+
if (active.size > 0) {
|
|
148
|
+
this.#pendingProperties = active;
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
this.#finish();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (Date.now() >= this.#deadline) this.#finish();
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
157
|
+
* Returns active CSS transition properties expanded to the names reported by
|
|
158
|
+
* terminal events. CSS animations and pseudo-element effects are excluded.
|
|
159
|
+
*/
|
|
160
|
+
#activeTransitionProperties(element) {
|
|
161
|
+
if (!element || typeof element.getAnimations !== "function") return /* @__PURE__ */ new Set();
|
|
162
|
+
try {
|
|
163
|
+
const properties = element.getAnimations().flatMap((animation) => {
|
|
164
|
+
if (animation.playState === "idle" || animation.playState === "finished") return [];
|
|
165
|
+
const effect = animation.effect;
|
|
166
|
+
if (effect?.pseudoElement) return [];
|
|
167
|
+
const target = effect?.target;
|
|
168
|
+
if (target && target !== element) return [];
|
|
169
|
+
const property = animation.transitionProperty;
|
|
170
|
+
return typeof property === "string" && property.length > 0 ? [property] : [];
|
|
171
|
+
});
|
|
172
|
+
return new Set(properties);
|
|
173
|
+
} catch {
|
|
174
|
+
return /* @__PURE__ */ new Set();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/** Returns explicit positive-time properties, or `null` for the ambiguous `all`. */
|
|
178
|
+
#explicitPendingProperties(timings) {
|
|
179
|
+
if (timings.some(({ property }) => property === "all")) return null;
|
|
180
|
+
const pending = new Set(
|
|
181
|
+
timings.filter(({ totalMs }) => totalMs > 0).map(({ property }) => property)
|
|
182
|
+
);
|
|
183
|
+
return pending.size > 0 ? pending : null;
|
|
184
|
+
}
|
|
185
|
+
/** Completes exactly once, releasing listeners and the fallback before the callback. */
|
|
186
|
+
#finish() {
|
|
187
|
+
const complete = this.#complete;
|
|
188
|
+
if (!complete) return;
|
|
189
|
+
this.#complete = null;
|
|
190
|
+
this.#teardown();
|
|
191
|
+
complete();
|
|
192
|
+
}
|
|
193
|
+
/** Releases the exact element listeners and timer owned by the current wait. */
|
|
194
|
+
#teardown() {
|
|
195
|
+
this.#timers.clearAll();
|
|
196
|
+
this.#element?.removeEventListener("transitionend", this.#onTerminal);
|
|
197
|
+
this.#element?.removeEventListener("transitioncancel", this.#onTerminal);
|
|
198
|
+
this.#element = null;
|
|
199
|
+
this.#pendingProperties = null;
|
|
200
|
+
this.#deadline = 0;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
58
204
|
// src/controllers/transition_controller.ts
|
|
59
205
|
var tokensOf = (value) => value.split(/\s+/).filter(Boolean);
|
|
60
|
-
var firstTimeMs = (value) => {
|
|
61
|
-
const first = value.split(",")[0]?.trim() ?? "";
|
|
62
|
-
const amount = Number.parseFloat(first);
|
|
63
|
-
if (Number.isNaN(amount)) return 0;
|
|
64
|
-
return first.endsWith("ms") ? amount : amount * 1e3;
|
|
65
|
-
};
|
|
66
206
|
var TransitionController = class extends Controller {
|
|
67
207
|
static values = {
|
|
68
208
|
enter: { type: String, default: "" },
|
|
@@ -75,9 +215,9 @@ var TransitionController = class extends Controller {
|
|
|
75
215
|
};
|
|
76
216
|
static actions = ["enter", "leave", "toggle"];
|
|
77
217
|
static events = ["entered", "left"];
|
|
78
|
-
|
|
218
|
+
/** Owns the cancellable completion wait (terminal events + bounded fallback). */
|
|
219
|
+
#transition = new TransitionCompletion();
|
|
79
220
|
#rafId = null;
|
|
80
|
-
#endListener = null;
|
|
81
221
|
connect() {
|
|
82
222
|
this.#strip();
|
|
83
223
|
this.element.setAttribute("data-transition-state", this.element.hidden ? "left" : "entered");
|
|
@@ -104,7 +244,7 @@ var TransitionController = class extends Controller {
|
|
|
104
244
|
const isEnter = kind === "enter";
|
|
105
245
|
if (isEnter) this.element.hidden = false;
|
|
106
246
|
this.element.setAttribute("data-transition-state", isEnter ? "entering" : "leaving");
|
|
107
|
-
if (
|
|
247
|
+
if (prefersReducedMotion()) {
|
|
108
248
|
this.#finish(kind);
|
|
109
249
|
return;
|
|
110
250
|
}
|
|
@@ -116,12 +256,13 @@ var TransitionController = class extends Controller {
|
|
|
116
256
|
this.#rafId = null;
|
|
117
257
|
this.#remove(from);
|
|
118
258
|
this.#add(to);
|
|
119
|
-
this.#
|
|
259
|
+
this.#transition.wait(this.element, () => this.#finish(kind), {
|
|
260
|
+
timeoutMs: this.timeoutValue
|
|
261
|
+
});
|
|
120
262
|
});
|
|
121
263
|
}
|
|
122
264
|
/** Settles the element into the completed state, clearing the stage classes. */
|
|
123
265
|
#finish(kind) {
|
|
124
|
-
this.#cleanupEnd();
|
|
125
266
|
this.#strip();
|
|
126
267
|
if (kind === "enter") {
|
|
127
268
|
this.element.setAttribute("data-transition-state", "entered");
|
|
@@ -132,29 +273,13 @@ var TransitionController = class extends Controller {
|
|
|
132
273
|
this.dispatch("left", { detail: {} });
|
|
133
274
|
}
|
|
134
275
|
}
|
|
135
|
-
/** Resolves on the element's own `transitionend`, with a safety timeout fallback. */
|
|
136
|
-
#awaitEnd(done) {
|
|
137
|
-
this.#endListener = (event) => {
|
|
138
|
-
if (event.target === this.element) done();
|
|
139
|
-
};
|
|
140
|
-
this.element.addEventListener("transitionend", this.#endListener);
|
|
141
|
-
const ms = this.timeoutValue > 0 ? this.timeoutValue : this.#duration();
|
|
142
|
-
this.#timers.set(done, ms);
|
|
143
|
-
}
|
|
144
|
-
#cleanupEnd() {
|
|
145
|
-
if (this.#endListener) {
|
|
146
|
-
this.element.removeEventListener("transitionend", this.#endListener);
|
|
147
|
-
this.#endListener = null;
|
|
148
|
-
}
|
|
149
|
-
this.#timers.clearAll();
|
|
150
|
-
}
|
|
151
276
|
/** Cancels any in-flight transition (interruption / teardown). */
|
|
152
277
|
#cancel() {
|
|
153
278
|
if (this.#rafId !== null) {
|
|
154
279
|
this.#cancelRaf(this.#rafId);
|
|
155
280
|
this.#rafId = null;
|
|
156
281
|
}
|
|
157
|
-
this.#
|
|
282
|
+
this.#transition.cancel();
|
|
158
283
|
this.#strip();
|
|
159
284
|
}
|
|
160
285
|
#add(...lists) {
|
|
@@ -176,13 +301,6 @@ var TransitionController = class extends Controller {
|
|
|
176
301
|
this.leaveToValue
|
|
177
302
|
);
|
|
178
303
|
}
|
|
179
|
-
/** Auto-computed safety duration (transition time + delay, with a small buffer). */
|
|
180
|
-
#duration() {
|
|
181
|
-
if (typeof window.getComputedStyle !== "function") return 0;
|
|
182
|
-
const style = window.getComputedStyle(this.element);
|
|
183
|
-
const total = firstTimeMs(style.transitionDuration) + firstTimeMs(style.transitionDelay);
|
|
184
|
-
return total > 0 ? total + 50 : 0;
|
|
185
|
-
}
|
|
186
304
|
#raf(callback) {
|
|
187
305
|
if (typeof window.requestAnimationFrame === "function") {
|
|
188
306
|
return window.requestAnimationFrame(() => callback());
|
|
@@ -193,9 +311,6 @@ var TransitionController = class extends Controller {
|
|
|
193
311
|
if (typeof window.cancelAnimationFrame === "function") window.cancelAnimationFrame(id);
|
|
194
312
|
else window.clearTimeout(id);
|
|
195
313
|
}
|
|
196
|
-
#prefersReducedMotion() {
|
|
197
|
-
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
198
|
-
}
|
|
199
314
|
};
|
|
200
315
|
|
|
201
316
|
export { TransitionController };
|