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,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,24 +216,86 @@ 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/utils/state_reason.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
+
113
299
  var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
114
300
  var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
115
301
  function stateReasonFor(event) {
@@ -119,35 +305,56 @@ function stateReasonFor(event) {
119
305
  return "user";
120
306
  }
121
307
 
122
- // src/controllers/dropdown_controller.ts
123
308
  var DropdownController = class extends Controller {
124
309
  static targets = ["trigger", "menu"];
125
310
  static actions = ["close", "open", "toggle"];
126
311
  static events = ["close", "open"];
127
- /** Escape-stack membership while open; the shared resolver dismisses via it. */
128
312
  #escapeLayer = new EscapeLayer();
129
- /** Whether state moves are reported: set once `connect()` settled the baseline. */
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;
130
318
  #reporting = false;
131
- /** Closes the menu when a click lands outside the controller's element. */
319
+ #openState = false;
320
+ #morphRender = new MorphRenderWatcher(() => this.#repair());
132
321
  #onOutsideClick = (event) => {
133
322
  if (!this.element.contains(event.target)) {
134
323
  this.#close("outside");
135
324
  }
136
325
  };
137
- /** Starts in the closed state and registers outside-click handling. */
138
326
  connect() {
139
327
  this.#associateTriggerWithMenu();
140
328
  this.#close("api");
141
329
  document.addEventListener("click", this.#onOutsideClick, true);
330
+ this.#morphRender.observe(this.element);
331
+ this.#connected = true;
142
332
  this.#reporting = true;
143
333
  }
144
- /** Removes the listeners registered in {@link connect}. */
145
334
  disconnect() {
335
+ this.#connected = false;
146
336
  this.#reporting = false;
147
337
  this.#escapeLayer.deactivate();
148
338
  document.removeEventListener("click", this.#onOutsideClick, true);
339
+ this.#morphRender.disconnect();
340
+ }
341
+ triggerTargetConnected() {
342
+ if (this.#connected) this.#reflectTrigger();
343
+ }
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);
149
357
  }
150
- /** Toggles the menu between open and closed. Bound via `data-action`. */
151
358
  toggle(event) {
152
359
  if (this.#isOpen) {
153
360
  this.close(event);
@@ -155,62 +362,85 @@ var DropdownController = class extends Controller {
155
362
  this.open(event);
156
363
  }
157
364
  }
158
- /** Reveals the menu and reflects the open state on the trigger. */
159
365
  open(event) {
160
366
  this.#open(stateReasonFor(event));
161
367
  }
162
- /** Hides the menu and reflects the closed state on the trigger. */
163
368
  close(event) {
164
369
  this.#close(stateReasonFor(event));
165
370
  }
166
- /** Reveals the menu, reflects the open state, and reports a move. */
167
371
  #open(reason) {
168
372
  if (!this.hasMenuTarget) return;
373
+ const menu = this.menuTarget;
169
374
  const was = this.#isOpen;
170
375
  this.#escapeLayer.activate(document, {
171
376
  onDismiss: () => this.#closeAndRestore(),
172
377
  claims: claimsWhileFocusWithin(this.element)
173
378
  });
174
- this.menuTarget.hidden = false;
175
- if (this.hasTriggerTarget) {
176
- this.triggerTarget.setAttribute("aria-expanded", "true");
177
- }
379
+ this.#menuHidden.write(menu, null);
380
+ this.#menu = menu;
381
+ this.#openState = true;
382
+ if (this.hasTriggerTarget) this.#expanded.write(this.triggerTarget, "true");
178
383
  if (!was && this.#reporting) this.dispatch("open", { detail: { reason }, cancelable: false });
179
384
  }
180
- /** Hides the menu, reflects the closed state, and reports a move. */
181
385
  #close(reason) {
182
386
  const was = this.#isOpen;
183
387
  this.#escapeLayer.deactivate();
184
- if (!this.hasMenuTarget) return;
185
- this.menuTarget.hidden = true;
186
- if (this.hasTriggerTarget) {
187
- this.triggerTarget.setAttribute("aria-expanded", "false");
188
- }
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");
189
394
  if (was && this.#reporting) this.dispatch("close", { detail: { reason }, cancelable: false });
190
395
  }
191
- /** Closes and restores focus to the trigger (the keyboard-dismissal path). */
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);
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();
418
+ }
192
419
  #closeAndRestore() {
193
420
  this.#close("escape");
194
421
  if (this.hasTriggerTarget) this.triggerTarget.focus();
195
422
  }
196
- /** Whether the menu is currently visible. */
197
423
  get #isOpen() {
198
424
  return this.hasMenuTarget && !this.menuTarget.hidden;
199
425
  }
200
- /**
201
- * Associates the disclosure trigger and controlled region without clobbering
202
- * authored markup.
203
- */
204
426
  #associateTriggerWithMenu() {
205
427
  if (!this.hasTriggerTarget || !this.hasMenuTarget) return;
206
- if (this.triggerTarget.hasAttribute("aria-controls")) return;
207
- this.triggerTarget.setAttribute(
208
- "aria-controls",
209
- ensureId(this.menuTarget, "stimeo--dropdown-menu")
210
- );
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
+ }
211
442
  }
212
443
  };
213
444
 
214
445
  export { DropdownController };
215
- //# sourceMappingURL=dropdown_controller.js.map
216
446
  //# sourceMappingURL=dropdown_controller.js.map