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.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +209 -0
  3. data/README.md +128 -1
  4. data/dist/cable/index.js +392 -417
  5. data/dist/controllers/accordion_controller.js +228 -28
  6. data/dist/controllers/alert_dialog_controller.js +862 -221
  7. data/dist/controllers/announcer_controller.js +135 -191
  8. data/dist/controllers/aspect_ratio_controller.js +0 -10
  9. data/dist/controllers/auto_submit_controller.js +295 -110
  10. data/dist/controllers/avatar_controller.js +172 -106
  11. data/dist/controllers/breadcrumb_controller.js +0 -129
  12. data/dist/controllers/bulk_select_controller.js +264 -53
  13. data/dist/controllers/calendar_controller.js +346 -180
  14. data/dist/controllers/carousel_controller.js +587 -300
  15. data/dist/controllers/character_counter_controller.js +271 -143
  16. data/dist/controllers/checkbox_controller.js +77 -75
  17. data/dist/controllers/clipboard_controller.js +187 -108
  18. data/dist/controllers/collapsible_controller.js +374 -108
  19. data/dist/controllers/color_picker_controller.js +183 -127
  20. data/dist/controllers/combobox_controller.js +426 -109
  21. data/dist/controllers/command_palette_controller.js +994 -350
  22. data/dist/controllers/conditional_fields_controller.js +212 -125
  23. data/dist/controllers/confirm_controller.js +939 -245
  24. data/dist/controllers/context_menu_controller.js +275 -127
  25. data/dist/controllers/count_up_controller.js +73 -21
  26. data/dist/controllers/countdown_controller.js +235 -97
  27. data/dist/controllers/currency_input_controller.js +241 -118
  28. data/dist/controllers/data_grid_controller.js +229 -135
  29. data/dist/controllers/date_range_picker_controller.js +359 -165
  30. data/dist/controllers/dialog_controller.js +887 -225
  31. data/dist/controllers/direct_upload_controller.js +80 -142
  32. data/dist/controllers/dirty_form_controller.js +26 -47
  33. data/dist/controllers/dismissible_controller.js +175 -20
  34. data/dist/controllers/drawer_controller.js +963 -336
  35. data/dist/controllers/dropdown_controller.js +342 -85
  36. data/dist/controllers/editable_controller.js +182 -58
  37. data/dist/controllers/empty_state_controller.js +67 -53
  38. data/dist/controllers/file_dropzone_controller.js +426 -229
  39. data/dist/controllers/filter_controller.js +78 -36
  40. data/dist/controllers/flash_controller.js +328 -211
  41. data/dist/controllers/focus_controller.js +654 -239
  42. data/dist/controllers/form_field_controller.js +153 -136
  43. data/dist/controllers/form_validation_controller.js +29 -96
  44. data/dist/controllers/frame_loading_controller.js +263 -198
  45. data/dist/controllers/highlight_controller.js +119 -70
  46. data/dist/controllers/hover_card_controller.js +335 -118
  47. data/dist/controllers/idle_controller.js +316 -65
  48. data/dist/controllers/input_mask_controller.js +116 -61
  49. data/dist/controllers/intersection_controller.js +147 -103
  50. data/dist/controllers/lazy_frame_controller.js +9 -56
  51. data/dist/controllers/listbox_controller.js +413 -153
  52. data/dist/controllers/local_time_controller.js +54 -64
  53. data/dist/controllers/masonry_controller.js +70 -95
  54. data/dist/controllers/menu_controller.js +254 -167
  55. data/dist/controllers/menubar_controller.js +206 -331
  56. data/dist/controllers/meter_controller.js +159 -71
  57. data/dist/controllers/multi_select_controller.js +797 -388
  58. data/dist/controllers/navigation_menu_controller.js +156 -231
  59. data/dist/controllers/nested_form_controller.js +373 -174
  60. data/dist/controllers/network_status_controller.js +241 -45
  61. data/dist/controllers/number_input_controller.js +387 -227
  62. data/dist/controllers/optimistic_controller.js +28 -75
  63. data/dist/controllers/otp_controller.js +344 -196
  64. data/dist/controllers/overflow_indicator_controller.js +167 -93
  65. data/dist/controllers/overflow_menu_controller.js +408 -277
  66. data/dist/controllers/pagination_controller.js +342 -120
  67. data/dist/controllers/password_reveal_controller.js +424 -99
  68. data/dist/controllers/password_strength_controller.js +99 -153
  69. data/dist/controllers/persist_controller.js +252 -96
  70. data/dist/controllers/pointer_drag_controller.js +263 -164
  71. data/dist/controllers/popover_controller.js +267 -94
  72. data/dist/controllers/portal_controller.js +209 -72
  73. data/dist/controllers/preview_guard_controller.js +243 -112
  74. data/dist/controllers/progress_controller.js +132 -59
  75. data/dist/controllers/radio_group_controller.js +138 -133
  76. data/dist/controllers/range_slider_controller.js +245 -112
  77. data/dist/controllers/rating_controller.js +429 -194
  78. data/dist/controllers/read_more_controller.js +185 -47
  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 +294 -118
  83. data/dist/controllers/roving_controller.js +39 -84
  84. data/dist/controllers/scroll_area_controller.js +428 -221
  85. data/dist/controllers/scroll_restore_controller.js +0 -53
  86. data/dist/controllers/scroll_visibility_controller.js +395 -146
  87. data/dist/controllers/scrollspy_controller.js +181 -236
  88. data/dist/controllers/separator_controller.js +288 -132
  89. data/dist/controllers/sidebar_controller.js +1071 -375
  90. data/dist/controllers/skeleton_controller.js +248 -92
  91. data/dist/controllers/slider_controller.js +211 -96
  92. data/dist/controllers/smart_sticky_header_controller.js +163 -54
  93. data/dist/controllers/sortable_controller.js +13 -110
  94. data/dist/controllers/spinner_controller.js +277 -161
  95. data/dist/controllers/step_indicator_controller.js +107 -53
  96. data/dist/controllers/stepper_controller.js +171 -48
  97. data/dist/controllers/stick_to_bottom_controller.js +310 -116
  98. data/dist/controllers/sticky_observer_controller.js +214 -49
  99. data/dist/controllers/submit_once_controller.js +294 -192
  100. data/dist/controllers/switch_controller.js +107 -43
  101. data/dist/controllers/tabs_controller.js +50 -25
  102. data/dist/controllers/tags_input_controller.js +290 -149
  103. data/dist/controllers/textarea_autosize_controller.js +71 -58
  104. data/dist/controllers/theme_controller.js +148 -80
  105. data/dist/controllers/time_picker_controller.js +162 -81
  106. data/dist/controllers/toast_controller.js +365 -193
  107. data/dist/controllers/toggle_group_controller.js +194 -99
  108. data/dist/controllers/toolbar_controller.js +92 -90
  109. data/dist/controllers/tooltip_controller.js +185 -125
  110. data/dist/controllers/transition_controller.js +123 -115
  111. data/dist/controllers/tree_view_controller.js +248 -290
  112. data/dist/index.js +8649 -8572
  113. data/dist/positioning/index.js +148 -45
  114. data/lib/stimeo/ui/version.rb +1 -1
  115. metadata +3 -3
@@ -1,14 +1,21 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/dropdown_controller.ts
3
+ function sharedRegistry(key, create) {
4
+ const symbol = Symbol.for(key);
5
+ const scope = globalThis;
6
+ const existing = scope[symbol];
7
+ if (existing !== void 0) return existing;
8
+ const registry = create();
9
+ Object.defineProperty(globalThis, symbol, { value: registry });
10
+ return registry;
11
+ }
4
12
 
5
- // src/utils/aria_ids.ts
6
- var counter = 0;
13
+ var ids = sharedRegistry("stimeo-ui.aria-ids.registry.v1", () => ({ counter: 0 }));
7
14
  function uniqueId(prefix = "stimeo") {
8
15
  let candidate;
9
16
  do {
10
- counter += 1;
11
- candidate = `${prefix}-${counter}`;
17
+ ids.counter += 1;
18
+ candidate = `${prefix}-${ids.counter}`;
12
19
  } while (typeof document !== "undefined" && document.getElementById(candidate) !== null);
13
20
  return candidate;
14
21
  }
@@ -19,7 +26,143 @@ function ensureId(element, prefix = "stimeo") {
19
26
  return id;
20
27
  }
21
28
 
22
- // src/utils/escape_layer.ts
29
+ function isAttributeValue(value) {
30
+ return value === null || typeof value === "string";
31
+ }
32
+ function readLeaseRecord(element, record) {
33
+ try {
34
+ const parsed = JSON.parse(element.getAttribute(record) ?? "null");
35
+ if (!Array.isArray(parsed)) return void 0;
36
+ const [original, written, ...rest] = parsed;
37
+ const linked = rest[0] !== false;
38
+ const beneath = linked ? rest : rest.slice(1);
39
+ if (!beneath.every((under) => typeof under === "string")) return void 0;
40
+ return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
41
+ } catch {
42
+ return void 0;
43
+ }
44
+ }
45
+ var AttributeLease = class _AttributeLease {
46
+ static #registry = sharedRegistry(
47
+ "stimeo-ui.attribute-lease.registry.v1",
48
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
49
+ );
50
+ #attribute;
51
+ #record;
52
+ #base;
53
+ #held = /* @__PURE__ */ new Set();
54
+ constructor(attribute, owner, options = {}) {
55
+ this.#attribute = attribute;
56
+ this.#record = `data-${owner}-${attribute}-lease`;
57
+ this.#base = options.base === true;
58
+ const names = _AttributeLease.#registry.names;
59
+ names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
60
+ }
61
+ write(element, value) {
62
+ const stack = this.#stack(element);
63
+ const hold = this.#take(stack) ?? this.#acquire(element, stack);
64
+ this.#held.add(element);
65
+ const shows = !this.#covered(element, stack, hold);
66
+ hold.written = value;
67
+ this.#mark(element, stack);
68
+ if (shows) this.#reflect(element, value);
69
+ }
70
+ return(element) {
71
+ const held = this.#held.delete(element);
72
+ const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
73
+ const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
74
+ const hold = this.#take(stack);
75
+ if (!hold) return false;
76
+ const carries = element.getAttribute(this.#attribute) === hold.written;
77
+ const owns = carries && !this.#covered(element, stack, hold);
78
+ const index = stack.indexOf(hold);
79
+ stack.splice(index, 1);
80
+ const above = stack[index];
81
+ if (above) above.linked &&= hold.linked;
82
+ this.#mark(element, stack);
83
+ if (!owns) return true;
84
+ const below = hold.linked ? stack[index - 1] : void 0;
85
+ this.#reflect(element, below ? below.written : hold.original);
86
+ return true;
87
+ }
88
+ returnAll() {
89
+ for (const element of Array.from(this.#held)) this.return(element);
90
+ }
91
+ #stack(element) {
92
+ const stacks = _AttributeLease.#registry.stacks;
93
+ const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
94
+ stacks.set(element, byAttribute);
95
+ const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
96
+ byAttribute.set(this.#attribute, stack);
97
+ return stack;
98
+ }
99
+ #seed(element) {
100
+ const found = [];
101
+ for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
102
+ const read = readLeaseRecord(element, record);
103
+ if (read) found.push({ ...read, record });
104
+ }
105
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
106
+ return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
107
+ lease: null,
108
+ record,
109
+ written,
110
+ original,
111
+ linked: linked && index > 0
112
+ }));
113
+ }
114
+ #take(stack) {
115
+ const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
116
+ if (hold) hold.lease = this;
117
+ return hold;
118
+ }
119
+ #acquire(element, stack) {
120
+ const current = element.getAttribute(this.#attribute);
121
+ const top = stack.at(-1);
122
+ const bottom = stack[0];
123
+ const over = top?.written === current ? top : void 0;
124
+ const from = this.#base ? bottom : over;
125
+ const hold = {
126
+ lease: this,
127
+ record: this.#record,
128
+ written: current,
129
+ original: from ? from.original : current,
130
+ linked: !this.#base && over !== void 0
131
+ };
132
+ if (this.#base && bottom) {
133
+ bottom.linked = true;
134
+ stack.unshift(hold);
135
+ } else stack.push(hold);
136
+ return hold;
137
+ }
138
+ #covered(element, stack, hold) {
139
+ const current = element.getAttribute(this.#attribute);
140
+ return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
141
+ }
142
+ #mark(element, stack) {
143
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
144
+ stack.forEach((hold, index) => {
145
+ const names = stack.slice(0, index).map((under) => under.record);
146
+ const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
147
+ const marker = hold.linked || beneath.length === 0 ? [] : [false];
148
+ records.set(
149
+ hold.record,
150
+ hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
151
+ );
152
+ });
153
+ for (const [name, recorded] of records) {
154
+ if (element.getAttribute(name) === recorded) continue;
155
+ if (recorded === null) element.removeAttribute(name);
156
+ else element.setAttribute(name, recorded);
157
+ }
158
+ }
159
+ #reflect(element, value) {
160
+ if (element.getAttribute(this.#attribute) === value) return;
161
+ if (value === null) element.removeAttribute(this.#attribute);
162
+ else element.setAttribute(this.#attribute, value);
163
+ }
164
+ };
165
+
23
166
  function claimsWhileFocusWithin(element) {
24
167
  return () => {
25
168
  const active = element.ownerDocument.activeElement;
@@ -27,17 +170,11 @@ function claimsWhileFocusWithin(element) {
27
170
  };
28
171
  }
29
172
  var EscapeLayer = class _EscapeLayer {
30
- static #registries = /* @__PURE__ */ new WeakMap();
31
- #ownerDocument = null;
32
- /** Dismissal callback while active; `null` when inactive. */
33
- #onDismiss = null;
34
- /** Live predicate deciding whether the layer claims a press; `null` = always. */
35
- #claims = null;
36
- /**
37
- * Activates this layer at the top of its document's Escape stack, installing
38
- * the document's shared resolver listener if this is its first layer.
39
- * Re-activating an already-active layer moves it to the top.
40
- */
173
+ static #registries = sharedRegistry(
174
+ "stimeo-ui.escape-layer.registry.v1",
175
+ () => /* @__PURE__ */ new WeakMap()
176
+ );
177
+ #active = null;
41
178
  activate(ownerDocument = document, options) {
42
179
  this.deactivate();
43
180
  let registry = _EscapeLayer.#registries.get(ownerDocument);
@@ -46,44 +183,31 @@ var EscapeLayer = class _EscapeLayer {
46
183
  _EscapeLayer.#registries.set(ownerDocument, registry);
47
184
  ownerDocument.addEventListener("keydown", registry.onKeydown);
48
185
  }
49
- registry.stack.push(this);
50
- this.#ownerDocument = ownerDocument;
51
- this.#onDismiss = options.onDismiss;
52
- this.#claims = options.claims ?? null;
53
- }
54
- /**
55
- * Removes this layer from its document's Escape stack, uninstalling the
56
- * shared listener when the stack empties. Safe to call when inactive.
57
- */
186
+ const entry = { onDismiss: options.onDismiss, claims: options.claims ?? null };
187
+ registry.stack.push(entry);
188
+ this.#active = { document: ownerDocument, entry };
189
+ }
58
190
  deactivate() {
59
- const ownerDocument = this.#ownerDocument;
60
- if (!ownerDocument) return;
61
- const registry = _EscapeLayer.#registries.get(ownerDocument);
191
+ const active = this.#active;
192
+ if (!active) return;
193
+ const registry = _EscapeLayer.#registries.get(active.document);
62
194
  if (registry) {
63
- const index = registry.stack.lastIndexOf(this);
195
+ const index = registry.stack.lastIndexOf(active.entry);
64
196
  if (index >= 0) registry.stack.splice(index, 1);
65
197
  if (registry.stack.length === 0) {
66
- ownerDocument.removeEventListener("keydown", registry.onKeydown);
67
- _EscapeLayer.#registries.delete(ownerDocument);
198
+ active.document.removeEventListener("keydown", registry.onKeydown);
199
+ _EscapeLayer.#registries.delete(active.document);
68
200
  }
69
201
  }
70
- this.#ownerDocument = null;
71
- this.#onDismiss = null;
72
- this.#claims = null;
73
- }
74
- /**
75
- * Whether this active layer would own a press right now: it is the topmost
76
- * layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests
77
- * and diagnostics — production dismissal goes through the shared listener.
78
- */
202
+ this.#active = null;
203
+ }
79
204
  get ownsEscape() {
80
- const ownerDocument = this.#ownerDocument;
81
- if (!ownerDocument) return false;
82
- const registry = _EscapeLayer.#registries.get(ownerDocument);
205
+ const active = this.#active;
206
+ if (!active) return false;
207
+ const registry = _EscapeLayer.#registries.get(active.document);
83
208
  if (!registry) return false;
84
- return _EscapeLayer.#resolveOwner(registry.stack) === this;
209
+ return _EscapeLayer.#resolveOwner(registry.stack) === active.entry;
85
210
  }
86
- /** Builds a document's registry with its shared resolver listener. */
87
211
  static #createRegistry() {
88
212
  const registry = {
89
213
  stack: [],
@@ -92,98 +216,231 @@ var EscapeLayer = class _EscapeLayer {
92
216
  const owner = _EscapeLayer.#resolveOwner(registry.stack);
93
217
  if (!owner) return;
94
218
  event.preventDefault();
95
- owner.#onDismiss?.();
219
+ owner.onDismiss();
96
220
  }
97
221
  };
98
222
  return registry;
99
223
  }
100
- /** The topmost stack layer whose claims predicate passes, or `null`. */
101
224
  static #resolveOwner(stack) {
102
225
  for (let index = stack.length - 1; index >= 0; index--) {
103
226
  const layer = stack[index];
104
227
  if (!layer) continue;
105
- if (layer.#claims && !layer.#claims()) continue;
228
+ if (layer.claims && !layer.claims()) continue;
106
229
  return layer;
107
230
  }
108
231
  return null;
109
232
  }
110
233
  };
111
234
 
112
- // src/controllers/dropdown_controller.ts
235
+ var ListenerSet = class {
236
+ #abort = new AbortController();
237
+ add(target, type, handler, options) {
238
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
239
+ }
240
+ dispose() {
241
+ this.#abort.abort();
242
+ this.#abort = new AbortController();
243
+ }
244
+ };
245
+
246
+ var MicrotaskCoalescer = class {
247
+ #run;
248
+ #queued = false;
249
+ #active = false;
250
+ #generation = 0;
251
+ constructor(run) {
252
+ this.#run = run;
253
+ }
254
+ activate() {
255
+ this.#active = true;
256
+ }
257
+ cancel() {
258
+ this.#active = false;
259
+ this.#queued = false;
260
+ this.#generation += 1;
261
+ }
262
+ schedule() {
263
+ if (!this.#active || this.#queued) return;
264
+ this.#queued = true;
265
+ const generation = this.#generation;
266
+ queueMicrotask(() => {
267
+ if (generation !== this.#generation || !this.#queued || !this.#active) return;
268
+ this.#queued = false;
269
+ this.#run();
270
+ });
271
+ }
272
+ };
273
+
274
+ var MorphRenderWatcher = class {
275
+ #listeners = new ListenerSet();
276
+ #pass;
277
+ #element = null;
278
+ #onMorph = () => this.#pass.schedule();
279
+ constructor(run) {
280
+ this.#pass = new MicrotaskCoalescer(run);
281
+ }
282
+ observe(element) {
283
+ if (this.#element === element) return;
284
+ this.disconnect();
285
+ this.#element = element;
286
+ this.#pass.activate();
287
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
288
+ }
289
+ schedule() {
290
+ this.#pass.schedule();
291
+ }
292
+ disconnect() {
293
+ this.#listeners.dispose();
294
+ this.#pass.cancel();
295
+ this.#element = null;
296
+ }
297
+ };
298
+
299
+ var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
300
+ var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
301
+ function stateReasonFor(event) {
302
+ if (!event) return "api";
303
+ if (FOCUS_EVENTS.has(event.type)) return "focus";
304
+ if (POINTER_EVENTS.has(event.type)) return "pointer";
305
+ return "user";
306
+ }
307
+
113
308
  var DropdownController = class extends Controller {
114
309
  static targets = ["trigger", "menu"];
115
310
  static actions = ["close", "open", "toggle"];
116
- /** Escape-stack membership while open; the shared resolver dismisses via it. */
311
+ static events = ["close", "open"];
117
312
  #escapeLayer = new EscapeLayer();
118
- /** Closes the menu when a click lands outside the controller's element. */
313
+ #menuHidden = new AttributeLease("hidden", this.identifier);
314
+ #expanded = new AttributeLease("aria-expanded", this.identifier);
315
+ #suppliedControls = /* @__PURE__ */ new WeakMap();
316
+ #menu = null;
317
+ #connected = false;
318
+ #reporting = false;
319
+ #openState = false;
320
+ #morphRender = new MorphRenderWatcher(() => this.#repair());
119
321
  #onOutsideClick = (event) => {
120
322
  if (!this.element.contains(event.target)) {
121
- this.close();
323
+ this.#close("outside");
122
324
  }
123
325
  };
124
- /** Starts in the closed state and registers outside-click handling. */
125
326
  connect() {
126
327
  this.#associateTriggerWithMenu();
127
- this.close();
328
+ this.#close("api");
128
329
  document.addEventListener("click", this.#onOutsideClick, true);
330
+ this.#morphRender.observe(this.element);
331
+ this.#connected = true;
332
+ this.#reporting = true;
129
333
  }
130
- /** Removes the listeners registered in {@link connect}. */
131
334
  disconnect() {
335
+ this.#connected = false;
336
+ this.#reporting = false;
132
337
  this.#escapeLayer.deactivate();
133
338
  document.removeEventListener("click", this.#onOutsideClick, true);
339
+ this.#morphRender.disconnect();
340
+ }
341
+ triggerTargetConnected() {
342
+ if (this.#connected) this.#reflectTrigger();
134
343
  }
135
- /** Toggles the menu between open and closed. Bound via `data-action`. */
136
- toggle() {
344
+ triggerTargetDisconnected(trigger) {
345
+ if (!this.triggerTargets.includes(trigger)) {
346
+ this.#expanded.return(trigger);
347
+ this.#withdrawControls(trigger);
348
+ }
349
+ if (this.#connected) this.#reflectTrigger();
350
+ }
351
+ menuTargetConnected() {
352
+ this.#adoptMenu();
353
+ }
354
+ menuTargetDisconnected(menu) {
355
+ this.#adoptMenu();
356
+ if (!this.menuTargets.includes(menu)) this.#menuHidden.return(menu);
357
+ }
358
+ toggle(event) {
137
359
  if (this.#isOpen) {
138
- this.close();
360
+ this.close(event);
139
361
  } else {
140
- this.open();
362
+ this.open(event);
141
363
  }
142
364
  }
143
- /** Reveals the menu and reflects the open state on the trigger. */
144
- open() {
365
+ open(event) {
366
+ this.#open(stateReasonFor(event));
367
+ }
368
+ close(event) {
369
+ this.#close(stateReasonFor(event));
370
+ }
371
+ #open(reason) {
145
372
  if (!this.hasMenuTarget) return;
373
+ const menu = this.menuTarget;
374
+ const was = this.#isOpen;
146
375
  this.#escapeLayer.activate(document, {
147
376
  onDismiss: () => this.#closeAndRestore(),
148
377
  claims: claimsWhileFocusWithin(this.element)
149
378
  });
150
- this.menuTarget.hidden = false;
151
- if (this.hasTriggerTarget) {
152
- this.triggerTarget.setAttribute("aria-expanded", "true");
153
- }
379
+ this.#menuHidden.write(menu, null);
380
+ this.#menu = menu;
381
+ this.#openState = true;
382
+ if (this.hasTriggerTarget) this.#expanded.write(this.triggerTarget, "true");
383
+ if (!was && this.#reporting) this.dispatch("open", { detail: { reason }, cancelable: false });
154
384
  }
155
- /** Hides the menu and reflects the closed state on the trigger. */
156
- close() {
385
+ #close(reason) {
386
+ const was = this.#isOpen;
157
387
  this.#escapeLayer.deactivate();
158
- if (!this.hasMenuTarget) return;
159
- this.menuTarget.hidden = true;
160
- if (this.hasTriggerTarget) {
161
- this.triggerTarget.setAttribute("aria-expanded", "false");
388
+ this.#openState = false;
389
+ const menu = this.hasMenuTarget ? this.menuTarget : null;
390
+ this.#menu = menu;
391
+ if (!menu) return;
392
+ this.#menuHidden.write(menu, "");
393
+ if (this.hasTriggerTarget) this.#expanded.write(this.triggerTarget, "false");
394
+ if (was && this.#reporting) this.dispatch("close", { detail: { reason }, cancelable: false });
395
+ }
396
+ #adoptMenu() {
397
+ if (!this.#connected) return;
398
+ const menu = this.hasMenuTarget ? this.menuTarget : null;
399
+ const previous = this.#menu;
400
+ this.#menu = menu;
401
+ if (!menu) {
402
+ this.#escapeLayer.deactivate();
403
+ this.#openState = false;
404
+ } else if (menu !== previous) {
405
+ this.#menuHidden.write(menu, previous?.hidden ?? true ? "" : null);
162
406
  }
407
+ this.#reflectTrigger();
408
+ }
409
+ #reflectTrigger() {
410
+ if (!this.hasTriggerTarget) return;
411
+ this.#expanded.write(this.triggerTarget, String(this.#isOpen));
412
+ this.#associateTriggerWithMenu();
413
+ }
414
+ #repair() {
415
+ if (!this.hasMenuTarget) return;
416
+ this.#menuHidden.write(this.menuTarget, this.#openState ? null : "");
417
+ this.#reflectTrigger();
163
418
  }
164
- /** Closes and restores focus to the trigger (the keyboard-dismissal path). */
165
419
  #closeAndRestore() {
166
- this.close();
420
+ this.#close("escape");
167
421
  if (this.hasTriggerTarget) this.triggerTarget.focus();
168
422
  }
169
- /** Whether the menu is currently visible. */
170
423
  get #isOpen() {
171
424
  return this.hasMenuTarget && !this.menuTarget.hidden;
172
425
  }
173
- /**
174
- * Associates the disclosure trigger and controlled region without clobbering
175
- * authored markup.
176
- */
177
426
  #associateTriggerWithMenu() {
178
427
  if (!this.hasTriggerTarget || !this.hasMenuTarget) return;
179
- if (this.triggerTarget.hasAttribute("aria-controls")) return;
180
- this.triggerTarget.setAttribute(
181
- "aria-controls",
182
- ensureId(this.menuTarget, "stimeo--dropdown-menu")
183
- );
428
+ const trigger = this.triggerTarget;
429
+ const controls = trigger.getAttribute("aria-controls");
430
+ if (controls !== null && controls !== this.#suppliedControls.get(trigger)) return;
431
+ const id = ensureId(this.menuTarget, "stimeo--dropdown-menu");
432
+ if (controls === id) return;
433
+ trigger.setAttribute("aria-controls", id);
434
+ this.#suppliedControls.set(trigger, id);
435
+ }
436
+ #withdrawControls(trigger) {
437
+ const supplied = this.#suppliedControls.get(trigger);
438
+ this.#suppliedControls.delete(trigger);
439
+ if (supplied !== void 0 && trigger.getAttribute("aria-controls") === supplied) {
440
+ trigger.removeAttribute("aria-controls");
441
+ }
184
442
  }
185
443
  };
186
444
 
187
445
  export { DropdownController };
188
- //# sourceMappingURL=dropdown_controller.js.map
189
446
  //# sourceMappingURL=dropdown_controller.js.map