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