stimeo-ui 0.15.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 +209 -0
- data/README.md +128 -1
- data/dist/cable/index.js +392 -417
- data/dist/controllers/accordion_controller.js +228 -28
- data/dist/controllers/alert_dialog_controller.js +862 -221
- data/dist/controllers/announcer_controller.js +135 -191
- data/dist/controllers/aspect_ratio_controller.js +0 -10
- data/dist/controllers/auto_submit_controller.js +295 -110
- data/dist/controllers/avatar_controller.js +172 -106
- data/dist/controllers/breadcrumb_controller.js +0 -129
- data/dist/controllers/bulk_select_controller.js +264 -53
- data/dist/controllers/calendar_controller.js +346 -180
- data/dist/controllers/carousel_controller.js +587 -300
- data/dist/controllers/character_counter_controller.js +271 -143
- data/dist/controllers/checkbox_controller.js +77 -75
- data/dist/controllers/clipboard_controller.js +187 -108
- data/dist/controllers/collapsible_controller.js +374 -108
- data/dist/controllers/color_picker_controller.js +183 -127
- data/dist/controllers/combobox_controller.js +426 -109
- data/dist/controllers/command_palette_controller.js +994 -350
- data/dist/controllers/conditional_fields_controller.js +212 -125
- data/dist/controllers/confirm_controller.js +939 -245
- data/dist/controllers/context_menu_controller.js +275 -127
- data/dist/controllers/count_up_controller.js +73 -21
- data/dist/controllers/countdown_controller.js +235 -97
- data/dist/controllers/currency_input_controller.js +241 -118
- data/dist/controllers/data_grid_controller.js +229 -135
- data/dist/controllers/date_range_picker_controller.js +359 -165
- data/dist/controllers/dialog_controller.js +887 -225
- data/dist/controllers/direct_upload_controller.js +80 -142
- data/dist/controllers/dirty_form_controller.js +26 -47
- data/dist/controllers/dismissible_controller.js +175 -20
- data/dist/controllers/drawer_controller.js +963 -336
- data/dist/controllers/dropdown_controller.js +342 -85
- data/dist/controllers/editable_controller.js +182 -58
- data/dist/controllers/empty_state_controller.js +67 -53
- data/dist/controllers/file_dropzone_controller.js +426 -229
- data/dist/controllers/filter_controller.js +78 -36
- data/dist/controllers/flash_controller.js +328 -211
- data/dist/controllers/focus_controller.js +654 -239
- data/dist/controllers/form_field_controller.js +153 -136
- data/dist/controllers/form_validation_controller.js +29 -96
- data/dist/controllers/frame_loading_controller.js +263 -198
- data/dist/controllers/highlight_controller.js +119 -70
- data/dist/controllers/hover_card_controller.js +335 -118
- data/dist/controllers/idle_controller.js +316 -65
- data/dist/controllers/input_mask_controller.js +116 -61
- data/dist/controllers/intersection_controller.js +147 -103
- data/dist/controllers/lazy_frame_controller.js +9 -56
- data/dist/controllers/listbox_controller.js +413 -153
- data/dist/controllers/local_time_controller.js +54 -64
- data/dist/controllers/masonry_controller.js +70 -95
- data/dist/controllers/menu_controller.js +254 -167
- data/dist/controllers/menubar_controller.js +206 -331
- data/dist/controllers/meter_controller.js +159 -71
- data/dist/controllers/multi_select_controller.js +797 -388
- data/dist/controllers/navigation_menu_controller.js +156 -231
- data/dist/controllers/nested_form_controller.js +373 -174
- data/dist/controllers/network_status_controller.js +241 -45
- data/dist/controllers/number_input_controller.js +387 -227
- data/dist/controllers/optimistic_controller.js +28 -75
- data/dist/controllers/otp_controller.js +344 -196
- data/dist/controllers/overflow_indicator_controller.js +167 -93
- data/dist/controllers/overflow_menu_controller.js +408 -277
- data/dist/controllers/pagination_controller.js +342 -120
- data/dist/controllers/password_reveal_controller.js +424 -99
- data/dist/controllers/password_strength_controller.js +99 -153
- data/dist/controllers/persist_controller.js +252 -96
- data/dist/controllers/pointer_drag_controller.js +263 -164
- data/dist/controllers/popover_controller.js +267 -94
- data/dist/controllers/portal_controller.js +209 -72
- data/dist/controllers/preview_guard_controller.js +243 -112
- data/dist/controllers/progress_controller.js +132 -59
- data/dist/controllers/radio_group_controller.js +138 -133
- data/dist/controllers/range_slider_controller.js +245 -112
- data/dist/controllers/rating_controller.js +429 -194
- data/dist/controllers/read_more_controller.js +185 -47
- 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 +294 -118
- data/dist/controllers/roving_controller.js +39 -84
- data/dist/controllers/scroll_area_controller.js +428 -221
- data/dist/controllers/scroll_restore_controller.js +0 -53
- data/dist/controllers/scroll_visibility_controller.js +395 -146
- data/dist/controllers/scrollspy_controller.js +181 -236
- data/dist/controllers/separator_controller.js +288 -132
- data/dist/controllers/sidebar_controller.js +1071 -375
- data/dist/controllers/skeleton_controller.js +248 -92
- data/dist/controllers/slider_controller.js +211 -96
- data/dist/controllers/smart_sticky_header_controller.js +163 -54
- data/dist/controllers/sortable_controller.js +13 -110
- data/dist/controllers/spinner_controller.js +277 -161
- data/dist/controllers/step_indicator_controller.js +107 -53
- data/dist/controllers/stepper_controller.js +171 -48
- data/dist/controllers/stick_to_bottom_controller.js +310 -116
- data/dist/controllers/sticky_observer_controller.js +214 -49
- data/dist/controllers/submit_once_controller.js +294 -192
- data/dist/controllers/switch_controller.js +107 -43
- data/dist/controllers/tabs_controller.js +50 -25
- data/dist/controllers/tags_input_controller.js +290 -149
- data/dist/controllers/textarea_autosize_controller.js +71 -58
- data/dist/controllers/theme_controller.js +148 -80
- data/dist/controllers/time_picker_controller.js +162 -81
- data/dist/controllers/toast_controller.js +365 -193
- data/dist/controllers/toggle_group_controller.js +194 -99
- data/dist/controllers/toolbar_controller.js +92 -90
- data/dist/controllers/tooltip_controller.js +185 -125
- data/dist/controllers/transition_controller.js +123 -115
- data/dist/controllers/tree_view_controller.js +248 -290
- data/dist/index.js +8649 -8572
- data/dist/positioning/index.js +148 -45
- 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,15 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
160
263
|
}
|
|
161
264
|
};
|
|
162
265
|
|
|
163
|
-
|
|
266
|
+
var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
|
|
267
|
+
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
268
|
+
function stateReasonFor(event) {
|
|
269
|
+
if (!event) return "api";
|
|
270
|
+
if (FOCUS_EVENTS.has(event.type)) return "focus";
|
|
271
|
+
if (POINTER_EVENTS.has(event.type)) return "pointer";
|
|
272
|
+
return "user";
|
|
273
|
+
}
|
|
274
|
+
|
|
164
275
|
var MenuController = class extends Controller {
|
|
165
276
|
static targets = ["trigger", "menu", "item"];
|
|
166
277
|
static actions = [
|
|
@@ -171,27 +282,28 @@ var MenuController = class extends Controller {
|
|
|
171
282
|
"open",
|
|
172
283
|
"toggle"
|
|
173
284
|
];
|
|
285
|
+
static events = ["close", "open"];
|
|
174
286
|
#timers = new SafeTimeout();
|
|
175
|
-
/** Escape-stack membership while open; the shared resolver dismisses via it. */
|
|
176
287
|
#escapeLayer = new EscapeLayer();
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
* in between and may detach the item.
|
|
181
|
-
*/
|
|
288
|
+
#menuHidden = new AttributeLease("hidden", this.identifier);
|
|
289
|
+
#expanded = new AttributeLease("aria-expanded", this.identifier);
|
|
290
|
+
#menu = null;
|
|
182
291
|
#clickOwner = null;
|
|
183
|
-
/** Clicks already turned into an activation, so the two paths run it once. */
|
|
184
292
|
#activated = /* @__PURE__ */ new WeakSet();
|
|
185
|
-
|
|
293
|
+
#connected = false;
|
|
294
|
+
#reporting = false;
|
|
186
295
|
connect() {
|
|
187
|
-
this
|
|
296
|
+
this.#close("api");
|
|
188
297
|
this.element.addEventListener("click", this.#onItemClickCapture, true);
|
|
189
298
|
this.element.addEventListener("click", this.#onDelegatedItemClick);
|
|
190
299
|
this.element.addEventListener("keydown", this.#onDelegatedItemKeydown);
|
|
191
300
|
document.addEventListener("click", this.#onOutsideClick, true);
|
|
301
|
+
this.#connected = true;
|
|
302
|
+
this.#reporting = true;
|
|
192
303
|
}
|
|
193
|
-
/** Releases the listeners, stack membership, and any pending Tab-close task. */
|
|
194
304
|
disconnect() {
|
|
305
|
+
this.#connected = false;
|
|
306
|
+
this.#reporting = false;
|
|
195
307
|
this.#timers.clearAll();
|
|
196
308
|
this.#escapeLayer.deactivate();
|
|
197
309
|
this.element.removeEventListener("click", this.#onItemClickCapture, true);
|
|
@@ -199,64 +311,90 @@ var MenuController = class extends Controller {
|
|
|
199
311
|
this.element.removeEventListener("keydown", this.#onDelegatedItemKeydown);
|
|
200
312
|
document.removeEventListener("click", this.#onOutsideClick, true);
|
|
201
313
|
}
|
|
202
|
-
|
|
203
|
-
|
|
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
|
+
}
|
|
328
|
+
toggle(event) {
|
|
204
329
|
if (this.#isOpen) {
|
|
205
|
-
this.close();
|
|
330
|
+
this.close(event);
|
|
206
331
|
} else {
|
|
207
|
-
this.open();
|
|
332
|
+
this.open(event);
|
|
208
333
|
this.#focusFirst();
|
|
209
334
|
}
|
|
210
335
|
}
|
|
211
|
-
|
|
212
|
-
|
|
336
|
+
open(event) {
|
|
337
|
+
this.#open(stateReasonFor(event));
|
|
338
|
+
}
|
|
339
|
+
close(event) {
|
|
340
|
+
this.#close(stateReasonFor(event));
|
|
341
|
+
}
|
|
342
|
+
#open(reason) {
|
|
213
343
|
this.#timers.clearAll();
|
|
214
344
|
if (!this.hasMenuTarget) return;
|
|
345
|
+
const menu = this.menuTarget;
|
|
346
|
+
const was = this.#isOpen;
|
|
215
347
|
this.#escapeLayer.activate(document, {
|
|
216
|
-
onDismiss: () => this.#closeAndRestore(),
|
|
348
|
+
onDismiss: () => this.#closeAndRestore("escape"),
|
|
217
349
|
claims: claimsWhileFocusWithin(this.element)
|
|
218
350
|
});
|
|
219
|
-
this.
|
|
220
|
-
|
|
351
|
+
this.#menuHidden.write(menu, null);
|
|
352
|
+
this.#menu = menu;
|
|
353
|
+
if (this.hasTriggerTarget) this.#expanded.write(this.triggerTarget, "true");
|
|
354
|
+
if (!was && this.#reporting) this.dispatch("open", { detail: { reason }, cancelable: false });
|
|
221
355
|
}
|
|
222
|
-
|
|
223
|
-
close() {
|
|
356
|
+
#close(reason) {
|
|
224
357
|
this.#timers.clearAll();
|
|
225
358
|
this.#escapeLayer.deactivate();
|
|
226
|
-
|
|
227
|
-
this
|
|
228
|
-
if (
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
359
|
+
const menu = this.hasMenuTarget ? this.menuTarget : null;
|
|
360
|
+
this.#menu = menu;
|
|
361
|
+
if (!menu) return;
|
|
362
|
+
const was = this.#isOpen;
|
|
363
|
+
this.#menuHidden.write(menu, "");
|
|
364
|
+
if (this.hasTriggerTarget) this.#expanded.write(this.triggerTarget, "false");
|
|
365
|
+
if (was && this.#reporting) this.dispatch("close", { detail: { reason }, cancelable: false });
|
|
366
|
+
}
|
|
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
|
+
}
|
|
238
382
|
onTriggerKeydown(event) {
|
|
239
383
|
if (event.defaultPrevented) return;
|
|
240
384
|
if (isReservedArrowChord(event)) return;
|
|
241
385
|
if (event.key === "ArrowDown") {
|
|
242
386
|
event.preventDefault();
|
|
243
|
-
this.open();
|
|
387
|
+
this.open(event);
|
|
244
388
|
this.#focusFirst();
|
|
245
389
|
} else if (event.key === "ArrowUp") {
|
|
246
390
|
event.preventDefault();
|
|
247
|
-
this.open();
|
|
391
|
+
this.open(event);
|
|
248
392
|
this.#focusLast();
|
|
249
393
|
}
|
|
250
394
|
}
|
|
251
|
-
/** Implements roving focus and closing keys inside the menu. */
|
|
252
395
|
onItemKeydown(event) {
|
|
253
396
|
this.#handleItemKeydown(event, event.currentTarget);
|
|
254
397
|
}
|
|
255
|
-
/**
|
|
256
|
-
* The roving-focus body, shared by the per-element action and its delegated
|
|
257
|
-
* twin. `from` is the item the key belongs to: the bound element for the
|
|
258
|
-
* action, the resolved owner of the event target for the delegate.
|
|
259
|
-
*/
|
|
260
398
|
#handleItemKeydown(event, from) {
|
|
261
399
|
if (event.defaultPrevented) return;
|
|
262
400
|
if (isReservedArrowChord(event)) return;
|
|
@@ -281,81 +419,38 @@ var MenuController = class extends Controller {
|
|
|
281
419
|
break;
|
|
282
420
|
case "Tab":
|
|
283
421
|
this.#timers.clearAll();
|
|
284
|
-
this.#timers.set(() => this
|
|
422
|
+
this.#timers.set(() => this.#close("focus"), 0);
|
|
285
423
|
break;
|
|
286
424
|
}
|
|
287
425
|
}
|
|
288
|
-
/**
|
|
289
|
-
* Closes the menu after an item is activated. Bound via `data-action`, and
|
|
290
|
-
* also reached from the delegated listener.
|
|
291
|
-
*
|
|
292
|
-
* Markup that carries the per-element action *and* gets the delegate would run
|
|
293
|
-
* this twice for one gesture. `close()` writes the state hooks (`hidden`,
|
|
294
|
-
* `aria-expanded`), and an identical reassign still queues a MutationRecord,
|
|
295
|
-
* so a second pass is observable to anyone watching them. The event is
|
|
296
|
-
* therefore claimed: the path that gets there first does the work, the other
|
|
297
|
-
* one finds it claimed and returns. A programmatic call with no event always
|
|
298
|
-
* runs.
|
|
299
|
-
*/
|
|
300
426
|
activate(event) {
|
|
301
427
|
if (event !== void 0) {
|
|
302
428
|
if (this.#activated.has(event)) return;
|
|
303
429
|
this.#activated.add(event);
|
|
304
430
|
}
|
|
305
|
-
this.#closeAndRestore();
|
|
431
|
+
this.#closeAndRestore("select");
|
|
306
432
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
this.close();
|
|
433
|
+
#closeAndRestore(reason) {
|
|
434
|
+
this.#close(reason);
|
|
310
435
|
if (this.hasTriggerTarget) this.triggerTarget.focus();
|
|
311
436
|
}
|
|
312
|
-
/** Closes the menu when a click lands outside the controller's element. */
|
|
313
437
|
#onOutsideClick = (event) => {
|
|
314
|
-
if (this.#isOpen && !this.element.contains(event.target)) this
|
|
438
|
+
if (this.#isOpen && !this.element.contains(event.target)) this.#close("outside");
|
|
315
439
|
};
|
|
316
|
-
/**
|
|
317
|
-
* Delegated twin of {@link onItemKeydown}, bound on the controller element.
|
|
318
|
-
*
|
|
319
|
-
* An item that arrives *after* connect — Overflow Menu moves toolbar controls
|
|
320
|
-
* into this menu at runtime — carries whatever `data-action` its author wrote,
|
|
321
|
-
* and that is exactly the binding a consumer forgets, because the markup that
|
|
322
|
-
* declares the item lives nowhere near the menu. Listening on the container
|
|
323
|
-
* makes membership in the `item` target enough to be operable. The per-element
|
|
324
|
-
* action stays supported and is not double-handled: see
|
|
325
|
-
* {@link #handleItemKeydown}.
|
|
326
|
-
*/
|
|
327
440
|
#onDelegatedItemKeydown = (event) => {
|
|
328
441
|
const item = this.#itemFrom(event.target);
|
|
329
442
|
if (item === null) return;
|
|
330
443
|
this.#handleItemKeydown(event, item);
|
|
331
444
|
};
|
|
332
|
-
/**
|
|
333
|
-
* Delegated twin of {@link activate}, for the same reason as the keydown one.
|
|
334
|
-
*
|
|
335
|
-
* It works from the item the capture pass recorded, not from a fresh lookup:
|
|
336
|
-
* by the time a click bubbles up here, a consumer handler may already have
|
|
337
|
-
* detached the row it lives in, and `itemTargets` is a live query that would
|
|
338
|
-
* then resolve nothing — the activation would be lost and the menu left open
|
|
339
|
-
* over an item that no longer exists. `aria-disabled` items never reach this
|
|
340
|
-
* listener at all: the capture pass stops the click outright.
|
|
341
|
-
*/
|
|
342
445
|
#onDelegatedItemClick = (event) => {
|
|
343
446
|
const item = this.#clickOwner;
|
|
344
447
|
this.#clickOwner = null;
|
|
345
448
|
if (item === null) return;
|
|
346
449
|
this.activate(event);
|
|
347
450
|
};
|
|
348
|
-
/** The item target that is, or contains, `node`; `null` when it is neither. */
|
|
349
451
|
#itemFrom(node) {
|
|
350
452
|
return ownerOf(this.itemTargets, node);
|
|
351
453
|
}
|
|
352
|
-
/**
|
|
353
|
-
* First look at every click inside the controller, and the only point at which
|
|
354
|
-
* the DOM is still the one the user clicked. Two jobs: record which item owns
|
|
355
|
-
* the click for the delegate above, and stop an `aria-disabled` command before
|
|
356
|
-
* it reaches consumer handlers (native Enter/Space activation synthesizes a
|
|
357
|
-
* click and is blocked here too).
|
|
358
|
-
*/
|
|
359
454
|
#onItemClickCapture = (event) => {
|
|
360
455
|
const target = event.target;
|
|
361
456
|
if (!(target instanceof Node)) {
|
|
@@ -369,34 +464,26 @@ var MenuController = class extends Controller {
|
|
|
369
464
|
event.preventDefault();
|
|
370
465
|
event.stopImmediatePropagation();
|
|
371
466
|
};
|
|
372
|
-
/** Moves focus to the first navigable item (no-op if none). */
|
|
373
467
|
#focusFirst() {
|
|
468
|
+
if (!this.#isOpen) return;
|
|
374
469
|
this.#navigableItems[0]?.focus();
|
|
375
470
|
}
|
|
376
|
-
/** Moves focus to the last navigable item (no-op if none). */
|
|
377
471
|
#focusLast() {
|
|
472
|
+
if (!this.#isOpen) return;
|
|
378
473
|
const items = this.#navigableItems;
|
|
379
474
|
items[items.length - 1]?.focus();
|
|
380
475
|
}
|
|
381
|
-
/** Menu items eligible for roving focus (excludes disabled / hidden). */
|
|
382
476
|
get #navigableItems() {
|
|
383
477
|
return this.itemTargets.filter((item) => this.#isNavigable(item));
|
|
384
478
|
}
|
|
385
|
-
/**
|
|
386
|
-
* An item can take roving focus unless it is `hidden` or a natively `disabled`
|
|
387
|
-
* form control. `aria-disabled` remains focusable for discoverability; capture
|
|
388
|
-
* suppresses activation. CSS-only visibility is the consumer's responsibility.
|
|
389
|
-
*/
|
|
390
479
|
#isNavigable(item) {
|
|
391
480
|
if (item.hasAttribute("hidden")) return false;
|
|
392
481
|
return !item.disabled;
|
|
393
482
|
}
|
|
394
|
-
/** Whether the menu is currently visible. */
|
|
395
483
|
get #isOpen() {
|
|
396
484
|
return this.hasMenuTarget && !this.menuTarget.hidden;
|
|
397
485
|
}
|
|
398
486
|
};
|
|
399
487
|
|
|
400
488
|
export { MenuController };
|
|
401
|
-
//# sourceMappingURL=menu_controller.js.map
|
|
402
489
|
//# sourceMappingURL=menu_controller.js.map
|