stimeo-ui 0.2.0 → 0.2.1
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 +59 -0
- data/dist/controllers/alert_dialog_controller.js +318 -0
- data/dist/controllers/carousel_controller.js +272 -0
- data/dist/controllers/clipboard_controller.js +144 -0
- data/dist/controllers/collapsible_controller.js +327 -0
- data/dist/controllers/color_picker_controller.js +213 -0
- 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 +168 -0
- data/dist/controllers/date_range_picker_controller.js +417 -0
- data/dist/controllers/dismissible_controller.js +117 -0
- data/dist/controllers/drawer_controller.js +630 -0
- data/dist/controllers/editable_controller.js +168 -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/highlight_controller.js +6 -4
- data/dist/controllers/intersection_controller.js +41 -18
- data/dist/controllers/lazy_frame_controller.js +33 -11
- data/dist/controllers/masonry_controller.js +142 -0
- data/dist/controllers/menubar_controller.js +433 -0
- data/dist/controllers/multi_select_controller.js +472 -0
- data/dist/controllers/navigation_menu_controller.js +384 -0
- data/dist/controllers/overflow_indicator_controller.js +178 -27
- data/dist/controllers/password_reveal_controller.js +117 -0
- data/dist/controllers/range_slider_controller.js +166 -0
- data/dist/controllers/read_more_controller.js +194 -0
- data/dist/controllers/scroll_area_controller.js +15 -2
- data/dist/controllers/scroll_restore_controller.js +93 -0
- data/dist/controllers/scroll_visibility_controller.js +8 -4
- data/dist/controllers/scrollspy_controller.js +33 -11
- data/dist/controllers/separator_controller.js +87 -0
- data/dist/controllers/sidebar_controller.js +761 -0
- data/dist/controllers/stepper_controller.js +28 -12
- data/dist/controllers/stick_to_bottom_controller.js +8 -4
- data/dist/controllers/sticky_observer_controller.js +88 -20
- data/dist/controllers/tags_input_controller.js +275 -0
- data/dist/controllers/theme_controller.js +20 -10
- data/dist/controllers/time_picker_controller.js +212 -0
- data/dist/controllers/toast_controller.js +36 -9
- data/dist/controllers/transition_controller.js +153 -38
- data/dist/controllers/tree_view_controller.js +275 -0
- data/dist/index.js +811 -295
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +28 -2
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
// src/controllers/tree_view_controller.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/roving_tabindex.ts
|
|
6
|
+
var RovingTabindex = class {
|
|
7
|
+
/** Returns the current ordered item elements; called on every operation. */
|
|
8
|
+
#getItems;
|
|
9
|
+
/**
|
|
10
|
+
* @param getItems - Returns the current ordered item elements. Called on every
|
|
11
|
+
* operation so the live target list is always used.
|
|
12
|
+
*/
|
|
13
|
+
constructor(getItems) {
|
|
14
|
+
this.#getItems = getItems;
|
|
15
|
+
}
|
|
16
|
+
/** Index of the currently tabbable item (`tabindex="0"`), or `-1` if none. */
|
|
17
|
+
get activeIndex() {
|
|
18
|
+
return this.#getItems().findIndex((item) => item.tabIndex === 0);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Makes exactly the item at `index` tabbable (`tabindex="0"`) and removes every
|
|
22
|
+
* other item from the Tab sequence (`tabindex="-1"`). An out-of-range `index`
|
|
23
|
+
* (e.g. `-1`) leaves all items at `-1`, which a controller can use to express
|
|
24
|
+
* "nothing is currently tabbable".
|
|
25
|
+
*
|
|
26
|
+
* @param index - Position of the item to make tabbable.
|
|
27
|
+
* @param options - Pass `{ focus: true }` to also move DOM focus to that item.
|
|
28
|
+
*/
|
|
29
|
+
setActive(index, { focus = false } = {}) {
|
|
30
|
+
const items = this.#getItems();
|
|
31
|
+
items.forEach((item, i) => {
|
|
32
|
+
item.tabIndex = i === index ? 0 : -1;
|
|
33
|
+
});
|
|
34
|
+
if (focus) items[index]?.focus();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// src/utils/safe_timeout.ts
|
|
39
|
+
var TimerRegistry = class {
|
|
40
|
+
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
41
|
+
ids = /* @__PURE__ */ new Set();
|
|
42
|
+
/**
|
|
43
|
+
* Cancels a single tracked timer.
|
|
44
|
+
*
|
|
45
|
+
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
46
|
+
* registry), so callers can clear defensively without guarding.
|
|
47
|
+
*/
|
|
48
|
+
clear(id) {
|
|
49
|
+
if (this.ids.delete(id)) {
|
|
50
|
+
this.cancel(id);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
55
|
+
* to guarantee no timer outlives the element.
|
|
56
|
+
*/
|
|
57
|
+
clearAll() {
|
|
58
|
+
for (const id of this.ids) {
|
|
59
|
+
this.cancel(id);
|
|
60
|
+
}
|
|
61
|
+
this.ids.clear();
|
|
62
|
+
}
|
|
63
|
+
/** Number of timers currently tracked (pending). */
|
|
64
|
+
get size() {
|
|
65
|
+
return this.ids.size;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
var SafeTimeout = class extends TimerRegistry {
|
|
69
|
+
/**
|
|
70
|
+
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
71
|
+
*
|
|
72
|
+
* The id is removed from the registry automatically when the timeout fires,
|
|
73
|
+
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
74
|
+
*/
|
|
75
|
+
set(callback, delay) {
|
|
76
|
+
const id = this.schedule(() => {
|
|
77
|
+
this.ids.delete(id);
|
|
78
|
+
callback();
|
|
79
|
+
}, delay);
|
|
80
|
+
this.ids.add(id);
|
|
81
|
+
return id;
|
|
82
|
+
}
|
|
83
|
+
schedule(callback, delay) {
|
|
84
|
+
return window.setTimeout(callback, delay);
|
|
85
|
+
}
|
|
86
|
+
cancel(id) {
|
|
87
|
+
window.clearTimeout(id);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// src/controllers/tree_view_controller.ts
|
|
92
|
+
var TYPEAHEAD_TIMEOUT = 500;
|
|
93
|
+
var TreeViewController = class extends Controller {
|
|
94
|
+
static targets = ["item", "group"];
|
|
95
|
+
static actions = ["onClick", "onKeydown"];
|
|
96
|
+
static events = ["select", "toggle"];
|
|
97
|
+
#roving = new RovingTabindex(() => this.itemTargets);
|
|
98
|
+
#typeahead = "";
|
|
99
|
+
#typeaheadTimer = 0;
|
|
100
|
+
#timers = new SafeTimeout();
|
|
101
|
+
/** Establishes the single tab stop (keeps an existing one, else the first). */
|
|
102
|
+
connect() {
|
|
103
|
+
const active = this.#roving.activeIndex;
|
|
104
|
+
this.#roving.setActive(active === -1 ? 0 : active);
|
|
105
|
+
}
|
|
106
|
+
/** Clears the pending typeahead-reset timer. */
|
|
107
|
+
disconnect() {
|
|
108
|
+
this.#timers.clearAll();
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Routes tree keyboard interaction. Because `treeitem`s nest, only the handler
|
|
112
|
+
* on the nearest item to the event target acts; the same keydown bubbling to an
|
|
113
|
+
* ancestor item's handler is ignored to avoid double moves / selections.
|
|
114
|
+
*/
|
|
115
|
+
onKeydown(event) {
|
|
116
|
+
const item = event.currentTarget;
|
|
117
|
+
if (event.target.closest('[role="treeitem"]') !== item) return;
|
|
118
|
+
switch (event.key) {
|
|
119
|
+
case "ArrowDown":
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
this.#moveBy(item, 1);
|
|
122
|
+
break;
|
|
123
|
+
case "ArrowUp":
|
|
124
|
+
event.preventDefault();
|
|
125
|
+
this.#moveBy(item, -1);
|
|
126
|
+
break;
|
|
127
|
+
case "ArrowRight":
|
|
128
|
+
event.preventDefault();
|
|
129
|
+
this.#expandOrEnter(item);
|
|
130
|
+
break;
|
|
131
|
+
case "ArrowLeft":
|
|
132
|
+
event.preventDefault();
|
|
133
|
+
this.#collapseOrLeave(item);
|
|
134
|
+
break;
|
|
135
|
+
case "Home":
|
|
136
|
+
event.preventDefault();
|
|
137
|
+
this.#focusEdge(0);
|
|
138
|
+
break;
|
|
139
|
+
case "End":
|
|
140
|
+
event.preventDefault();
|
|
141
|
+
this.#focusEdge(-1);
|
|
142
|
+
break;
|
|
143
|
+
case "Enter":
|
|
144
|
+
case " ":
|
|
145
|
+
event.preventDefault();
|
|
146
|
+
this.#select(item);
|
|
147
|
+
break;
|
|
148
|
+
default:
|
|
149
|
+
if (this.#isPrintable(event)) {
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
this.#typeaheadTo(item, event.key);
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/** Selects the clicked item (nearest to the target only). */
|
|
157
|
+
onClick(event) {
|
|
158
|
+
const item = event.currentTarget;
|
|
159
|
+
if (event.target.closest('[role="treeitem"]') !== item) return;
|
|
160
|
+
this.#focusItem(item);
|
|
161
|
+
this.#select(item);
|
|
162
|
+
}
|
|
163
|
+
/** Moves focus to the next (`delta=1`) or previous visible item, if any. */
|
|
164
|
+
#moveBy(item, delta) {
|
|
165
|
+
const visible = this.#visibleItems;
|
|
166
|
+
const current = visible.indexOf(item);
|
|
167
|
+
const next = visible[current + delta];
|
|
168
|
+
if (next) this.#focusItem(next);
|
|
169
|
+
}
|
|
170
|
+
/** `ArrowRight`: expand a collapsed parent, else step into the first child. */
|
|
171
|
+
#expandOrEnter(item) {
|
|
172
|
+
const group = this.#childGroup(item);
|
|
173
|
+
if (!group) return;
|
|
174
|
+
if (this.#isExpanded(item)) {
|
|
175
|
+
const firstChild = group.querySelector(':scope > [role="treeitem"]');
|
|
176
|
+
if (firstChild) this.#focusItem(firstChild);
|
|
177
|
+
} else {
|
|
178
|
+
this.#setExpanded(item, true);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/** `ArrowLeft`: collapse an expanded parent, else step out to the parent item. */
|
|
182
|
+
#collapseOrLeave(item) {
|
|
183
|
+
if (this.#childGroup(item) && this.#isExpanded(item)) {
|
|
184
|
+
this.#setExpanded(item, false);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const parent = this.#parentItem(item);
|
|
188
|
+
if (parent) this.#focusItem(parent);
|
|
189
|
+
}
|
|
190
|
+
/** Focuses the first (`0`) or last (`-1`) visible item. */
|
|
191
|
+
#focusEdge(index) {
|
|
192
|
+
const visible = this.#visibleItems;
|
|
193
|
+
const target = index < 0 ? visible[visible.length - 1] : visible[index];
|
|
194
|
+
if (target) this.#focusItem(target);
|
|
195
|
+
}
|
|
196
|
+
/** Applies single selection and dispatches `select`. */
|
|
197
|
+
#select(item) {
|
|
198
|
+
for (const candidate of this.itemTargets) {
|
|
199
|
+
candidate.setAttribute("aria-selected", candidate === item ? "true" : "false");
|
|
200
|
+
}
|
|
201
|
+
this.dispatch("select", { detail: { item } });
|
|
202
|
+
}
|
|
203
|
+
/** Toggles `aria-expanded` + the child group's `hidden`, dispatching `toggle`. */
|
|
204
|
+
#setExpanded(item, expanded) {
|
|
205
|
+
const group = this.#childGroup(item);
|
|
206
|
+
if (!group) return;
|
|
207
|
+
item.setAttribute("aria-expanded", String(expanded));
|
|
208
|
+
group.hidden = !expanded;
|
|
209
|
+
this.dispatch("toggle", { detail: { item, expanded } });
|
|
210
|
+
}
|
|
211
|
+
/** Makes `item` the single tab stop and moves DOM focus to it. */
|
|
212
|
+
#focusItem(item) {
|
|
213
|
+
const index = this.itemTargets.indexOf(item);
|
|
214
|
+
if (index !== -1) this.#roving.setActive(index, { focus: true });
|
|
215
|
+
}
|
|
216
|
+
/** Advances the typeahead buffer and focuses the next matching visible item. */
|
|
217
|
+
#typeaheadTo(item, char) {
|
|
218
|
+
this.#timers.clear(this.#typeaheadTimer);
|
|
219
|
+
this.#typeahead += char.toLowerCase();
|
|
220
|
+
this.#typeaheadTimer = this.#timers.set(() => {
|
|
221
|
+
this.#typeahead = "";
|
|
222
|
+
}, TYPEAHEAD_TIMEOUT);
|
|
223
|
+
const visible = this.#visibleItems;
|
|
224
|
+
const start = visible.indexOf(item);
|
|
225
|
+
for (let offset = 1; offset <= visible.length; offset += 1) {
|
|
226
|
+
const candidate = visible[(start + offset) % visible.length];
|
|
227
|
+
if (candidate && this.#label(candidate).startsWith(this.#typeahead)) {
|
|
228
|
+
this.#focusItem(candidate);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/** The visible items: those with no collapsed (`hidden`) ancestor group. */
|
|
234
|
+
get #visibleItems() {
|
|
235
|
+
return this.itemTargets.filter((item) => {
|
|
236
|
+
let node = item.parentElement;
|
|
237
|
+
while (node && node !== this.element) {
|
|
238
|
+
if (node.matches('[role="group"]') && node.hidden) return false;
|
|
239
|
+
node = node.parentElement;
|
|
240
|
+
}
|
|
241
|
+
return true;
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
/** The child `group` owned directly by `item`, or `null` for a leaf. */
|
|
245
|
+
#childGroup(item) {
|
|
246
|
+
return item.querySelector(':scope > [role="group"]');
|
|
247
|
+
}
|
|
248
|
+
/** The nearest ancestor `treeitem`, or `null` at the root level. */
|
|
249
|
+
#parentItem(item) {
|
|
250
|
+
return item.parentElement?.closest('[role="treeitem"]') ?? null;
|
|
251
|
+
}
|
|
252
|
+
/** Whether a parent item is currently expanded. */
|
|
253
|
+
#isExpanded(item) {
|
|
254
|
+
return item.getAttribute("aria-expanded") === "true";
|
|
255
|
+
}
|
|
256
|
+
/** `item`'s own label text (excluding any nested child group), lowercased. */
|
|
257
|
+
#label(item) {
|
|
258
|
+
let text = "";
|
|
259
|
+
for (const node of Array.from(item.childNodes)) {
|
|
260
|
+
if (node.nodeType === Node.ELEMENT_NODE && node.matches('[role="group"]')) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
text += node.textContent ?? "";
|
|
264
|
+
}
|
|
265
|
+
return text.trim().toLowerCase();
|
|
266
|
+
}
|
|
267
|
+
/** Whether `event.key` is a single printable character (no modifier chord). */
|
|
268
|
+
#isPrintable(event) {
|
|
269
|
+
return event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
export { TreeViewController };
|
|
274
|
+
//# sourceMappingURL=tree_view_controller.js.map
|
|
275
|
+
//# sourceMappingURL=tree_view_controller.js.map
|