stimeo-ui 0.16.0 → 0.17.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 +31 -0
- data/README.md +8 -1
- data/dist/cable/index.js +363 -436
- data/dist/controllers/accordion_controller.js +166 -55
- data/dist/controllers/alert_dialog_controller.js +862 -221
- data/dist/controllers/announcer_controller.js +135 -198
- data/dist/controllers/aspect_ratio_controller.js +0 -10
- data/dist/controllers/auto_submit_controller.js +271 -127
- data/dist/controllers/avatar_controller.js +171 -105
- data/dist/controllers/breadcrumb_controller.js +0 -130
- data/dist/controllers/bulk_select_controller.js +262 -52
- data/dist/controllers/calendar_controller.js +175 -209
- data/dist/controllers/carousel_controller.js +461 -399
- data/dist/controllers/character_counter_controller.js +246 -156
- data/dist/controllers/checkbox_controller.js +32 -74
- data/dist/controllers/clipboard_controller.js +168 -147
- data/dist/controllers/collapsible_controller.js +301 -120
- data/dist/controllers/color_picker_controller.js +149 -139
- data/dist/controllers/combobox_controller.js +360 -133
- data/dist/controllers/command_palette_controller.js +970 -358
- data/dist/controllers/conditional_fields_controller.js +166 -124
- data/dist/controllers/confirm_controller.js +939 -248
- data/dist/controllers/context_menu_controller.js +256 -128
- data/dist/controllers/count_up_controller.js +73 -21
- data/dist/controllers/countdown_controller.js +183 -148
- data/dist/controllers/currency_input_controller.js +134 -158
- data/dist/controllers/data_grid_controller.js +114 -186
- data/dist/controllers/date_range_picker_controller.js +288 -206
- data/dist/controllers/dialog_controller.js +858 -223
- data/dist/controllers/direct_upload_controller.js +73 -153
- data/dist/controllers/dirty_form_controller.js +14 -48
- data/dist/controllers/dismissible_controller.js +175 -21
- data/dist/controllers/drawer_controller.js +933 -341
- data/dist/controllers/dropdown_controller.js +312 -82
- data/dist/controllers/editable_controller.js +158 -68
- data/dist/controllers/empty_state_controller.js +67 -53
- data/dist/controllers/file_dropzone_controller.js +311 -207
- data/dist/controllers/filter_controller.js +67 -39
- data/dist/controllers/flash_controller.js +172 -314
- data/dist/controllers/focus_controller.js +654 -240
- data/dist/controllers/form_field_controller.js +147 -132
- data/dist/controllers/form_validation_controller.js +13 -86
- data/dist/controllers/frame_loading_controller.js +264 -213
- data/dist/controllers/highlight_controller.js +107 -75
- data/dist/controllers/hover_card_controller.js +309 -118
- data/dist/controllers/idle_controller.js +267 -101
- data/dist/controllers/input_mask_controller.js +71 -72
- data/dist/controllers/intersection_controller.js +147 -106
- data/dist/controllers/lazy_frame_controller.js +9 -65
- data/dist/controllers/listbox_controller.js +306 -204
- data/dist/controllers/local_time_controller.js +54 -64
- data/dist/controllers/masonry_controller.js +70 -93
- data/dist/controllers/menu_controller.js +214 -156
- data/dist/controllers/menubar_controller.js +170 -329
- data/dist/controllers/meter_controller.js +151 -67
- data/dist/controllers/multi_select_controller.js +593 -358
- data/dist/controllers/navigation_menu_controller.js +124 -232
- data/dist/controllers/nested_form_controller.js +352 -182
- data/dist/controllers/network_status_controller.js +242 -54
- data/dist/controllers/number_input_controller.js +320 -263
- data/dist/controllers/optimistic_controller.js +16 -76
- data/dist/controllers/otp_controller.js +198 -193
- data/dist/controllers/overflow_indicator_controller.js +116 -113
- data/dist/controllers/overflow_menu_controller.js +343 -283
- data/dist/controllers/pagination_controller.js +295 -119
- data/dist/controllers/password_reveal_controller.js +389 -121
- data/dist/controllers/password_strength_controller.js +99 -153
- data/dist/controllers/persist_controller.js +237 -103
- data/dist/controllers/pointer_drag_controller.js +185 -165
- data/dist/controllers/popover_controller.js +236 -97
- data/dist/controllers/portal_controller.js +209 -76
- data/dist/controllers/preview_guard_controller.js +232 -116
- data/dist/controllers/progress_controller.js +125 -56
- data/dist/controllers/radio_group_controller.js +117 -137
- data/dist/controllers/range_slider_controller.js +192 -105
- data/dist/controllers/rating_controller.js +418 -207
- data/dist/controllers/read_more_controller.js +134 -63
- data/dist/controllers/reading_progress_controller.js +147 -135
- data/dist/controllers/relative_time_controller.js +145 -110
- data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
- data/dist/controllers/resizable_controller.js +255 -139
- data/dist/controllers/roving_controller.js +39 -84
- data/dist/controllers/scroll_area_controller.js +428 -225
- data/dist/controllers/scroll_restore_controller.js +0 -53
- data/dist/controllers/scroll_visibility_controller.js +395 -145
- data/dist/controllers/scrollspy_controller.js +181 -242
- data/dist/controllers/separator_controller.js +267 -140
- data/dist/controllers/sidebar_controller.js +1023 -386
- data/dist/controllers/skeleton_controller.js +248 -97
- data/dist/controllers/slider_controller.js +159 -79
- data/dist/controllers/smart_sticky_header_controller.js +162 -63
- data/dist/controllers/sortable_controller.js +0 -112
- data/dist/controllers/spinner_controller.js +277 -169
- data/dist/controllers/step_indicator_controller.js +99 -46
- data/dist/controllers/stepper_controller.js +114 -73
- data/dist/controllers/stick_to_bottom_controller.js +262 -138
- data/dist/controllers/sticky_observer_controller.js +214 -49
- data/dist/controllers/submit_once_controller.js +294 -205
- data/dist/controllers/switch_controller.js +33 -60
- data/dist/controllers/tabs_controller.js +35 -29
- data/dist/controllers/tags_input_controller.js +136 -145
- data/dist/controllers/textarea_autosize_controller.js +71 -58
- data/dist/controllers/theme_controller.js +130 -112
- data/dist/controllers/time_picker_controller.js +146 -80
- data/dist/controllers/toast_controller.js +186 -297
- data/dist/controllers/toggle_group_controller.js +95 -136
- data/dist/controllers/toolbar_controller.js +68 -98
- data/dist/controllers/tooltip_controller.js +158 -124
- data/dist/controllers/transition_controller.js +123 -119
- data/dist/controllers/tree_view_controller.js +150 -345
- data/dist/index.js +6370 -9346
- data/dist/positioning/index.js +148 -47
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +3 -3
|
@@ -1,14 +1,157 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
// src/controllers/menu_controller.ts
|
|
4
|
-
|
|
5
|
-
// src/utils/arrow_step.ts
|
|
6
3
|
function isReservedArrowChord(event, allow = []) {
|
|
7
4
|
if (!event.key.startsWith("Arrow")) return false;
|
|
8
5
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
9
6
|
}
|
|
10
7
|
|
|
11
|
-
|
|
8
|
+
function sharedRegistry(key, create) {
|
|
9
|
+
const symbol = Symbol.for(key);
|
|
10
|
+
const scope = globalThis;
|
|
11
|
+
const existing = scope[symbol];
|
|
12
|
+
if (existing !== void 0) return existing;
|
|
13
|
+
const registry = create();
|
|
14
|
+
Object.defineProperty(globalThis, symbol, { value: registry });
|
|
15
|
+
return registry;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isAttributeValue(value) {
|
|
19
|
+
return value === null || typeof value === "string";
|
|
20
|
+
}
|
|
21
|
+
function readLeaseRecord(element, record) {
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(element.getAttribute(record) ?? "null");
|
|
24
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
25
|
+
const [original, written, ...rest] = parsed;
|
|
26
|
+
const linked = rest[0] !== false;
|
|
27
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
28
|
+
if (!beneath.every((under) => typeof under === "string")) return void 0;
|
|
29
|
+
return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
|
|
30
|
+
} catch {
|
|
31
|
+
return void 0;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
var AttributeLease = class _AttributeLease {
|
|
35
|
+
static #registry = sharedRegistry(
|
|
36
|
+
"stimeo-ui.attribute-lease.registry.v1",
|
|
37
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
|
|
38
|
+
);
|
|
39
|
+
#attribute;
|
|
40
|
+
#record;
|
|
41
|
+
#base;
|
|
42
|
+
#held = /* @__PURE__ */ new Set();
|
|
43
|
+
constructor(attribute, owner, options = {}) {
|
|
44
|
+
this.#attribute = attribute;
|
|
45
|
+
this.#record = `data-${owner}-${attribute}-lease`;
|
|
46
|
+
this.#base = options.base === true;
|
|
47
|
+
const names = _AttributeLease.#registry.names;
|
|
48
|
+
names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
|
|
49
|
+
}
|
|
50
|
+
write(element, value) {
|
|
51
|
+
const stack = this.#stack(element);
|
|
52
|
+
const hold = this.#take(stack) ?? this.#acquire(element, stack);
|
|
53
|
+
this.#held.add(element);
|
|
54
|
+
const shows = !this.#covered(element, stack, hold);
|
|
55
|
+
hold.written = value;
|
|
56
|
+
this.#mark(element, stack);
|
|
57
|
+
if (shows) this.#reflect(element, value);
|
|
58
|
+
}
|
|
59
|
+
return(element) {
|
|
60
|
+
const held = this.#held.delete(element);
|
|
61
|
+
const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
|
|
62
|
+
const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
|
|
63
|
+
const hold = this.#take(stack);
|
|
64
|
+
if (!hold) return false;
|
|
65
|
+
const carries = element.getAttribute(this.#attribute) === hold.written;
|
|
66
|
+
const owns = carries && !this.#covered(element, stack, hold);
|
|
67
|
+
const index = stack.indexOf(hold);
|
|
68
|
+
stack.splice(index, 1);
|
|
69
|
+
const above = stack[index];
|
|
70
|
+
if (above) above.linked &&= hold.linked;
|
|
71
|
+
this.#mark(element, stack);
|
|
72
|
+
if (!owns) return true;
|
|
73
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
74
|
+
this.#reflect(element, below ? below.written : hold.original);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
returnAll() {
|
|
78
|
+
for (const element of Array.from(this.#held)) this.return(element);
|
|
79
|
+
}
|
|
80
|
+
#stack(element) {
|
|
81
|
+
const stacks = _AttributeLease.#registry.stacks;
|
|
82
|
+
const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
|
|
83
|
+
stacks.set(element, byAttribute);
|
|
84
|
+
const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
|
|
85
|
+
byAttribute.set(this.#attribute, stack);
|
|
86
|
+
return stack;
|
|
87
|
+
}
|
|
88
|
+
#seed(element) {
|
|
89
|
+
const found = [];
|
|
90
|
+
for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
|
|
91
|
+
const read = readLeaseRecord(element, record);
|
|
92
|
+
if (read) found.push({ ...read, record });
|
|
93
|
+
}
|
|
94
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
95
|
+
return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
|
|
96
|
+
lease: null,
|
|
97
|
+
record,
|
|
98
|
+
written,
|
|
99
|
+
original,
|
|
100
|
+
linked: linked && index > 0
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
#take(stack) {
|
|
104
|
+
const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
|
|
105
|
+
if (hold) hold.lease = this;
|
|
106
|
+
return hold;
|
|
107
|
+
}
|
|
108
|
+
#acquire(element, stack) {
|
|
109
|
+
const current = element.getAttribute(this.#attribute);
|
|
110
|
+
const top = stack.at(-1);
|
|
111
|
+
const bottom = stack[0];
|
|
112
|
+
const over = top?.written === current ? top : void 0;
|
|
113
|
+
const from = this.#base ? bottom : over;
|
|
114
|
+
const hold = {
|
|
115
|
+
lease: this,
|
|
116
|
+
record: this.#record,
|
|
117
|
+
written: current,
|
|
118
|
+
original: from ? from.original : current,
|
|
119
|
+
linked: !this.#base && over !== void 0
|
|
120
|
+
};
|
|
121
|
+
if (this.#base && bottom) {
|
|
122
|
+
bottom.linked = true;
|
|
123
|
+
stack.unshift(hold);
|
|
124
|
+
} else stack.push(hold);
|
|
125
|
+
return hold;
|
|
126
|
+
}
|
|
127
|
+
#covered(element, stack, hold) {
|
|
128
|
+
const current = element.getAttribute(this.#attribute);
|
|
129
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
|
|
130
|
+
}
|
|
131
|
+
#mark(element, stack) {
|
|
132
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
133
|
+
stack.forEach((hold, index) => {
|
|
134
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
135
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
136
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
137
|
+
records.set(
|
|
138
|
+
hold.record,
|
|
139
|
+
hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
for (const [name, recorded] of records) {
|
|
143
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
144
|
+
if (recorded === null) element.removeAttribute(name);
|
|
145
|
+
else element.setAttribute(name, recorded);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
#reflect(element, value) {
|
|
149
|
+
if (element.getAttribute(this.#attribute) === value) return;
|
|
150
|
+
if (value === null) element.removeAttribute(this.#attribute);
|
|
151
|
+
else element.setAttribute(this.#attribute, value);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
12
155
|
function claimsWhileFocusWithin(element) {
|
|
13
156
|
return () => {
|
|
14
157
|
const active = element.ownerDocument.activeElement;
|
|
@@ -16,17 +159,11 @@ function claimsWhileFocusWithin(element) {
|
|
|
16
159
|
};
|
|
17
160
|
}
|
|
18
161
|
var EscapeLayer = class _EscapeLayer {
|
|
19
|
-
static #registries =
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
#claims = null;
|
|
25
|
-
/**
|
|
26
|
-
* Activates this layer at the top of its document's Escape stack, installing
|
|
27
|
-
* the document's shared resolver listener if this is its first layer.
|
|
28
|
-
* Re-activating an already-active layer moves it to the top.
|
|
29
|
-
*/
|
|
162
|
+
static #registries = sharedRegistry(
|
|
163
|
+
"stimeo-ui.escape-layer.registry.v1",
|
|
164
|
+
() => /* @__PURE__ */ new WeakMap()
|
|
165
|
+
);
|
|
166
|
+
#active = null;
|
|
30
167
|
activate(ownerDocument = document, options) {
|
|
31
168
|
this.deactivate();
|
|
32
169
|
let registry = _EscapeLayer.#registries.get(ownerDocument);
|
|
@@ -35,44 +172,31 @@ var EscapeLayer = class _EscapeLayer {
|
|
|
35
172
|
_EscapeLayer.#registries.set(ownerDocument, registry);
|
|
36
173
|
ownerDocument.addEventListener("keydown", registry.onKeydown);
|
|
37
174
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
this.#
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Removes this layer from its document's Escape stack, uninstalling the
|
|
45
|
-
* shared listener when the stack empties. Safe to call when inactive.
|
|
46
|
-
*/
|
|
175
|
+
const entry = { onDismiss: options.onDismiss, claims: options.claims ?? null };
|
|
176
|
+
registry.stack.push(entry);
|
|
177
|
+
this.#active = { document: ownerDocument, entry };
|
|
178
|
+
}
|
|
47
179
|
deactivate() {
|
|
48
|
-
const
|
|
49
|
-
if (!
|
|
50
|
-
const registry = _EscapeLayer.#registries.get(
|
|
180
|
+
const active = this.#active;
|
|
181
|
+
if (!active) return;
|
|
182
|
+
const registry = _EscapeLayer.#registries.get(active.document);
|
|
51
183
|
if (registry) {
|
|
52
|
-
const index = registry.stack.lastIndexOf(
|
|
184
|
+
const index = registry.stack.lastIndexOf(active.entry);
|
|
53
185
|
if (index >= 0) registry.stack.splice(index, 1);
|
|
54
186
|
if (registry.stack.length === 0) {
|
|
55
|
-
|
|
56
|
-
_EscapeLayer.#registries.delete(
|
|
187
|
+
active.document.removeEventListener("keydown", registry.onKeydown);
|
|
188
|
+
_EscapeLayer.#registries.delete(active.document);
|
|
57
189
|
}
|
|
58
190
|
}
|
|
59
|
-
this.#
|
|
60
|
-
|
|
61
|
-
this.#claims = null;
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Whether this active layer would own a press right now: it is the topmost
|
|
65
|
-
* layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests
|
|
66
|
-
* and diagnostics — production dismissal goes through the shared listener.
|
|
67
|
-
*/
|
|
191
|
+
this.#active = null;
|
|
192
|
+
}
|
|
68
193
|
get ownsEscape() {
|
|
69
|
-
const
|
|
70
|
-
if (!
|
|
71
|
-
const registry = _EscapeLayer.#registries.get(
|
|
194
|
+
const active = this.#active;
|
|
195
|
+
if (!active) return false;
|
|
196
|
+
const registry = _EscapeLayer.#registries.get(active.document);
|
|
72
197
|
if (!registry) return false;
|
|
73
|
-
return _EscapeLayer.#resolveOwner(registry.stack) ===
|
|
198
|
+
return _EscapeLayer.#resolveOwner(registry.stack) === active.entry;
|
|
74
199
|
}
|
|
75
|
-
/** Builds a document's registry with its shared resolver listener. */
|
|
76
200
|
static #createRegistry() {
|
|
77
201
|
const registry = {
|
|
78
202
|
stack: [],
|
|
@@ -81,24 +205,22 @@ var EscapeLayer = class _EscapeLayer {
|
|
|
81
205
|
const owner = _EscapeLayer.#resolveOwner(registry.stack);
|
|
82
206
|
if (!owner) return;
|
|
83
207
|
event.preventDefault();
|
|
84
|
-
owner
|
|
208
|
+
owner.onDismiss();
|
|
85
209
|
}
|
|
86
210
|
};
|
|
87
211
|
return registry;
|
|
88
212
|
}
|
|
89
|
-
/** The topmost stack layer whose claims predicate passes, or `null`. */
|
|
90
213
|
static #resolveOwner(stack) {
|
|
91
214
|
for (let index = stack.length - 1; index >= 0; index--) {
|
|
92
215
|
const layer = stack[index];
|
|
93
216
|
if (!layer) continue;
|
|
94
|
-
if (layer
|
|
217
|
+
if (layer.claims && !layer.claims()) continue;
|
|
95
218
|
return layer;
|
|
96
219
|
}
|
|
97
220
|
return null;
|
|
98
221
|
}
|
|
99
222
|
};
|
|
100
223
|
|
|
101
|
-
// src/utils/event_owner.ts
|
|
102
224
|
function ownerIndex(candidates, node) {
|
|
103
225
|
if (!(node instanceof Node)) return -1;
|
|
104
226
|
return candidates.findIndex((candidate) => candidate.contains(node));
|
|
@@ -107,43 +229,24 @@ function ownerOf(candidates, node) {
|
|
|
107
229
|
return candidates[ownerIndex(candidates, node)] ?? null;
|
|
108
230
|
}
|
|
109
231
|
|
|
110
|
-
// src/utils/safe_timeout.ts
|
|
111
232
|
var TimerRegistry = class {
|
|
112
|
-
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
113
233
|
ids = /* @__PURE__ */ new Set();
|
|
114
|
-
/**
|
|
115
|
-
* Cancels a single tracked timer.
|
|
116
|
-
*
|
|
117
|
-
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
118
|
-
* registry), so callers can clear defensively without guarding.
|
|
119
|
-
*/
|
|
120
234
|
clear(id) {
|
|
121
235
|
if (this.ids.delete(id)) {
|
|
122
236
|
this.cancel(id);
|
|
123
237
|
}
|
|
124
238
|
}
|
|
125
|
-
/**
|
|
126
|
-
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
127
|
-
* to guarantee no timer outlives the element.
|
|
128
|
-
*/
|
|
129
239
|
clearAll() {
|
|
130
240
|
for (const id of this.ids) {
|
|
131
241
|
this.cancel(id);
|
|
132
242
|
}
|
|
133
243
|
this.ids.clear();
|
|
134
244
|
}
|
|
135
|
-
/** Number of timers currently tracked (pending). */
|
|
136
245
|
get size() {
|
|
137
246
|
return this.ids.size;
|
|
138
247
|
}
|
|
139
248
|
};
|
|
140
249
|
var SafeTimeout = class extends TimerRegistry {
|
|
141
|
-
/**
|
|
142
|
-
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
143
|
-
*
|
|
144
|
-
* The id is removed from the registry automatically when the timeout fires,
|
|
145
|
-
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
146
|
-
*/
|
|
147
250
|
set(callback, delay) {
|
|
148
251
|
const id = this.schedule(() => {
|
|
149
252
|
this.ids.delete(id);
|
|
@@ -160,7 +263,6 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
160
263
|
}
|
|
161
264
|
};
|
|
162
265
|
|
|
163
|
-
// src/utils/state_reason.ts
|
|
164
266
|
var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
|
|
165
267
|
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
166
268
|
function stateReasonFor(event) {
|
|
@@ -170,7 +272,6 @@ function stateReasonFor(event) {
|
|
|
170
272
|
return "user";
|
|
171
273
|
}
|
|
172
274
|
|
|
173
|
-
// src/controllers/menu_controller.ts
|
|
174
275
|
var MenuController = class extends Controller {
|
|
175
276
|
static targets = ["trigger", "menu", "item"];
|
|
176
277
|
static actions = [
|
|
@@ -183,29 +284,25 @@ var MenuController = class extends Controller {
|
|
|
183
284
|
];
|
|
184
285
|
static events = ["close", "open"];
|
|
185
286
|
#timers = new SafeTimeout();
|
|
186
|
-
/** Escape-stack membership while open; the shared resolver dismisses via it. */
|
|
187
287
|
#escapeLayer = new EscapeLayer();
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
* in between and may detach the item.
|
|
192
|
-
*/
|
|
288
|
+
#menuHidden = new AttributeLease("hidden", this.identifier);
|
|
289
|
+
#expanded = new AttributeLease("aria-expanded", this.identifier);
|
|
290
|
+
#menu = null;
|
|
193
291
|
#clickOwner = null;
|
|
194
|
-
/** Clicks already turned into an activation, so the two paths run it once. */
|
|
195
292
|
#activated = /* @__PURE__ */ new WeakSet();
|
|
196
|
-
|
|
293
|
+
#connected = false;
|
|
197
294
|
#reporting = false;
|
|
198
|
-
/** Starts closed and registers activation / outside-click listeners. */
|
|
199
295
|
connect() {
|
|
200
296
|
this.#close("api");
|
|
201
297
|
this.element.addEventListener("click", this.#onItemClickCapture, true);
|
|
202
298
|
this.element.addEventListener("click", this.#onDelegatedItemClick);
|
|
203
299
|
this.element.addEventListener("keydown", this.#onDelegatedItemKeydown);
|
|
204
300
|
document.addEventListener("click", this.#onOutsideClick, true);
|
|
301
|
+
this.#connected = true;
|
|
205
302
|
this.#reporting = true;
|
|
206
303
|
}
|
|
207
|
-
/** Releases the listeners, stack membership, and any pending Tab-close task. */
|
|
208
304
|
disconnect() {
|
|
305
|
+
this.#connected = false;
|
|
209
306
|
this.#reporting = false;
|
|
210
307
|
this.#timers.clearAll();
|
|
211
308
|
this.#escapeLayer.deactivate();
|
|
@@ -214,7 +311,20 @@ var MenuController = class extends Controller {
|
|
|
214
311
|
this.element.removeEventListener("keydown", this.#onDelegatedItemKeydown);
|
|
215
312
|
document.removeEventListener("click", this.#onOutsideClick, true);
|
|
216
313
|
}
|
|
217
|
-
|
|
314
|
+
triggerTargetConnected() {
|
|
315
|
+
if (this.#connected) this.#reflectTrigger();
|
|
316
|
+
}
|
|
317
|
+
triggerTargetDisconnected(trigger) {
|
|
318
|
+
if (!this.triggerTargets.includes(trigger)) this.#expanded.return(trigger);
|
|
319
|
+
if (this.#connected) this.#reflectTrigger();
|
|
320
|
+
}
|
|
321
|
+
menuTargetConnected() {
|
|
322
|
+
this.#adoptMenu();
|
|
323
|
+
}
|
|
324
|
+
menuTargetDisconnected(menu) {
|
|
325
|
+
this.#adoptMenu();
|
|
326
|
+
if (!this.menuTargets.includes(menu)) this.#menuHidden.return(menu);
|
|
327
|
+
}
|
|
218
328
|
toggle(event) {
|
|
219
329
|
if (this.#isOpen) {
|
|
220
330
|
this.close(event);
|
|
@@ -223,45 +333,52 @@ var MenuController = class extends Controller {
|
|
|
223
333
|
this.#focusFirst();
|
|
224
334
|
}
|
|
225
335
|
}
|
|
226
|
-
/** Opens the menu and reflects the expanded state on the trigger. */
|
|
227
336
|
open(event) {
|
|
228
337
|
this.#open(stateReasonFor(event));
|
|
229
338
|
}
|
|
230
|
-
/** Closes the menu and reflects the collapsed state on the trigger. */
|
|
231
339
|
close(event) {
|
|
232
340
|
this.#close(stateReasonFor(event));
|
|
233
341
|
}
|
|
234
|
-
/** Opens the menu, reflects the expanded state, and reports a move. */
|
|
235
342
|
#open(reason) {
|
|
236
343
|
this.#timers.clearAll();
|
|
237
344
|
if (!this.hasMenuTarget) return;
|
|
345
|
+
const menu = this.menuTarget;
|
|
238
346
|
const was = this.#isOpen;
|
|
239
347
|
this.#escapeLayer.activate(document, {
|
|
240
348
|
onDismiss: () => this.#closeAndRestore("escape"),
|
|
241
349
|
claims: claimsWhileFocusWithin(this.element)
|
|
242
350
|
});
|
|
243
|
-
this.
|
|
244
|
-
|
|
351
|
+
this.#menuHidden.write(menu, null);
|
|
352
|
+
this.#menu = menu;
|
|
353
|
+
if (this.hasTriggerTarget) this.#expanded.write(this.triggerTarget, "true");
|
|
245
354
|
if (!was && this.#reporting) this.dispatch("open", { detail: { reason }, cancelable: false });
|
|
246
355
|
}
|
|
247
|
-
/** Closes the menu, reflects the collapsed state, and reports a move. */
|
|
248
356
|
#close(reason) {
|
|
249
357
|
this.#timers.clearAll();
|
|
250
358
|
this.#escapeLayer.deactivate();
|
|
251
|
-
|
|
359
|
+
const menu = this.hasMenuTarget ? this.menuTarget : null;
|
|
360
|
+
this.#menu = menu;
|
|
361
|
+
if (!menu) return;
|
|
252
362
|
const was = this.#isOpen;
|
|
253
|
-
this.
|
|
254
|
-
if (this.hasTriggerTarget) this.triggerTarget
|
|
363
|
+
this.#menuHidden.write(menu, "");
|
|
364
|
+
if (this.hasTriggerTarget) this.#expanded.write(this.triggerTarget, "false");
|
|
255
365
|
if (was && this.#reporting) this.dispatch("close", { detail: { reason }, cancelable: false });
|
|
256
366
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
367
|
+
#adoptMenu() {
|
|
368
|
+
if (!this.#connected) return;
|
|
369
|
+
const menu = this.hasMenuTarget ? this.menuTarget : null;
|
|
370
|
+
const previous = this.#menu;
|
|
371
|
+
this.#menu = menu;
|
|
372
|
+
if (!menu) {
|
|
373
|
+
this.#escapeLayer.deactivate();
|
|
374
|
+
} else if (menu !== previous) {
|
|
375
|
+
this.#menuHidden.write(menu, previous?.hidden ?? true ? "" : null);
|
|
376
|
+
}
|
|
377
|
+
this.#reflectTrigger();
|
|
378
|
+
}
|
|
379
|
+
#reflectTrigger() {
|
|
380
|
+
if (this.hasTriggerTarget) this.#expanded.write(this.triggerTarget, String(this.#isOpen));
|
|
381
|
+
}
|
|
265
382
|
onTriggerKeydown(event) {
|
|
266
383
|
if (event.defaultPrevented) return;
|
|
267
384
|
if (isReservedArrowChord(event)) return;
|
|
@@ -275,15 +392,9 @@ var MenuController = class extends Controller {
|
|
|
275
392
|
this.#focusLast();
|
|
276
393
|
}
|
|
277
394
|
}
|
|
278
|
-
/** Implements roving focus and closing keys inside the menu. */
|
|
279
395
|
onItemKeydown(event) {
|
|
280
396
|
this.#handleItemKeydown(event, event.currentTarget);
|
|
281
397
|
}
|
|
282
|
-
/**
|
|
283
|
-
* The roving-focus body, shared by the per-element action and its delegated
|
|
284
|
-
* twin. `from` is the item the key belongs to: the bound element for the
|
|
285
|
-
* action, the resolved owner of the event target for the delegate.
|
|
286
|
-
*/
|
|
287
398
|
#handleItemKeydown(event, from) {
|
|
288
399
|
if (event.defaultPrevented) return;
|
|
289
400
|
if (isReservedArrowChord(event)) return;
|
|
@@ -312,18 +423,6 @@ var MenuController = class extends Controller {
|
|
|
312
423
|
break;
|
|
313
424
|
}
|
|
314
425
|
}
|
|
315
|
-
/**
|
|
316
|
-
* Closes the menu after an item is activated. Bound via `data-action`, and
|
|
317
|
-
* also reached from the delegated listener.
|
|
318
|
-
*
|
|
319
|
-
* Markup that carries the per-element action *and* gets the delegate would run
|
|
320
|
-
* this twice for one gesture. `close()` writes the state hooks (`hidden`,
|
|
321
|
-
* `aria-expanded`), and an identical reassign still queues a MutationRecord,
|
|
322
|
-
* so a second pass is observable to anyone watching them. The event is
|
|
323
|
-
* therefore claimed: the path that gets there first does the work, the other
|
|
324
|
-
* one finds it claimed and returns. A programmatic call with no event always
|
|
325
|
-
* runs.
|
|
326
|
-
*/
|
|
327
426
|
activate(event) {
|
|
328
427
|
if (event !== void 0) {
|
|
329
428
|
if (this.#activated.has(event)) return;
|
|
@@ -331,58 +430,27 @@ var MenuController = class extends Controller {
|
|
|
331
430
|
}
|
|
332
431
|
this.#closeAndRestore("select");
|
|
333
432
|
}
|
|
334
|
-
/** Closes and returns focus to the trigger (Escape / item-activation path). */
|
|
335
433
|
#closeAndRestore(reason) {
|
|
336
434
|
this.#close(reason);
|
|
337
435
|
if (this.hasTriggerTarget) this.triggerTarget.focus();
|
|
338
436
|
}
|
|
339
|
-
/** Closes the menu when a click lands outside the controller's element. */
|
|
340
437
|
#onOutsideClick = (event) => {
|
|
341
438
|
if (this.#isOpen && !this.element.contains(event.target)) this.#close("outside");
|
|
342
439
|
};
|
|
343
|
-
/**
|
|
344
|
-
* Delegated twin of {@link onItemKeydown}, bound on the controller element.
|
|
345
|
-
*
|
|
346
|
-
* An item that arrives *after* connect — `stimeo--overflow-menu` moves toolbar controls
|
|
347
|
-
* into this menu at runtime — carries whatever `data-action` its author wrote,
|
|
348
|
-
* and that is exactly the binding a consumer forgets, because the markup that
|
|
349
|
-
* declares the item lives nowhere near the menu. Listening on the container
|
|
350
|
-
* makes membership in the `item` target enough to be operable. The per-element
|
|
351
|
-
* action stays supported and is not double-handled: see
|
|
352
|
-
* {@link #handleItemKeydown}.
|
|
353
|
-
*/
|
|
354
440
|
#onDelegatedItemKeydown = (event) => {
|
|
355
441
|
const item = this.#itemFrom(event.target);
|
|
356
442
|
if (item === null) return;
|
|
357
443
|
this.#handleItemKeydown(event, item);
|
|
358
444
|
};
|
|
359
|
-
/**
|
|
360
|
-
* Delegated twin of {@link activate}, for the same reason as the keydown one.
|
|
361
|
-
*
|
|
362
|
-
* It works from the item the capture pass recorded, not from a fresh lookup:
|
|
363
|
-
* by the time a click bubbles up here, a consumer handler may already have
|
|
364
|
-
* detached the row it lives in, and `itemTargets` is a live query that would
|
|
365
|
-
* then resolve nothing — the activation would be lost and the menu left open
|
|
366
|
-
* over an item that no longer exists. `aria-disabled` items never reach this
|
|
367
|
-
* listener at all: the capture pass stops the click outright.
|
|
368
|
-
*/
|
|
369
445
|
#onDelegatedItemClick = (event) => {
|
|
370
446
|
const item = this.#clickOwner;
|
|
371
447
|
this.#clickOwner = null;
|
|
372
448
|
if (item === null) return;
|
|
373
449
|
this.activate(event);
|
|
374
450
|
};
|
|
375
|
-
/** The item target that is, or contains, `node`; `null` when it is neither. */
|
|
376
451
|
#itemFrom(node) {
|
|
377
452
|
return ownerOf(this.itemTargets, node);
|
|
378
453
|
}
|
|
379
|
-
/**
|
|
380
|
-
* First look at every click inside the controller, and the only point at which
|
|
381
|
-
* the DOM is still the one the user clicked. Two jobs: record which item owns
|
|
382
|
-
* the click for the delegate above, and stop an `aria-disabled` command before
|
|
383
|
-
* it reaches consumer handlers (native Enter/Space activation synthesizes a
|
|
384
|
-
* click and is blocked here too).
|
|
385
|
-
*/
|
|
386
454
|
#onItemClickCapture = (event) => {
|
|
387
455
|
const target = event.target;
|
|
388
456
|
if (!(target instanceof Node)) {
|
|
@@ -396,36 +464,26 @@ var MenuController = class extends Controller {
|
|
|
396
464
|
event.preventDefault();
|
|
397
465
|
event.stopImmediatePropagation();
|
|
398
466
|
};
|
|
399
|
-
/** Moves focus to the first navigable item (no-op if none). */
|
|
400
467
|
#focusFirst() {
|
|
401
468
|
if (!this.#isOpen) return;
|
|
402
469
|
this.#navigableItems[0]?.focus();
|
|
403
470
|
}
|
|
404
|
-
/** Moves focus to the last navigable item (no-op if none). */
|
|
405
471
|
#focusLast() {
|
|
406
472
|
if (!this.#isOpen) return;
|
|
407
473
|
const items = this.#navigableItems;
|
|
408
474
|
items[items.length - 1]?.focus();
|
|
409
475
|
}
|
|
410
|
-
/** Menu items eligible for roving focus (excludes disabled / hidden). */
|
|
411
476
|
get #navigableItems() {
|
|
412
477
|
return this.itemTargets.filter((item) => this.#isNavigable(item));
|
|
413
478
|
}
|
|
414
|
-
/**
|
|
415
|
-
* An item can take roving focus unless it is `hidden` or a natively `disabled`
|
|
416
|
-
* form control. `aria-disabled` remains focusable for discoverability; capture
|
|
417
|
-
* suppresses activation. CSS-only visibility is the consumer's responsibility.
|
|
418
|
-
*/
|
|
419
479
|
#isNavigable(item) {
|
|
420
480
|
if (item.hasAttribute("hidden")) return false;
|
|
421
481
|
return !item.disabled;
|
|
422
482
|
}
|
|
423
|
-
/** Whether the menu is currently visible. */
|
|
424
483
|
get #isOpen() {
|
|
425
484
|
return this.hasMenuTarget && !this.menuTarget.hidden;
|
|
426
485
|
}
|
|
427
486
|
};
|
|
428
487
|
|
|
429
488
|
export { MenuController };
|
|
430
|
-
//# sourceMappingURL=menu_controller.js.map
|
|
431
489
|
//# sourceMappingURL=menu_controller.js.map
|