stimeo-ui 0.1.0.pre.beta.3 → 0.2.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 +90 -0
- data/README.md +6 -6
- data/dist/controllers/auto_submit_controller.js +56 -21
- data/dist/controllers/character_counter_controller.js +52 -15
- data/dist/controllers/combobox_controller.js +64 -3
- data/dist/controllers/command_palette_controller.js +243 -39
- data/dist/controllers/confirm_controller.js +94 -8
- data/dist/controllers/context_menu_controller.js +191 -13
- data/dist/controllers/countdown_controller.js +2 -2
- data/dist/controllers/dialog_controller.js +94 -8
- data/dist/controllers/dropdown_controller.js +139 -16
- data/dist/controllers/focus_controller.js +94 -8
- data/dist/controllers/form_validation_controller.js +2 -2
- data/dist/controllers/hover_card_controller.js +103 -27
- data/dist/controllers/listbox_controller.js +1 -0
- data/dist/controllers/menu_controller.js +181 -8
- data/dist/controllers/otp_controller.js +62 -22
- data/dist/controllers/pointer_drag_controller.js +3 -3
- data/dist/controllers/popover_controller.js +118 -27
- data/dist/controllers/toast_controller.js +108 -29
- data/dist/controllers/tooltip_controller.js +148 -42
- data/dist/index.js +833 -348
- data/lib/stimeo/ui/version.rb +5 -4
- metadata +2 -2
|
@@ -2,6 +2,90 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/tooltip_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/escape_layer.ts
|
|
6
|
+
var EscapeLayer = class _EscapeLayer {
|
|
7
|
+
static #registries = /* @__PURE__ */ new WeakMap();
|
|
8
|
+
#ownerDocument = null;
|
|
9
|
+
/** Dismissal callback while active; `null` when inactive. */
|
|
10
|
+
#onDismiss = null;
|
|
11
|
+
/** Live predicate deciding whether the layer claims a press; `null` = always. */
|
|
12
|
+
#claims = null;
|
|
13
|
+
/**
|
|
14
|
+
* Activates this layer at the top of its document's Escape stack, installing
|
|
15
|
+
* the document's shared resolver listener if this is its first layer.
|
|
16
|
+
* Re-activating an already-active layer moves it to the top.
|
|
17
|
+
*/
|
|
18
|
+
activate(ownerDocument = document, options) {
|
|
19
|
+
this.deactivate();
|
|
20
|
+
let registry = _EscapeLayer.#registries.get(ownerDocument);
|
|
21
|
+
if (!registry) {
|
|
22
|
+
registry = _EscapeLayer.#createRegistry();
|
|
23
|
+
_EscapeLayer.#registries.set(ownerDocument, registry);
|
|
24
|
+
ownerDocument.addEventListener("keydown", registry.onKeydown);
|
|
25
|
+
}
|
|
26
|
+
registry.stack.push(this);
|
|
27
|
+
this.#ownerDocument = ownerDocument;
|
|
28
|
+
this.#onDismiss = options.onDismiss;
|
|
29
|
+
this.#claims = options.claims ?? null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Removes this layer from its document's Escape stack, uninstalling the
|
|
33
|
+
* shared listener when the stack empties. Safe to call when inactive.
|
|
34
|
+
*/
|
|
35
|
+
deactivate() {
|
|
36
|
+
const ownerDocument = this.#ownerDocument;
|
|
37
|
+
if (!ownerDocument) return;
|
|
38
|
+
const registry = _EscapeLayer.#registries.get(ownerDocument);
|
|
39
|
+
if (registry) {
|
|
40
|
+
const index = registry.stack.lastIndexOf(this);
|
|
41
|
+
if (index >= 0) registry.stack.splice(index, 1);
|
|
42
|
+
if (registry.stack.length === 0) {
|
|
43
|
+
ownerDocument.removeEventListener("keydown", registry.onKeydown);
|
|
44
|
+
_EscapeLayer.#registries.delete(ownerDocument);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
this.#ownerDocument = null;
|
|
48
|
+
this.#onDismiss = null;
|
|
49
|
+
this.#claims = null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Whether this active layer would own a press right now: it is the topmost
|
|
53
|
+
* layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests
|
|
54
|
+
* and diagnostics — production dismissal goes through the shared listener.
|
|
55
|
+
*/
|
|
56
|
+
get ownsEscape() {
|
|
57
|
+
const ownerDocument = this.#ownerDocument;
|
|
58
|
+
if (!ownerDocument) return false;
|
|
59
|
+
const registry = _EscapeLayer.#registries.get(ownerDocument);
|
|
60
|
+
if (!registry) return false;
|
|
61
|
+
return _EscapeLayer.#resolveOwner(registry.stack) === this;
|
|
62
|
+
}
|
|
63
|
+
/** Builds a document's registry with its shared resolver listener. */
|
|
64
|
+
static #createRegistry() {
|
|
65
|
+
const registry = {
|
|
66
|
+
stack: [],
|
|
67
|
+
onKeydown: (event) => {
|
|
68
|
+
if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
|
|
69
|
+
const owner = _EscapeLayer.#resolveOwner(registry.stack);
|
|
70
|
+
if (!owner) return;
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
owner.#onDismiss?.();
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
return registry;
|
|
76
|
+
}
|
|
77
|
+
/** The topmost stack layer whose claims predicate passes, or `null`. */
|
|
78
|
+
static #resolveOwner(stack) {
|
|
79
|
+
for (let index = stack.length - 1; index >= 0; index--) {
|
|
80
|
+
const layer = stack[index];
|
|
81
|
+
if (!layer) continue;
|
|
82
|
+
if (layer.#claims && !layer.#claims()) continue;
|
|
83
|
+
return layer;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
5
89
|
// src/utils/safe_timeout.ts
|
|
6
90
|
var TimerRegistry = class {
|
|
7
91
|
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
@@ -88,27 +172,39 @@ var TooltipController = class extends Controller {
|
|
|
88
172
|
hideDelay: { type: Number, default: 0 },
|
|
89
173
|
closeOnScroll: { type: Boolean, default: false }
|
|
90
174
|
};
|
|
91
|
-
static actions = ["hide", "
|
|
92
|
-
/**
|
|
175
|
+
static actions = ["hide", "show"];
|
|
176
|
+
/** Registry whose pending timers are cancelled individually with their guard ids. */
|
|
93
177
|
#timers = new SafeTimeout();
|
|
94
|
-
/**
|
|
178
|
+
/** Escape-stack membership while shown; the shared resolver dismisses via it. */
|
|
179
|
+
#escapeLayer = new EscapeLayer();
|
|
180
|
+
/** The id of the currently pending show timer, if any. */
|
|
95
181
|
#pendingShow = null;
|
|
182
|
+
/** The id of the currently pending hide timer, if any. */
|
|
96
183
|
#pendingHide = null;
|
|
184
|
+
/** Whether focus or the pointer currently requires the tooltip to persist. */
|
|
185
|
+
#focusActive = false;
|
|
186
|
+
#pointerActive = false;
|
|
97
187
|
/** Cleanup for the dismiss-on-scroll listeners while shown, or `null`. */
|
|
98
188
|
#stopScrollDismiss = null;
|
|
99
|
-
/** Starts hidden. */
|
|
189
|
+
/** Starts hidden with no stale timer or interaction state from a prior connection. */
|
|
100
190
|
connect() {
|
|
191
|
+
this.#cancelShow();
|
|
192
|
+
this.#cancelHide();
|
|
193
|
+
this.#resetInteractionState();
|
|
101
194
|
this.#conceal();
|
|
102
195
|
}
|
|
103
|
-
/** Clears timers
|
|
196
|
+
/** Clears timers, the Escape-stack membership, and scroll listeners so nothing outlives the element. */
|
|
104
197
|
disconnect() {
|
|
105
|
-
this.#
|
|
106
|
-
|
|
198
|
+
this.#cancelShow();
|
|
199
|
+
this.#cancelHide();
|
|
200
|
+
this.#resetInteractionState();
|
|
201
|
+
this.#escapeLayer.deactivate();
|
|
107
202
|
this.#stopScrollDismiss?.();
|
|
108
203
|
this.#stopScrollDismiss = null;
|
|
109
204
|
}
|
|
110
|
-
/** Shows
|
|
111
|
-
show() {
|
|
205
|
+
/** Shows after `showDelay`, recording the focus/pointer reason supplied by an action event. */
|
|
206
|
+
show(event) {
|
|
207
|
+
this.#activateInteraction(event);
|
|
112
208
|
this.#cancelHide();
|
|
113
209
|
if (this.#isVisible || this.#pendingShow !== null) return;
|
|
114
210
|
if (this.showDelayValue <= 0) {
|
|
@@ -120,8 +216,13 @@ var TooltipController = class extends Controller {
|
|
|
120
216
|
this.#reveal();
|
|
121
217
|
}, this.showDelayValue);
|
|
122
218
|
}
|
|
123
|
-
/** Hides
|
|
124
|
-
hide() {
|
|
219
|
+
/** Hides after `hideDelay` once no focus/pointer reason remains; eventless calls are explicit. */
|
|
220
|
+
hide(event) {
|
|
221
|
+
const interactionEnded = this.#deactivateInteraction(event);
|
|
222
|
+
if (interactionEnded && this.#hasActiveInteraction) {
|
|
223
|
+
this.#cancelHide();
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
125
226
|
this.#cancelShow();
|
|
126
227
|
if (!this.#isVisible || this.#pendingHide !== null) return;
|
|
127
228
|
if (this.hideDelayValue <= 0) {
|
|
@@ -133,52 +234,57 @@ var TooltipController = class extends Controller {
|
|
|
133
234
|
this.#conceal();
|
|
134
235
|
}, this.hideDelayValue);
|
|
135
236
|
}
|
|
136
|
-
/**
|
|
137
|
-
* Dismisses the tooltip on `Escape` from the trigger. The authoritative
|
|
138
|
-
* dismissal path is the document-level listener (added while shown) so a
|
|
139
|
-
* hover-triggered tooltip is dismissible regardless of focus; this handler
|
|
140
|
-
* keeps the documented `keydown->#onKeydown` binding meaningful too.
|
|
141
|
-
*/
|
|
142
|
-
onKeydown(event) {
|
|
143
|
-
if (event.key === "Escape" && this.#isVisible) {
|
|
144
|
-
event.preventDefault();
|
|
145
|
-
this.#cancelShow();
|
|
146
|
-
this.#cancelHide();
|
|
147
|
-
this.#conceal();
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
/** Reveals the content and starts watching for a dismissing `Escape`/scroll. */
|
|
237
|
+
/** Reveals the content and joins the Escape stack / starts the scroll watcher. */
|
|
151
238
|
#reveal() {
|
|
152
239
|
if (!this.hasContentTarget) return;
|
|
153
240
|
this.contentTarget.hidden = false;
|
|
154
241
|
this.contentTarget.setAttribute("data-state", "open");
|
|
155
|
-
|
|
242
|
+
this.#escapeLayer.activate(document, { onDismiss: () => this.#dismiss() });
|
|
156
243
|
if (this.closeOnScrollValue && !this.#stopScrollDismiss) {
|
|
157
|
-
this.#stopScrollDismiss = observeScrollDismiss(this.element, () =>
|
|
158
|
-
this.#cancelShow();
|
|
159
|
-
this.#cancelHide();
|
|
160
|
-
this.#conceal();
|
|
161
|
-
});
|
|
244
|
+
this.#stopScrollDismiss = observeScrollDismiss(this.element, () => this.#dismiss());
|
|
162
245
|
}
|
|
163
246
|
}
|
|
164
|
-
/** Hides the content and stops
|
|
247
|
+
/** Hides the content and leaves the Escape stack / stops the scroll watcher. */
|
|
165
248
|
#conceal() {
|
|
166
|
-
|
|
249
|
+
this.#escapeLayer.deactivate();
|
|
167
250
|
this.#stopScrollDismiss?.();
|
|
168
251
|
this.#stopScrollDismiss = null;
|
|
169
252
|
if (!this.hasContentTarget) return;
|
|
170
253
|
this.contentTarget.hidden = true;
|
|
171
254
|
this.contentTarget.setAttribute("data-state", "closed");
|
|
172
255
|
}
|
|
173
|
-
/**
|
|
174
|
-
#
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
256
|
+
/** Cancels pending timers and conceals immediately (shared Escape / scroll path). */
|
|
257
|
+
#dismiss() {
|
|
258
|
+
this.#cancelShow();
|
|
259
|
+
this.#cancelHide();
|
|
260
|
+
this.#conceal();
|
|
261
|
+
}
|
|
262
|
+
/** Records the modality whose enter/focus event requires the tooltip to stay visible. */
|
|
263
|
+
#activateInteraction(event) {
|
|
264
|
+
if (event?.type === "mouseenter") this.#pointerActive = true;
|
|
265
|
+
if (event?.type === "focusin") this.#focusActive = true;
|
|
266
|
+
}
|
|
267
|
+
/** Clears a modality on leave/blur and reports whether the event represented such a change. */
|
|
268
|
+
#deactivateInteraction(event) {
|
|
269
|
+
if (event?.type === "mouseleave") {
|
|
270
|
+
this.#pointerActive = false;
|
|
271
|
+
return true;
|
|
180
272
|
}
|
|
181
|
-
|
|
273
|
+
if (event?.type === "focusout") {
|
|
274
|
+
this.#focusActive = false;
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
/** Discards interaction reasons at a lifecycle boundary. */
|
|
280
|
+
#resetInteractionState() {
|
|
281
|
+
this.#focusActive = false;
|
|
282
|
+
this.#pointerActive = false;
|
|
283
|
+
}
|
|
284
|
+
/** Whether focus or pointer presence still requires a persistent tooltip. */
|
|
285
|
+
get #hasActiveInteraction() {
|
|
286
|
+
return this.#focusActive || this.#pointerActive;
|
|
287
|
+
}
|
|
182
288
|
/** Cancels any pending show timer. */
|
|
183
289
|
#cancelShow() {
|
|
184
290
|
if (this.#pendingShow !== null) {
|