stimeo-ui 0.2.1 → 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 +104 -0
- data/dist/controllers/accordion_controller.js +10 -0
- data/dist/controllers/breadcrumb_controller.js +225 -13
- data/dist/controllers/calendar_controller.js +89 -22
- data/dist/controllers/carousel_controller.js +47 -6
- data/dist/controllers/collapsible_controller.js +2 -2
- data/dist/controllers/color_picker_controller.js +46 -7
- 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/data_grid_controller.js +82 -4
- data/dist/controllers/date_range_picker_controller.js +27 -3
- data/dist/controllers/editable_controller.js +1 -0
- data/dist/controllers/form_validation_controller.js +1 -1
- data/dist/controllers/intersection_controller.js +36 -11
- data/dist/controllers/lazy_frame_controller.js +31 -10
- data/dist/controllers/listbox_controller.js +257 -53
- data/dist/controllers/local_time_controller.js +2 -2
- data/dist/controllers/menu_controller.js +104 -17
- data/dist/controllers/menubar_controller.js +415 -63
- data/dist/controllers/multi_select_controller.js +312 -29
- data/dist/controllers/navigation_menu_controller.js +154 -27
- data/dist/controllers/number_input_controller.js +7 -0
- data/dist/controllers/otp_controller.js +18 -1
- data/dist/controllers/overflow_indicator_controller.js +81 -13
- data/dist/controllers/overflow_menu_controller.js +381 -57
- data/dist/controllers/pagination_controller.js +163 -32
- 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 +32 -6
- data/dist/controllers/rating_controller.js +16 -2
- data/dist/controllers/read_more_controller.js +63 -19
- data/dist/controllers/resizable_controller.js +65 -1
- data/dist/controllers/roving_controller.js +17 -2
- data/dist/controllers/scroll_area_controller.js +86 -12
- data/dist/controllers/scroll_restore_controller.js +1 -1
- data/dist/controllers/scroll_visibility_controller.js +33 -3
- data/dist/controllers/scrollspy_controller.js +346 -73
- data/dist/controllers/separator_controller.js +9 -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/stick_to_bottom_controller.js +2 -1
- data/dist/controllers/sticky_observer_controller.js +32 -11
- data/dist/controllers/switch_controller.js +1 -0
- data/dist/controllers/tabs_controller.js +26 -3
- data/dist/controllers/tags_input_controller.js +22 -2
- data/dist/controllers/theme_controller.js +22 -3
- data/dist/controllers/time_picker_controller.js +20 -1
- data/dist/controllers/toast_controller.js +4 -5
- data/dist/controllers/toggle_group_controller.js +23 -2
- data/dist/controllers/toolbar_controller.js +230 -31
- data/dist/controllers/tree_view_controller.js +467 -51
- data/dist/index.js +3514 -689
- data/lib/stimeo/ui/version.rb +2 -3
- metadata +2 -2
|
@@ -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
|
|