stimeo-ui 0.17.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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +106 -1
  3. data/README.md +19 -10
  4. data/dist/cable/index.js +22 -14
  5. data/dist/controllers/accordion_controller.js +14 -6
  6. data/dist/controllers/alert_dialog_controller.js +145 -118
  7. data/dist/controllers/announcer_controller.js +113 -17
  8. data/dist/controllers/auto_submit_controller.js +185 -60
  9. data/dist/controllers/avatar_controller.js +55 -43
  10. data/dist/controllers/bulk_select_controller.js +64 -50
  11. data/dist/controllers/calendar_controller.js +55 -53
  12. data/dist/controllers/carousel_controller.js +124 -86
  13. data/dist/controllers/character_counter_controller.js +291 -95
  14. data/dist/controllers/checkbox_controller.js +6 -0
  15. data/dist/controllers/clipboard_controller.js +118 -27
  16. data/dist/controllers/collapsible_controller.js +52 -49
  17. data/dist/controllers/color_picker_controller.js +29 -7
  18. data/dist/controllers/combobox_controller.js +210 -71
  19. data/dist/controllers/command_palette_controller.js +289 -140
  20. data/dist/controllers/conditional_fields_controller.js +145 -58
  21. data/dist/controllers/confirm_controller.js +145 -118
  22. data/dist/controllers/context_menu_controller.js +138 -73
  23. data/dist/controllers/countdown_controller.js +109 -21
  24. data/dist/controllers/currency_input_controller.js +446 -72
  25. data/dist/controllers/data_grid_controller.js +134 -28
  26. data/dist/controllers/date_range_picker_controller.js +93 -72
  27. data/dist/controllers/dialog_controller.js +145 -118
  28. data/dist/controllers/direct_upload_controller.js +24 -14
  29. data/dist/controllers/dirty_form_controller.js +68 -31
  30. data/dist/controllers/dismissible_controller.js +64 -46
  31. data/dist/controllers/drawer_controller.js +145 -118
  32. data/dist/controllers/dropdown_controller.js +76 -63
  33. data/dist/controllers/editable_controller.js +199 -62
  34. data/dist/controllers/empty_state_controller.js +13 -6
  35. data/dist/controllers/file_dropzone_controller.js +55 -48
  36. data/dist/controllers/filter_controller.js +9 -2
  37. data/dist/controllers/flash_controller.js +66 -33
  38. data/dist/controllers/focus_controller.js +39 -14
  39. data/dist/controllers/form_field_controller.js +52 -43
  40. data/dist/controllers/form_validation_controller.js +18 -11
  41. data/dist/controllers/frame_loading_controller.js +55 -48
  42. data/dist/controllers/hover_card_controller.js +67 -61
  43. data/dist/controllers/idle_controller.js +10 -6
  44. data/dist/controllers/input_mask_controller.js +411 -24
  45. data/dist/controllers/intersection_controller.js +3 -0
  46. data/dist/controllers/lazy_frame_controller.js +15 -3
  47. data/dist/controllers/listbox_controller.js +260 -79
  48. data/dist/controllers/local_time_controller.js +48 -13
  49. data/dist/controllers/masonry_controller.js +11 -4
  50. data/dist/controllers/menu_controller.js +138 -82
  51. data/dist/controllers/menubar_controller.js +65 -19
  52. data/dist/controllers/meter_controller.js +270 -24
  53. data/dist/controllers/multi_select_controller.js +251 -94
  54. data/dist/controllers/navigation_menu_controller.js +67 -19
  55. data/dist/controllers/nested_form_controller.js +98 -64
  56. data/dist/controllers/network_status_controller.js +70 -52
  57. data/dist/controllers/number_input_controller.js +344 -94
  58. data/dist/controllers/optimistic_controller.js +29 -11
  59. data/dist/controllers/otp_controller.js +502 -104
  60. data/dist/controllers/overflow_indicator_controller.js +6 -0
  61. data/dist/controllers/overflow_menu_controller.js +136 -21
  62. data/dist/controllers/pagination_controller.js +47 -14
  63. data/dist/controllers/password_reveal_controller.js +52 -49
  64. data/dist/controllers/password_strength_controller.js +13 -6
  65. data/dist/controllers/persist_controller.js +48 -128
  66. data/dist/controllers/pointer_drag_controller.js +249 -23
  67. data/dist/controllers/popover_controller.js +82 -65
  68. data/dist/controllers/portal_controller.js +6 -0
  69. data/dist/controllers/preview_guard_controller.js +14 -3
  70. data/dist/controllers/progress_controller.js +271 -27
  71. data/dist/controllers/radio_group_controller.js +167 -80
  72. data/dist/controllers/range_slider_controller.js +27 -7
  73. data/dist/controllers/rating_controller.js +93 -57
  74. data/dist/controllers/read_more_controller.js +10 -6
  75. data/dist/controllers/reading_progress_controller.js +22 -5
  76. data/dist/controllers/relative_time_controller.js +75 -11
  77. data/dist/controllers/reset_on_restore_controller.js +6 -0
  78. data/dist/controllers/resizable_controller.js +55 -22
  79. data/dist/controllers/roving_controller.js +10 -4
  80. data/dist/controllers/scroll_area_controller.js +129 -88
  81. data/dist/controllers/scroll_restore_controller.js +14 -2
  82. data/dist/controllers/scroll_visibility_controller.js +93 -60
  83. data/dist/controllers/scrollspy_controller.js +11 -5
  84. data/dist/controllers/separator_controller.js +59 -43
  85. data/dist/controllers/sidebar_controller.js +150 -122
  86. data/dist/controllers/skeleton_controller.js +55 -48
  87. data/dist/controllers/slider_controller.js +27 -6
  88. data/dist/controllers/smart_sticky_header_controller.js +13 -5
  89. data/dist/controllers/sortable_controller.js +27 -14
  90. data/dist/controllers/spinner_controller.js +55 -48
  91. data/dist/controllers/step_indicator_controller.js +6 -0
  92. data/dist/controllers/stepper_controller.js +6 -0
  93. data/dist/controllers/stick_to_bottom_controller.js +19 -8
  94. data/dist/controllers/sticky_observer_controller.js +49 -43
  95. data/dist/controllers/submit_once_controller.js +145 -85
  96. data/dist/controllers/switch_controller.js +172 -28
  97. data/dist/controllers/tabs_controller.js +6 -1
  98. data/dist/controllers/tags_input_controller.js +157 -28
  99. data/dist/controllers/textarea_autosize_controller.js +40 -19
  100. data/dist/controllers/theme_controller.js +10 -4
  101. data/dist/controllers/time_picker_controller.js +192 -54
  102. data/dist/controllers/toast_controller.js +58 -27
  103. data/dist/controllers/toggle_group_controller.js +158 -56
  104. data/dist/controllers/toolbar_controller.js +150 -21
  105. data/dist/controllers/tree_view_controller.js +134 -25
  106. data/dist/index.js +2451 -1312
  107. data/dist/positioning/index.js +6 -0
  108. data/lib/generators/stimeo/install/install_generator.rb +7 -2
  109. data/lib/generators/stimeo/install/templates/stimeo.js +19 -6
  110. data/lib/stimeo/ui/version.rb +1 -1
  111. metadata +2 -2
@@ -1,20 +1,41 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
+ function isAriaDisabled(element) {
4
+ for (let current = element; current; current = current.parentElement) {
5
+ if (current.getAttribute("aria-disabled") === "true") return true;
6
+ }
7
+ return false;
8
+ }
9
+
3
10
  function isReservedArrowChord(event, allow = []) {
4
11
  if (!event.key.startsWith("Arrow")) return false;
5
12
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
6
13
  }
14
+ function hasModifierChord(event) {
15
+ return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
16
+ }
17
+
18
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
19
+ if (element.getAttribute(name) === value) return false;
20
+ if (value === null) element.removeAttribute(name);
21
+ else element.setAttribute(name, value);
22
+ return true;
23
+ }
7
24
 
8
25
  function sharedRegistry(key, create) {
9
26
  const symbol = Symbol.for(key);
10
27
  const scope = globalThis;
11
28
  const existing = scope[symbol];
12
29
  if (existing !== void 0) return existing;
13
- const registry = create();
14
- Object.defineProperty(globalThis, symbol, { value: registry });
15
- return registry;
30
+ const registry2 = create();
31
+ Object.defineProperty(globalThis, symbol, { value: registry2 });
32
+ return registry2;
16
33
  }
17
34
 
35
+ var registry = sharedRegistry(
36
+ "stimeo-ui.attribute-lease.registry.v1",
37
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
38
+ );
18
39
  function isAttributeValue(value) {
19
40
  return value === null || typeof value === "string";
20
41
  }
@@ -31,24 +52,46 @@ function readLeaseRecord(element, record) {
31
52
  return void 0;
32
53
  }
33
54
  }
34
- var AttributeLease = class _AttributeLease {
35
- static #registry = sharedRegistry(
36
- "stimeo-ui.attribute-lease.registry.v1",
37
- () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
38
- );
55
+ function attributeStack(element, attribute) {
56
+ const byAttribute = registry.stacks.get(element) ?? /* @__PURE__ */ new Map();
57
+ registry.stacks.set(element, byAttribute);
58
+ const known = byAttribute.get(attribute);
59
+ if (known) return known;
60
+ const found = [];
61
+ for (const record of registry.names.get(attribute) ?? []) {
62
+ const read = readLeaseRecord(element, record);
63
+ if (read) found.push({ ...read, record });
64
+ }
65
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
66
+ const stack = found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
67
+ lease: null,
68
+ record,
69
+ written,
70
+ original,
71
+ linked: linked && index > 0
72
+ }));
73
+ byAttribute.set(attribute, stack);
74
+ return stack;
75
+ }
76
+ function findHold(stack, lease, record) {
77
+ return stack.find((held) => held.lease === lease) ?? stack.find((held) => held.lease === null && held.record === record);
78
+ }
79
+ var AttributeLease = class {
39
80
  #attribute;
40
81
  #record;
41
82
  #base;
83
+ #preserveAuthor;
42
84
  #held = /* @__PURE__ */ new Set();
43
85
  constructor(attribute, owner, options = {}) {
44
86
  this.#attribute = attribute;
45
87
  this.#record = `data-${owner}-${attribute}-lease`;
46
88
  this.#base = options.base === true;
47
- const names = _AttributeLease.#registry.names;
89
+ this.#preserveAuthor = options.preserveAuthor === true;
90
+ const names = registry.names;
48
91
  names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
49
92
  }
50
93
  write(element, value) {
51
- const stack = this.#stack(element);
94
+ const stack = attributeStack(element, this.#attribute);
52
95
  const hold = this.#take(stack) ?? this.#acquire(element, stack);
53
96
  this.#held.add(element);
54
97
  const shows = !this.#covered(element, stack, hold);
@@ -58,8 +101,8 @@ var AttributeLease = class _AttributeLease {
58
101
  }
59
102
  return(element) {
60
103
  const held = this.#held.delete(element);
61
- const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
62
- const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
104
+ const known = registry.stacks.get(element)?.has(this.#attribute);
105
+ const stack = held || known || element.hasAttribute(this.#record) ? attributeStack(element, this.#attribute) : [];
63
106
  const hold = this.#take(stack);
64
107
  if (!hold) return false;
65
108
  const carries = element.getAttribute(this.#attribute) === hold.written;
@@ -77,31 +120,8 @@ var AttributeLease = class _AttributeLease {
77
120
  returnAll() {
78
121
  for (const element of Array.from(this.#held)) this.return(element);
79
122
  }
80
- #stack(element) {
81
- const stacks = _AttributeLease.#registry.stacks;
82
- const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
83
- stacks.set(element, byAttribute);
84
- const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
85
- byAttribute.set(this.#attribute, stack);
86
- return stack;
87
- }
88
- #seed(element) {
89
- const found = [];
90
- for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
91
- const read = readLeaseRecord(element, record);
92
- if (read) found.push({ ...read, record });
93
- }
94
- const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
95
- return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
96
- lease: null,
97
- record,
98
- written,
99
- original,
100
- linked: linked && index > 0
101
- }));
102
- }
103
123
  #take(stack) {
104
- const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
124
+ const hold = findHold(stack, this, this.#record);
105
125
  if (hold) hold.lease = this;
106
126
  return hold;
107
127
  }
@@ -110,7 +130,7 @@ var AttributeLease = class _AttributeLease {
110
130
  const top = stack.at(-1);
111
131
  const bottom = stack[0];
112
132
  const over = top?.written === current ? top : void 0;
113
- const from = this.#base ? bottom : over;
133
+ const from = this.#base ? this.#preserveAuthor && !stack.some((held) => held.written === current) ? void 0 : bottom : over;
114
134
  const hold = {
115
135
  lease: this,
116
136
  record: this.#record,
@@ -140,15 +160,23 @@ var AttributeLease = class _AttributeLease {
140
160
  );
141
161
  });
142
162
  for (const [name, recorded] of records) {
143
- if (element.getAttribute(name) === recorded) continue;
144
- if (recorded === null) element.removeAttribute(name);
145
- else element.setAttribute(name, recorded);
163
+ setAttributeIfChanged(element, name, recorded);
146
164
  }
147
165
  }
148
166
  #reflect(element, value) {
149
- if (element.getAttribute(this.#attribute) === value) return;
150
- if (value === null) element.removeAttribute(this.#attribute);
151
- else element.setAttribute(this.#attribute, value);
167
+ setAttributeIfChanged(element, this.#attribute, value);
168
+ }
169
+ };
170
+
171
+ var ClickVerdicts = class {
172
+ #verdicts = /* @__PURE__ */ new WeakMap();
173
+ record(event, entries, refused) {
174
+ this.#verdicts.set(event, { entries, refused });
175
+ }
176
+ for(event, entry) {
177
+ const verdict = event && this.#verdicts.get(event);
178
+ if (!verdict?.entries.includes(entry)) return void 0;
179
+ return verdict.refused;
152
180
  }
153
181
  };
154
182
 
@@ -166,25 +194,25 @@ var EscapeLayer = class _EscapeLayer {
166
194
  #active = null;
167
195
  activate(ownerDocument = document, options) {
168
196
  this.deactivate();
169
- let registry = _EscapeLayer.#registries.get(ownerDocument);
170
- if (!registry) {
171
- registry = _EscapeLayer.#createRegistry();
172
- _EscapeLayer.#registries.set(ownerDocument, registry);
173
- ownerDocument.addEventListener("keydown", registry.onKeydown);
197
+ let registry2 = _EscapeLayer.#registries.get(ownerDocument);
198
+ if (!registry2) {
199
+ registry2 = _EscapeLayer.#createRegistry();
200
+ _EscapeLayer.#registries.set(ownerDocument, registry2);
201
+ ownerDocument.addEventListener("keydown", registry2.onKeydown);
174
202
  }
175
203
  const entry = { onDismiss: options.onDismiss, claims: options.claims ?? null };
176
- registry.stack.push(entry);
204
+ registry2.stack.push(entry);
177
205
  this.#active = { document: ownerDocument, entry };
178
206
  }
179
207
  deactivate() {
180
208
  const active = this.#active;
181
209
  if (!active) return;
182
- const registry = _EscapeLayer.#registries.get(active.document);
183
- if (registry) {
184
- const index = registry.stack.lastIndexOf(active.entry);
185
- if (index >= 0) registry.stack.splice(index, 1);
186
- if (registry.stack.length === 0) {
187
- active.document.removeEventListener("keydown", registry.onKeydown);
210
+ const registry2 = _EscapeLayer.#registries.get(active.document);
211
+ if (registry2) {
212
+ const index = registry2.stack.lastIndexOf(active.entry);
213
+ if (index >= 0) registry2.stack.splice(index, 1);
214
+ if (registry2.stack.length === 0) {
215
+ active.document.removeEventListener("keydown", registry2.onKeydown);
188
216
  _EscapeLayer.#registries.delete(active.document);
189
217
  }
190
218
  }
@@ -193,22 +221,22 @@ var EscapeLayer = class _EscapeLayer {
193
221
  get ownsEscape() {
194
222
  const active = this.#active;
195
223
  if (!active) return false;
196
- const registry = _EscapeLayer.#registries.get(active.document);
197
- if (!registry) return false;
198
- return _EscapeLayer.#resolveOwner(registry.stack) === active.entry;
224
+ const registry2 = _EscapeLayer.#registries.get(active.document);
225
+ if (!registry2) return false;
226
+ return _EscapeLayer.#resolveOwner(registry2.stack) === active.entry;
199
227
  }
200
228
  static #createRegistry() {
201
- const registry = {
229
+ const registry2 = {
202
230
  stack: [],
203
231
  onKeydown: (event) => {
204
232
  if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
205
- const owner = _EscapeLayer.#resolveOwner(registry.stack);
233
+ const owner = _EscapeLayer.#resolveOwner(registry2.stack);
206
234
  if (!owner) return;
207
235
  event.preventDefault();
208
236
  owner.onDismiss();
209
237
  }
210
238
  };
211
- return registry;
239
+ return registry2;
212
240
  }
213
241
  static #resolveOwner(stack) {
214
242
  for (let index = stack.length - 1; index >= 0; index--) {
@@ -228,6 +256,21 @@ function ownerIndex(candidates, node) {
228
256
  function ownerOf(candidates, node) {
229
257
  return candidates[ownerIndex(candidates, node)] ?? null;
230
258
  }
259
+ function ownersOf(candidates, node) {
260
+ if (!(node instanceof Node)) return [];
261
+ return candidates.filter((candidate) => candidate.contains(node));
262
+ }
263
+
264
+ var ListenerSet = class {
265
+ #abort = new AbortController();
266
+ add(target, type, handler, options) {
267
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
268
+ }
269
+ dispose() {
270
+ this.#abort.abort();
271
+ this.#abort = new AbortController();
272
+ }
273
+ };
231
274
 
232
275
  var TimerRegistry = class {
233
276
  ids = /* @__PURE__ */ new Set();
@@ -289,15 +332,17 @@ var MenuController = class extends Controller {
289
332
  #expanded = new AttributeLease("aria-expanded", this.identifier);
290
333
  #menu = null;
291
334
  #clickOwner = null;
335
+ #clickVerdicts = new ClickVerdicts();
292
336
  #activated = /* @__PURE__ */ new WeakSet();
293
337
  #connected = false;
294
338
  #reporting = false;
339
+ #listeners = new ListenerSet();
295
340
  connect() {
296
341
  this.#close("api");
297
- this.element.addEventListener("click", this.#onItemClickCapture, true);
298
- this.element.addEventListener("click", this.#onDelegatedItemClick);
299
- this.element.addEventListener("keydown", this.#onDelegatedItemKeydown);
300
- document.addEventListener("click", this.#onOutsideClick, true);
342
+ this.#listeners.add(this.element, "click", this.#onItemClickCapture, { capture: true });
343
+ this.#listeners.add(this.element, "click", this.#onDelegatedItemClick);
344
+ this.#listeners.add(this.element, "keydown", this.#onDelegatedItemKeydown);
345
+ this.#listeners.add(document, "click", this.#onOutsideClick, { capture: true });
301
346
  this.#connected = true;
302
347
  this.#reporting = true;
303
348
  }
@@ -306,10 +351,7 @@ var MenuController = class extends Controller {
306
351
  this.#reporting = false;
307
352
  this.#timers.clearAll();
308
353
  this.#escapeLayer.deactivate();
309
- this.element.removeEventListener("click", this.#onItemClickCapture, true);
310
- this.element.removeEventListener("click", this.#onDelegatedItemClick);
311
- this.element.removeEventListener("keydown", this.#onDelegatedItemKeydown);
312
- document.removeEventListener("click", this.#onOutsideClick, true);
354
+ this.#listeners.dispose();
313
355
  }
314
356
  triggerTargetConnected() {
315
357
  if (this.#connected) this.#reflectTrigger();
@@ -398,6 +440,7 @@ var MenuController = class extends Controller {
398
440
  #handleItemKeydown(event, from) {
399
441
  if (event.defaultPrevented) return;
400
442
  if (isReservedArrowChord(event)) return;
443
+ if ((event.key === "Home" || event.key === "End") && hasModifierChord(event)) return;
401
444
  const items = this.#navigableItems;
402
445
  const currentIndex = items.indexOf(from);
403
446
  switch (event.key) {
@@ -424,12 +467,22 @@ var MenuController = class extends Controller {
424
467
  }
425
468
  }
426
469
  activate(event) {
427
- if (event !== void 0) {
428
- if (this.#activated.has(event)) return;
429
- this.#activated.add(event);
430
- }
470
+ if (event === void 0) this.#closeAndRestore("select");
471
+ else this.#activateItem(this.#itemFrom(event.target), event);
472
+ }
473
+ #activateItem(item, event) {
474
+ if (item !== null && this.#refuses(item, event)) return;
475
+ if (this.#activated.has(event)) return;
476
+ this.#activated.add(event);
431
477
  this.#closeAndRestore("select");
432
478
  }
479
+ #refuses(item, event) {
480
+ const recorded = this.#clickVerdicts.for(event, item);
481
+ if (recorded !== void 0) return recorded;
482
+ return ownersOf(this.itemTargets, event.target).some(
483
+ (owner) => this.#isActivationDisabled(owner)
484
+ );
485
+ }
433
486
  #closeAndRestore(reason) {
434
487
  this.#close(reason);
435
488
  if (this.hasTriggerTarget) this.triggerTarget.focus();
@@ -446,20 +499,20 @@ var MenuController = class extends Controller {
446
499
  const item = this.#clickOwner;
447
500
  this.#clickOwner = null;
448
501
  if (item === null) return;
449
- this.activate(event);
502
+ this.#activateItem(item, event);
450
503
  };
451
504
  #itemFrom(node) {
452
505
  return ownerOf(this.itemTargets, node);
453
506
  }
454
507
  #onItemClickCapture = (event) => {
455
- const target = event.target;
456
- if (!(target instanceof Node)) {
457
- this.#clickOwner = null;
458
- return;
459
- }
460
- const item = this.#itemFrom(target);
461
- this.#clickOwner = item;
462
- if (item === null || item.getAttribute("aria-disabled") !== "true") return;
508
+ const items = ownersOf(this.itemTargets, event.target);
509
+ this.#clickOwner = items[0] ?? null;
510
+ this.#clickVerdicts.record(
511
+ event,
512
+ items,
513
+ items.some((item) => this.#isActivationDisabled(item))
514
+ );
515
+ if (!items.some((item) => isAriaDisabled(item))) return;
463
516
  this.#clickOwner = null;
464
517
  event.preventDefault();
465
518
  event.stopImmediatePropagation();
@@ -480,6 +533,9 @@ var MenuController = class extends Controller {
480
533
  if (item.hasAttribute("hidden")) return false;
481
534
  return !item.disabled;
482
535
  }
536
+ #isActivationDisabled(item) {
537
+ return !this.#isNavigable(item) || isAriaDisabled(item);
538
+ }
483
539
  get #isOpen() {
484
540
  return this.hasMenuTarget && !this.menuTarget.hidden;
485
541
  }
@@ -22,6 +22,13 @@ function actionSource(source) {
22
22
  };
23
23
  }
24
24
 
25
+ function isAriaDisabled(element) {
26
+ for (let current = element; current; current = current.parentElement) {
27
+ if (current.getAttribute("aria-disabled") === "true") return true;
28
+ }
29
+ return false;
30
+ }
31
+
25
32
  function isRtl(element) {
26
33
  return window.getComputedStyle(element).direction === "rtl";
27
34
  }
@@ -30,6 +37,21 @@ function isReservedArrowChord(event, allow = []) {
30
37
  if (!event.key.startsWith("Arrow")) return false;
31
38
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
32
39
  }
40
+ function hasModifierChord(event) {
41
+ return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
42
+ }
43
+
44
+ var ClickVerdicts = class {
45
+ #verdicts = /* @__PURE__ */ new WeakMap();
46
+ record(event, entries, refused) {
47
+ this.#verdicts.set(event, { entries, refused });
48
+ }
49
+ for(event, entry) {
50
+ const verdict = event && this.#verdicts.get(event);
51
+ if (!verdict?.entries.includes(entry)) return void 0;
52
+ return verdict.refused;
53
+ }
54
+ };
33
55
 
34
56
  function sharedRegistry(key, create) {
35
57
  const symbol = Symbol.for(key);
@@ -117,6 +139,10 @@ function ownerIndex(candidates, node) {
117
139
  function ownerOf(candidates, node) {
118
140
  return candidates[ownerIndex(candidates, node)] ?? null;
119
141
  }
142
+ function ownersOf(candidates, node) {
143
+ if (!(node instanceof Node)) return [];
144
+ return candidates.filter((candidate) => candidate.contains(node));
145
+ }
120
146
 
121
147
  var ListenerSet = class {
122
148
  #abort = new AbortController();
@@ -145,6 +171,9 @@ var MicrotaskCoalescer = class {
145
171
  this.#queued = false;
146
172
  this.#generation += 1;
147
173
  }
174
+ get pending() {
175
+ return this.#queued;
176
+ }
148
177
  schedule() {
149
178
  if (!this.#active || this.#queued) return;
150
179
  this.#queued = true;
@@ -175,6 +204,9 @@ var MorphRenderWatcher = class {
175
204
  schedule() {
176
205
  this.#pass.schedule();
177
206
  }
207
+ get pending() {
208
+ return this.#pass.pending;
209
+ }
178
210
  disconnect() {
179
211
  this.#listeners.dispose();
180
212
  this.#pass.cancel();
@@ -298,6 +330,7 @@ var MenubarController = class extends Controller {
298
330
  #connected = false;
299
331
  #reporting = false;
300
332
  #focused = null;
333
+ #clickVerdicts = new ClickVerdicts();
301
334
  #onFocusIn = (event) => {
302
335
  this.#focused = event.target;
303
336
  };
@@ -306,14 +339,15 @@ var MenubarController = class extends Controller {
306
339
  if (next && !this.element.contains(next)) this.#focused = null;
307
340
  };
308
341
  #morph = new MorphRenderWatcher(() => this.#reconcile());
342
+ #listeners = new ListenerSet();
309
343
  connect() {
310
344
  this.#morph.observe(this.element);
311
345
  this.#closeAllMenus("api");
312
346
  this.#reconcile();
313
- this.element.addEventListener("click", this.#onDisabledClickCapture, true);
314
- this.element.addEventListener("focusin", this.#onFocusIn);
315
- this.element.addEventListener("focusout", this.#onFocusOut);
316
- document.addEventListener("click", this.#onOutsideClick, true);
347
+ this.#listeners.add(this.element, "click", this.#onClickCapture, { capture: true });
348
+ this.#listeners.add(this.element, "focusin", this.#onFocusIn);
349
+ this.#listeners.add(this.element, "focusout", this.#onFocusOut);
350
+ this.#listeners.add(document, "click", this.#onOutsideClick, { capture: true });
317
351
  if (typeof MutationObserver !== "undefined") {
318
352
  this.#observer = new MutationObserver(() => this.#morph.schedule());
319
353
  this.#observer.observe(this.element, {
@@ -332,10 +366,7 @@ var MenubarController = class extends Controller {
332
366
  this.#reporting = false;
333
367
  this.#escapeLayer.deactivate();
334
368
  this.#layerActive = false;
335
- this.element.removeEventListener("click", this.#onDisabledClickCapture, true);
336
- this.element.removeEventListener("focusin", this.#onFocusIn);
337
- this.element.removeEventListener("focusout", this.#onFocusOut);
338
- document.removeEventListener("click", this.#onOutsideClick, true);
369
+ this.#listeners.dispose();
339
370
  this.#focused = null;
340
371
  this.#observer?.disconnect();
341
372
  this.#observer = null;
@@ -368,16 +399,18 @@ var MenubarController = class extends Controller {
368
399
  if (!top || !this.topTargets.includes(top)) return;
369
400
  if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
370
401
  return;
402
+ if (this.#refuses(top, event, origin)) return;
371
403
  const moveFocus = event !== null || this.element.contains(document.activeElement);
372
404
  if (this.#isExpanded(top)) {
373
405
  this.#closeMenu(top, reason);
374
406
  } else {
375
- this.#openMenu(top, "first", reason, moveFocus);
407
+ this.#openMenu(top, "first", reason, moveFocus, true);
376
408
  }
377
409
  }
378
410
  onTopKeydown(event) {
379
411
  if (event.defaultPrevented) return;
380
412
  if (isReservedArrowChord(event)) return;
413
+ if ((event.key === "Home" || event.key === "End") && hasModifierChord(event)) return;
381
414
  const tops = this.#navigableTops;
382
415
  const index = tops.indexOf(event.currentTarget);
383
416
  if (index === -1) return;
@@ -417,6 +450,7 @@ var MenubarController = class extends Controller {
417
450
  onItemKeydown(event) {
418
451
  if (event.defaultPrevented) return;
419
452
  if (isReservedArrowChord(event)) return;
453
+ if ((event.key === "Home" || event.key === "End") && hasModifierChord(event)) return;
420
454
  const item = event.currentTarget;
421
455
  const menu = item.closest("[role='menu']");
422
456
  if (!menu) return;
@@ -465,6 +499,7 @@ var MenubarController = class extends Controller {
465
499
  if (!item || !this.itemTargets.includes(item)) return;
466
500
  if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
467
501
  return;
502
+ if (this.#refuses(item, event, origin)) return;
468
503
  const moveFocus = event !== null || this.element.contains(document.activeElement);
469
504
  const menu = item.closest("[role='menu']");
470
505
  const top = this.#topFor(menu);
@@ -482,10 +517,10 @@ var MenubarController = class extends Controller {
482
517
  this.#roving.setActive(this.topTargets.indexOf(destination), { focus: true });
483
518
  }
484
519
  }
485
- #openMenu(opener, focus, reason, moveFocus = true) {
520
+ #openMenu(opener, focus, reason, moveFocus = true, judged = false) {
486
521
  if (!opener) return;
487
522
  const menu = this.#menuFor(opener);
488
- if (!menu || this.#isActivationBlocked(opener)) {
523
+ if (!menu || !judged && isAriaDisabled(opener)) {
489
524
  if (menu || !opener.hasAttribute("aria-controls")) {
490
525
  this.#closeAllMenus(reason);
491
526
  this.#focusTop(
@@ -562,13 +597,14 @@ var MenubarController = class extends Controller {
562
597
  this.#closeAllMenus("outside");
563
598
  }
564
599
  };
565
- #onDisabledClickCapture = (event) => {
566
- const target = event.target;
567
- if (!(target instanceof Node)) return;
568
- const blocked = [...this.topTargets, ...this.itemTargets].some(
569
- (element) => this.#isActivationBlocked(element) && element.contains(target)
600
+ #onClickCapture = (event) => {
601
+ const entries = ownersOf(this.#entries, event.target);
602
+ this.#clickVerdicts.record(
603
+ event,
604
+ entries,
605
+ entries.some((entry) => this.#isActivationDisabled(entry))
570
606
  );
571
- if (!blocked) return;
607
+ if (!entries.some((entry) => isAriaDisabled(entry))) return;
572
608
  event.preventDefault();
573
609
  event.stopImmediatePropagation();
574
610
  };
@@ -637,6 +673,9 @@ var MenubarController = class extends Controller {
637
673
  #itemsIn(menu) {
638
674
  return this.itemTargets.filter((item) => menu.contains(item) && this.#isNavigable(item));
639
675
  }
676
+ get #entries() {
677
+ return [...this.topTargets, ...this.itemTargets];
678
+ }
640
679
  get #navigableTops() {
641
680
  return this.topTargets.filter((top) => this.#isNavigable(top));
642
681
  }
@@ -644,8 +683,15 @@ var MenubarController = class extends Controller {
644
683
  if (element.hasAttribute("hidden")) return false;
645
684
  return !element.disabled;
646
685
  }
647
- #isActivationBlocked(element) {
648
- return element.getAttribute("aria-disabled") === "true";
686
+ #isActivationDisabled(element) {
687
+ return !this.#isNavigable(element) || isAriaDisabled(element);
688
+ }
689
+ #refuses(entry, event, origin) {
690
+ const recorded = this.#clickVerdicts.for(event, entry);
691
+ if (recorded !== void 0) return recorded;
692
+ if (this.#isActivationDisabled(entry)) return true;
693
+ const target = event ? event.target : origin;
694
+ return ownersOf(this.#entries, target).some((owner) => this.#isActivationDisabled(owner));
649
695
  }
650
696
  #focusAt(items, index) {
651
697
  items[index]?.focus();