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.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/README.md +8 -1
  4. data/dist/cable/index.js +363 -436
  5. data/dist/controllers/accordion_controller.js +166 -55
  6. data/dist/controllers/alert_dialog_controller.js +862 -221
  7. data/dist/controllers/announcer_controller.js +135 -198
  8. data/dist/controllers/aspect_ratio_controller.js +0 -10
  9. data/dist/controllers/auto_submit_controller.js +271 -127
  10. data/dist/controllers/avatar_controller.js +171 -105
  11. data/dist/controllers/breadcrumb_controller.js +0 -130
  12. data/dist/controllers/bulk_select_controller.js +262 -52
  13. data/dist/controllers/calendar_controller.js +175 -209
  14. data/dist/controllers/carousel_controller.js +461 -399
  15. data/dist/controllers/character_counter_controller.js +246 -156
  16. data/dist/controllers/checkbox_controller.js +32 -74
  17. data/dist/controllers/clipboard_controller.js +168 -147
  18. data/dist/controllers/collapsible_controller.js +301 -120
  19. data/dist/controllers/color_picker_controller.js +149 -139
  20. data/dist/controllers/combobox_controller.js +360 -133
  21. data/dist/controllers/command_palette_controller.js +970 -358
  22. data/dist/controllers/conditional_fields_controller.js +166 -124
  23. data/dist/controllers/confirm_controller.js +939 -248
  24. data/dist/controllers/context_menu_controller.js +256 -128
  25. data/dist/controllers/count_up_controller.js +73 -21
  26. data/dist/controllers/countdown_controller.js +183 -148
  27. data/dist/controllers/currency_input_controller.js +134 -158
  28. data/dist/controllers/data_grid_controller.js +114 -186
  29. data/dist/controllers/date_range_picker_controller.js +288 -206
  30. data/dist/controllers/dialog_controller.js +858 -223
  31. data/dist/controllers/direct_upload_controller.js +73 -153
  32. data/dist/controllers/dirty_form_controller.js +14 -48
  33. data/dist/controllers/dismissible_controller.js +175 -21
  34. data/dist/controllers/drawer_controller.js +933 -341
  35. data/dist/controllers/dropdown_controller.js +312 -82
  36. data/dist/controllers/editable_controller.js +158 -68
  37. data/dist/controllers/empty_state_controller.js +67 -53
  38. data/dist/controllers/file_dropzone_controller.js +311 -207
  39. data/dist/controllers/filter_controller.js +67 -39
  40. data/dist/controllers/flash_controller.js +172 -314
  41. data/dist/controllers/focus_controller.js +654 -240
  42. data/dist/controllers/form_field_controller.js +147 -132
  43. data/dist/controllers/form_validation_controller.js +13 -86
  44. data/dist/controllers/frame_loading_controller.js +264 -213
  45. data/dist/controllers/highlight_controller.js +107 -75
  46. data/dist/controllers/hover_card_controller.js +309 -118
  47. data/dist/controllers/idle_controller.js +267 -101
  48. data/dist/controllers/input_mask_controller.js +71 -72
  49. data/dist/controllers/intersection_controller.js +147 -106
  50. data/dist/controllers/lazy_frame_controller.js +9 -65
  51. data/dist/controllers/listbox_controller.js +306 -204
  52. data/dist/controllers/local_time_controller.js +54 -64
  53. data/dist/controllers/masonry_controller.js +70 -93
  54. data/dist/controllers/menu_controller.js +214 -156
  55. data/dist/controllers/menubar_controller.js +170 -329
  56. data/dist/controllers/meter_controller.js +151 -67
  57. data/dist/controllers/multi_select_controller.js +593 -358
  58. data/dist/controllers/navigation_menu_controller.js +124 -232
  59. data/dist/controllers/nested_form_controller.js +352 -182
  60. data/dist/controllers/network_status_controller.js +242 -54
  61. data/dist/controllers/number_input_controller.js +320 -263
  62. data/dist/controllers/optimistic_controller.js +16 -76
  63. data/dist/controllers/otp_controller.js +198 -193
  64. data/dist/controllers/overflow_indicator_controller.js +116 -113
  65. data/dist/controllers/overflow_menu_controller.js +343 -283
  66. data/dist/controllers/pagination_controller.js +295 -119
  67. data/dist/controllers/password_reveal_controller.js +389 -121
  68. data/dist/controllers/password_strength_controller.js +99 -153
  69. data/dist/controllers/persist_controller.js +237 -103
  70. data/dist/controllers/pointer_drag_controller.js +185 -165
  71. data/dist/controllers/popover_controller.js +236 -97
  72. data/dist/controllers/portal_controller.js +209 -76
  73. data/dist/controllers/preview_guard_controller.js +232 -116
  74. data/dist/controllers/progress_controller.js +125 -56
  75. data/dist/controllers/radio_group_controller.js +117 -137
  76. data/dist/controllers/range_slider_controller.js +192 -105
  77. data/dist/controllers/rating_controller.js +418 -207
  78. data/dist/controllers/read_more_controller.js +134 -63
  79. data/dist/controllers/reading_progress_controller.js +147 -135
  80. data/dist/controllers/relative_time_controller.js +145 -110
  81. data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
  82. data/dist/controllers/resizable_controller.js +255 -139
  83. data/dist/controllers/roving_controller.js +39 -84
  84. data/dist/controllers/scroll_area_controller.js +428 -225
  85. data/dist/controllers/scroll_restore_controller.js +0 -53
  86. data/dist/controllers/scroll_visibility_controller.js +395 -145
  87. data/dist/controllers/scrollspy_controller.js +181 -242
  88. data/dist/controllers/separator_controller.js +267 -140
  89. data/dist/controllers/sidebar_controller.js +1023 -386
  90. data/dist/controllers/skeleton_controller.js +248 -97
  91. data/dist/controllers/slider_controller.js +159 -79
  92. data/dist/controllers/smart_sticky_header_controller.js +162 -63
  93. data/dist/controllers/sortable_controller.js +0 -112
  94. data/dist/controllers/spinner_controller.js +277 -169
  95. data/dist/controllers/step_indicator_controller.js +99 -46
  96. data/dist/controllers/stepper_controller.js +114 -73
  97. data/dist/controllers/stick_to_bottom_controller.js +262 -138
  98. data/dist/controllers/sticky_observer_controller.js +214 -49
  99. data/dist/controllers/submit_once_controller.js +294 -205
  100. data/dist/controllers/switch_controller.js +33 -60
  101. data/dist/controllers/tabs_controller.js +35 -29
  102. data/dist/controllers/tags_input_controller.js +136 -145
  103. data/dist/controllers/textarea_autosize_controller.js +71 -58
  104. data/dist/controllers/theme_controller.js +130 -112
  105. data/dist/controllers/time_picker_controller.js +146 -80
  106. data/dist/controllers/toast_controller.js +186 -297
  107. data/dist/controllers/toggle_group_controller.js +95 -136
  108. data/dist/controllers/toolbar_controller.js +68 -98
  109. data/dist/controllers/tooltip_controller.js +158 -124
  110. data/dist/controllers/transition_controller.js +123 -119
  111. data/dist/controllers/tree_view_controller.js +150 -345
  112. data/dist/index.js +6370 -9346
  113. data/dist/positioning/index.js +148 -47
  114. data/lib/stimeo/ui/version.rb +1 -1
  115. metadata +3 -3
@@ -1,14 +1,179 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/context_menu_controller.ts
3
+ var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
4
+ var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
5
+ function stateReasonFor(event) {
6
+ if (!event) return "api";
7
+ if (FOCUS_EVENTS.has(event.type)) return "focus";
8
+ if (POINTER_EVENTS.has(event.type)) return "pointer";
9
+ return "user";
10
+ }
11
+
12
+ function actionSource(source) {
13
+ if (source instanceof HTMLElement) {
14
+ return { event: null, host: source, origin: source, reason: "api" };
15
+ }
16
+ const event = source ?? null;
17
+ return {
18
+ event,
19
+ host: event?.currentTarget instanceof Element ? event.currentTarget : null,
20
+ origin: event?.target instanceof Element ? event.target : null,
21
+ reason: stateReasonFor(event)
22
+ };
23
+ }
4
24
 
5
- // src/utils/arrow_step.ts
6
25
  function isReservedArrowChord(event, allow = []) {
7
26
  if (!event.key.startsWith("Arrow")) return false;
8
27
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
9
28
  }
10
29
 
11
- // src/utils/escape_layer.ts
30
+ function sharedRegistry(key, create) {
31
+ const symbol = Symbol.for(key);
32
+ const scope = globalThis;
33
+ const existing = scope[symbol];
34
+ if (existing !== void 0) return existing;
35
+ const registry = create();
36
+ Object.defineProperty(globalThis, symbol, { value: registry });
37
+ return registry;
38
+ }
39
+
40
+ function isAttributeValue(value) {
41
+ return value === null || typeof value === "string";
42
+ }
43
+ function readLeaseRecord(element, record) {
44
+ try {
45
+ const parsed = JSON.parse(element.getAttribute(record) ?? "null");
46
+ if (!Array.isArray(parsed)) return void 0;
47
+ const [original, written, ...rest] = parsed;
48
+ const linked = rest[0] !== false;
49
+ const beneath = linked ? rest : rest.slice(1);
50
+ if (!beneath.every((under) => typeof under === "string")) return void 0;
51
+ return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
52
+ } catch {
53
+ return void 0;
54
+ }
55
+ }
56
+ var AttributeLease = class _AttributeLease {
57
+ static #registry = sharedRegistry(
58
+ "stimeo-ui.attribute-lease.registry.v1",
59
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
60
+ );
61
+ #attribute;
62
+ #record;
63
+ #base;
64
+ #held = /* @__PURE__ */ new Set();
65
+ constructor(attribute, owner, options = {}) {
66
+ this.#attribute = attribute;
67
+ this.#record = `data-${owner}-${attribute}-lease`;
68
+ this.#base = options.base === true;
69
+ const names = _AttributeLease.#registry.names;
70
+ names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
71
+ }
72
+ write(element, value) {
73
+ const stack = this.#stack(element);
74
+ const hold = this.#take(stack) ?? this.#acquire(element, stack);
75
+ this.#held.add(element);
76
+ const shows = !this.#covered(element, stack, hold);
77
+ hold.written = value;
78
+ this.#mark(element, stack);
79
+ if (shows) this.#reflect(element, value);
80
+ }
81
+ return(element) {
82
+ const held = this.#held.delete(element);
83
+ const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
84
+ const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
85
+ const hold = this.#take(stack);
86
+ if (!hold) return false;
87
+ const carries = element.getAttribute(this.#attribute) === hold.written;
88
+ const owns = carries && !this.#covered(element, stack, hold);
89
+ const index = stack.indexOf(hold);
90
+ stack.splice(index, 1);
91
+ const above = stack[index];
92
+ if (above) above.linked &&= hold.linked;
93
+ this.#mark(element, stack);
94
+ if (!owns) return true;
95
+ const below = hold.linked ? stack[index - 1] : void 0;
96
+ this.#reflect(element, below ? below.written : hold.original);
97
+ return true;
98
+ }
99
+ returnAll() {
100
+ for (const element of Array.from(this.#held)) this.return(element);
101
+ }
102
+ #stack(element) {
103
+ const stacks = _AttributeLease.#registry.stacks;
104
+ const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
105
+ stacks.set(element, byAttribute);
106
+ const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
107
+ byAttribute.set(this.#attribute, stack);
108
+ return stack;
109
+ }
110
+ #seed(element) {
111
+ const found = [];
112
+ for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
113
+ const read = readLeaseRecord(element, record);
114
+ if (read) found.push({ ...read, record });
115
+ }
116
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
117
+ return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
118
+ lease: null,
119
+ record,
120
+ written,
121
+ original,
122
+ linked: linked && index > 0
123
+ }));
124
+ }
125
+ #take(stack) {
126
+ const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
127
+ if (hold) hold.lease = this;
128
+ return hold;
129
+ }
130
+ #acquire(element, stack) {
131
+ const current = element.getAttribute(this.#attribute);
132
+ const top = stack.at(-1);
133
+ const bottom = stack[0];
134
+ const over = top?.written === current ? top : void 0;
135
+ const from = this.#base ? bottom : over;
136
+ const hold = {
137
+ lease: this,
138
+ record: this.#record,
139
+ written: current,
140
+ original: from ? from.original : current,
141
+ linked: !this.#base && over !== void 0
142
+ };
143
+ if (this.#base && bottom) {
144
+ bottom.linked = true;
145
+ stack.unshift(hold);
146
+ } else stack.push(hold);
147
+ return hold;
148
+ }
149
+ #covered(element, stack, hold) {
150
+ const current = element.getAttribute(this.#attribute);
151
+ return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
152
+ }
153
+ #mark(element, stack) {
154
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
155
+ stack.forEach((hold, index) => {
156
+ const names = stack.slice(0, index).map((under) => under.record);
157
+ const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
158
+ const marker = hold.linked || beneath.length === 0 ? [] : [false];
159
+ records.set(
160
+ hold.record,
161
+ hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
162
+ );
163
+ });
164
+ for (const [name, recorded] of records) {
165
+ if (element.getAttribute(name) === recorded) continue;
166
+ if (recorded === null) element.removeAttribute(name);
167
+ else element.setAttribute(name, recorded);
168
+ }
169
+ }
170
+ #reflect(element, value) {
171
+ if (element.getAttribute(this.#attribute) === value) return;
172
+ if (value === null) element.removeAttribute(this.#attribute);
173
+ else element.setAttribute(this.#attribute, value);
174
+ }
175
+ };
176
+
12
177
  function claimsWhileFocusWithin(element) {
13
178
  return () => {
14
179
  const active = element.ownerDocument.activeElement;
@@ -16,17 +181,11 @@ function claimsWhileFocusWithin(element) {
16
181
  };
17
182
  }
18
183
  var EscapeLayer = class _EscapeLayer {
19
- static #registries = /* @__PURE__ */ new WeakMap();
20
- #ownerDocument = null;
21
- /** Dismissal callback while active; `null` when inactive. */
22
- #onDismiss = null;
23
- /** Live predicate deciding whether the layer claims a press; `null` = always. */
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
- */
184
+ static #registries = sharedRegistry(
185
+ "stimeo-ui.escape-layer.registry.v1",
186
+ () => /* @__PURE__ */ new WeakMap()
187
+ );
188
+ #active = null;
30
189
  activate(ownerDocument = document, options) {
31
190
  this.deactivate();
32
191
  let registry = _EscapeLayer.#registries.get(ownerDocument);
@@ -35,44 +194,31 @@ var EscapeLayer = class _EscapeLayer {
35
194
  _EscapeLayer.#registries.set(ownerDocument, registry);
36
195
  ownerDocument.addEventListener("keydown", registry.onKeydown);
37
196
  }
38
- registry.stack.push(this);
39
- this.#ownerDocument = ownerDocument;
40
- this.#onDismiss = options.onDismiss;
41
- this.#claims = options.claims ?? null;
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
- */
197
+ const entry = { onDismiss: options.onDismiss, claims: options.claims ?? null };
198
+ registry.stack.push(entry);
199
+ this.#active = { document: ownerDocument, entry };
200
+ }
47
201
  deactivate() {
48
- const ownerDocument = this.#ownerDocument;
49
- if (!ownerDocument) return;
50
- const registry = _EscapeLayer.#registries.get(ownerDocument);
202
+ const active = this.#active;
203
+ if (!active) return;
204
+ const registry = _EscapeLayer.#registries.get(active.document);
51
205
  if (registry) {
52
- const index = registry.stack.lastIndexOf(this);
206
+ const index = registry.stack.lastIndexOf(active.entry);
53
207
  if (index >= 0) registry.stack.splice(index, 1);
54
208
  if (registry.stack.length === 0) {
55
- ownerDocument.removeEventListener("keydown", registry.onKeydown);
56
- _EscapeLayer.#registries.delete(ownerDocument);
209
+ active.document.removeEventListener("keydown", registry.onKeydown);
210
+ _EscapeLayer.#registries.delete(active.document);
57
211
  }
58
212
  }
59
- this.#ownerDocument = null;
60
- this.#onDismiss = null;
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
- */
213
+ this.#active = null;
214
+ }
68
215
  get ownsEscape() {
69
- const ownerDocument = this.#ownerDocument;
70
- if (!ownerDocument) return false;
71
- const registry = _EscapeLayer.#registries.get(ownerDocument);
216
+ const active = this.#active;
217
+ if (!active) return false;
218
+ const registry = _EscapeLayer.#registries.get(active.document);
72
219
  if (!registry) return false;
73
- return _EscapeLayer.#resolveOwner(registry.stack) === this;
220
+ return _EscapeLayer.#resolveOwner(registry.stack) === active.entry;
74
221
  }
75
- /** Builds a document's registry with its shared resolver listener. */
76
222
  static #createRegistry() {
77
223
  const registry = {
78
224
  stack: [],
@@ -81,60 +227,40 @@ var EscapeLayer = class _EscapeLayer {
81
227
  const owner = _EscapeLayer.#resolveOwner(registry.stack);
82
228
  if (!owner) return;
83
229
  event.preventDefault();
84
- owner.#onDismiss?.();
230
+ owner.onDismiss();
85
231
  }
86
232
  };
87
233
  return registry;
88
234
  }
89
- /** The topmost stack layer whose claims predicate passes, or `null`. */
90
235
  static #resolveOwner(stack) {
91
236
  for (let index = stack.length - 1; index >= 0; index--) {
92
237
  const layer = stack[index];
93
238
  if (!layer) continue;
94
- if (layer.#claims && !layer.#claims()) continue;
239
+ if (layer.claims && !layer.claims()) continue;
95
240
  return layer;
96
241
  }
97
242
  return null;
98
243
  }
99
244
  };
100
245
 
101
- // src/utils/safe_timeout.ts
102
246
  var TimerRegistry = class {
103
- /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
104
247
  ids = /* @__PURE__ */ new Set();
105
- /**
106
- * Cancels a single tracked timer.
107
- *
108
- * No-ops if the id is unknown (already cleared, fired, or never owned by this
109
- * registry), so callers can clear defensively without guarding.
110
- */
111
248
  clear(id) {
112
249
  if (this.ids.delete(id)) {
113
250
  this.cancel(id);
114
251
  }
115
252
  }
116
- /**
117
- * Cancels every tracked timer. Call this from a controller's `disconnect()`
118
- * to guarantee no timer outlives the element.
119
- */
120
253
  clearAll() {
121
254
  for (const id of this.ids) {
122
255
  this.cancel(id);
123
256
  }
124
257
  this.ids.clear();
125
258
  }
126
- /** Number of timers currently tracked (pending). */
127
259
  get size() {
128
260
  return this.ids.size;
129
261
  }
130
262
  };
131
263
  var SafeTimeout = class extends TimerRegistry {
132
- /**
133
- * Schedules `callback` after `delay` ms and returns the timer id.
134
- *
135
- * The id is removed from the registry automatically when the timeout fires,
136
- * so {@link TimerRegistry.size | size} reflects only still-pending timers.
137
- */
138
264
  set(callback, delay) {
139
265
  const id = this.schedule(() => {
140
266
  this.ids.delete(id);
@@ -151,36 +277,28 @@ var SafeTimeout = class extends TimerRegistry {
151
277
  }
152
278
  };
153
279
 
154
- // src/utils/state_reason.ts
155
- var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
156
- var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
157
- function stateReasonFor(event) {
158
- if (!event) return "api";
159
- if (FOCUS_EVENTS.has(event.type)) return "focus";
160
- if (POINTER_EVENTS.has(event.type)) return "pointer";
161
- return "user";
162
- }
163
-
164
- // src/controllers/context_menu_controller.ts
165
- var ContextMenuController = class extends Controller {
280
+ var ContextMenuController = class _ContextMenuController extends Controller {
281
+ static #COORDINATE = ["--stimeo--context-menu-x", "--stimeo--context-menu-y"];
166
282
  static targets = ["region", "menu", "item"];
167
283
  static actions = ["activate", "onItemKeydown", "onRegionKeydown", "open"];
168
284
  static events = ["close", "open"];
169
285
  #timers = new SafeTimeout();
170
- /** Escape-stack membership while open; the shared resolver dismisses via it. */
171
286
  #escapeLayer = new EscapeLayer();
172
- /** Whether state moves are reported: set once `connect()` settled the baseline. */
287
+ #menuHidden = new AttributeLease("hidden", this.identifier);
288
+ #regionState = new AttributeLease("data-state", this.identifier);
289
+ #menu = null;
290
+ #connected = false;
173
291
  #reporting = false;
174
- /** Starts closed and registers delegated activation and outside-pointer listeners. */
175
292
  connect() {
176
293
  this.#closeMenu("api");
177
294
  this.element.addEventListener("click", this.#onItemClickCapture, true);
178
295
  document.addEventListener("click", this.#onOutsidePointer, true);
179
296
  document.addEventListener("contextmenu", this.#onOutsidePointer, true);
297
+ this.#connected = true;
180
298
  this.#reporting = true;
181
299
  }
182
- /** Releases the listeners, stack membership, and pending Tab-close task. */
183
300
  disconnect() {
301
+ this.#connected = false;
184
302
  this.#reporting = false;
185
303
  this.#timers.clearAll();
186
304
  this.#escapeLayer.deactivate();
@@ -188,15 +306,31 @@ var ContextMenuController = class extends Controller {
188
306
  document.removeEventListener("click", this.#onOutsidePointer, true);
189
307
  document.removeEventListener("contextmenu", this.#onOutsidePointer, true);
190
308
  }
191
- /**
192
- * Opens the menu from a `contextmenu` event: suppresses the native menu and
193
- * places this one at the pointer coordinate.
194
- */
195
- open(event) {
196
- event.preventDefault();
197
- this.#openAt(event.clientX, event.clientY, stateReasonFor(event));
309
+ regionTargetConnected() {
310
+ if (this.#connected) this.#reflectRegion();
311
+ }
312
+ regionTargetDisconnected(region) {
313
+ if (!this.regionTargets.includes(region)) this.#regionState.return(region);
314
+ if (this.#connected) this.#reflectRegion();
315
+ }
316
+ menuTargetConnected() {
317
+ this.#adoptMenu();
318
+ }
319
+ menuTargetDisconnected(menu) {
320
+ this.#adoptMenu();
321
+ if (!this.menuTargets.includes(menu)) this.#menuHidden.return(menu);
322
+ }
323
+ open(input) {
324
+ const source = actionSource(input);
325
+ source.event?.preventDefault();
326
+ if (source.event instanceof MouseEvent) {
327
+ this.#openAt(source.event.clientX, source.event.clientY, source.reason);
328
+ return;
329
+ }
330
+ const anchor = source.host ?? (this.hasRegionTarget ? this.regionTarget : null);
331
+ const rect = anchor?.getBoundingClientRect() ?? { left: 0, top: 0, width: 0, height: 0 };
332
+ this.#openAt(rect.left + rect.width / 2, rect.top + rect.height / 2, source.reason);
198
333
  }
199
- /** Keyboard entry on the region: `Shift+F10` / `ContextMenu` open at center. */
200
334
  onRegionKeydown(event) {
201
335
  if (event.defaultPrevented) return;
202
336
  const isContextKey = event.key === "ContextMenu" || event.key === "F10" && event.shiftKey;
@@ -205,7 +339,6 @@ var ContextMenuController = class extends Controller {
205
339
  const rect = this.hasRegionTarget ? this.regionTarget.getBoundingClientRect() : { left: 0, top: 0, width: 0, height: 0 };
206
340
  this.#openAt(rect.left + rect.width / 2, rect.top + rect.height / 2, stateReasonFor(event));
207
341
  }
208
- /** Roving focus and closing keys inside the menu. */
209
342
  onItemKeydown(event) {
210
343
  if (event.defaultPrevented) return;
211
344
  if (isReservedArrowChord(event)) return;
@@ -240,56 +373,67 @@ var ContextMenuController = class extends Controller {
240
373
  break;
241
374
  }
242
375
  }
243
- /** Closes after an item is activated and restores focus to the region. */
244
376
  activate() {
245
377
  this.#closeAndRestore("select");
246
378
  }
247
- /** Opens the menu at viewport coordinates `(x, y)` and focuses the first item. */
248
379
  #openAt(x, y, reason) {
249
380
  if (!this.hasMenuTarget) return;
250
381
  this.#timers.clearAll();
382
+ const menu = this.menuTarget;
251
383
  const was = this.#isOpen;
252
384
  this.#escapeLayer.activate(document, {
253
385
  onDismiss: () => this.#closeAndRestore("escape"),
254
386
  claims: claimsWhileFocusWithin(this.element)
255
387
  });
256
- this.menuTarget.style.setProperty("--stimeo--context-menu-x", `${x}px`);
257
- this.menuTarget.style.setProperty("--stimeo--context-menu-y", `${y}px`);
258
- this.menuTarget.hidden = false;
259
- if (this.hasRegionTarget) this.regionTarget.setAttribute("data-state", "open");
388
+ menu.style.setProperty(_ContextMenuController.#COORDINATE[0], `${x}px`);
389
+ menu.style.setProperty(_ContextMenuController.#COORDINATE[1], `${y}px`);
390
+ this.#menuHidden.write(menu, null);
391
+ this.#menu = menu;
392
+ if (this.hasRegionTarget) this.#regionState.write(this.regionTarget, "open");
260
393
  if (!was && this.#reporting) this.dispatch("open", { detail: { reason }, cancelable: false });
261
394
  if (!this.#isOpen) return;
262
395
  this.#navigableItems[0]?.focus();
263
396
  }
264
- /** Hides the menu, reflects the collapsed state on the region, and reports a move. */
265
397
  #closeMenu(reason) {
266
398
  this.#timers.clearAll();
267
399
  this.#escapeLayer.deactivate();
268
- if (!this.hasMenuTarget) return;
400
+ const menu = this.hasMenuTarget ? this.menuTarget : null;
401
+ this.#menu = menu;
402
+ if (!menu) return;
269
403
  const was = this.#isOpen;
270
- this.menuTarget.hidden = true;
271
- if (this.hasRegionTarget) this.regionTarget.setAttribute("data-state", "closed");
404
+ this.#menuHidden.write(menu, "");
405
+ if (this.hasRegionTarget) this.#regionState.write(this.regionTarget, "closed");
272
406
  if (was && this.#reporting) this.dispatch("close", { detail: { reason }, cancelable: false });
273
407
  }
274
- /** Closes the menu and returns focus to the region (Escape / activation). */
408
+ #adoptMenu() {
409
+ if (!this.#connected) return;
410
+ const menu = this.hasMenuTarget ? this.menuTarget : null;
411
+ const previous = this.#menu;
412
+ this.#menu = menu;
413
+ if (!menu) {
414
+ this.#escapeLayer.deactivate();
415
+ } else if (menu !== previous) {
416
+ const open = previous !== null && !previous.hidden;
417
+ if (open) {
418
+ for (const property of _ContextMenuController.#COORDINATE) {
419
+ menu.style.setProperty(property, previous.style.getPropertyValue(property));
420
+ }
421
+ }
422
+ this.#menuHidden.write(menu, open ? null : "");
423
+ }
424
+ this.#reflectRegion();
425
+ }
426
+ #reflectRegion() {
427
+ if (!this.hasRegionTarget) return;
428
+ this.#regionState.write(this.regionTarget, this.#isOpen ? "open" : "closed");
429
+ }
275
430
  #closeAndRestore(reason) {
276
431
  this.#closeMenu(reason);
277
432
  if (this.hasRegionTarget) this.regionTarget.focus();
278
433
  }
279
- /**
280
- * Closes when a click or context-menu invocation lands outside this instance.
281
- * Both subscriptions observe in the capture phase, so the target is judged
282
- * against the tree the user actually pressed even when a consumer handler
283
- * detaches it, and application code that stops bubbling cannot leave the menu
284
- * open.
285
- */
286
434
  #onOutsidePointer = (event) => {
287
435
  if (this.#isOpen && !this.element.contains(event.target)) this.#closeMenu("outside");
288
436
  };
289
- /**
290
- * Captures clicks so `aria-disabled` commands cannot reach consumer handlers.
291
- * Native Enter/Space activation also synthesizes a click and is blocked here.
292
- */
293
437
  #onItemClickCapture = (event) => {
294
438
  const target = event.target;
295
439
  if (!(target instanceof Node)) return;
@@ -300,33 +444,17 @@ var ContextMenuController = class extends Controller {
300
444
  event.preventDefault();
301
445
  event.stopImmediatePropagation();
302
446
  };
303
- /** Menu items eligible for roving focus (excludes hidden / natively disabled). */
304
447
  get #navigableItems() {
305
448
  return this.itemTargets.filter((item) => this.#isNavigable(item));
306
449
  }
307
- /**
308
- * An item can take roving focus unless it is `hidden` or a natively `disabled`
309
- * form control. CSS-only visibility is not detectable here and is the
310
- * consumer's responsibility.
311
- *
312
- * **`aria-disabled="true"` stays reachable.** APG separates the two attributes
313
- * by intent: `disabled` is for controls whose existence can be inferred from a
314
- * neighbour (a greyed Next next to a Prev), while `aria-disabled` marks a
315
- * control that must stay *discoverable* — and it names menu items as the
316
- * example. Skipping it would hide the command's existence from a keyboard user
317
- * entirely. Activation is suppressed separately, so the item announces itself
318
- * and does nothing.
319
- */
320
450
  #isNavigable(item) {
321
451
  if (item.hasAttribute("hidden")) return false;
322
452
  return !item.disabled;
323
453
  }
324
- /** Whether the menu is currently visible. */
325
454
  get #isOpen() {
326
455
  return this.hasMenuTarget && !this.menuTarget.hidden;
327
456
  }
328
457
  };
329
458
 
330
459
  export { ContextMenuController };
331
- //# sourceMappingURL=context_menu_controller.js.map
332
460
  //# sourceMappingURL=context_menu_controller.js.map