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,104 +1,207 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/toolbar_controller.ts
4
-
5
- // src/utils/logical_scroll.ts
6
3
  function isRtl(element) {
7
4
  return window.getComputedStyle(element).direction === "rtl";
8
5
  }
9
6
 
10
- // src/utils/arrow_step.ts
11
7
  function isReservedArrowChord(event, allow = []) {
12
8
  if (!event.key.startsWith("Arrow")) return false;
13
9
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
14
10
  }
11
+ function hasModifierChord(event) {
12
+ return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
13
+ }
15
14
 
16
- // src/utils/composition_tracker.ts
17
15
  var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
18
16
  "insertCompositionText",
19
17
  "insertFromComposition",
20
18
  "deleteCompositionText",
21
19
  "deleteByComposition"
22
20
  ]);
21
+ function isCompositionInputType(inputType) {
22
+ return inputType !== void 0 && COMPOSITION_INPUT_TYPES.has(inputType);
23
+ }
24
+ var BEFORE_MORPH_ATTRIBUTE = "turbo:before-morph-attribute";
25
+ var textOf = (element) => element.value;
26
+ var holdsFocus = (element) => element instanceof Node && element.getRootNode().activeElement === element;
23
27
  var CompositionTracker = class {
24
28
  #observedTargets = /* @__PURE__ */ new Set();
25
- #activeTargets = /* @__PURE__ */ new Set();
29
+ #composition = null;
26
30
  #onStart;
27
31
  #onEnd;
28
- /** The field whose confirming `input` is still owed, while the window is open. */
32
+ #onAbandon;
29
33
  #confirmedTarget = null;
34
+ #keyComposing = false;
35
+ #connected = false;
36
+ #abandoned = null;
30
37
  constructor(options = {}) {
31
38
  this.#onStart = options.onStart;
32
39
  this.#onEnd = options.onEnd;
40
+ this.#onAbandon = options.onAbandon;
33
41
  }
34
- /** Starts lifecycle tracking for `target`; repeated calls are idempotent. */
35
42
  observe(target) {
43
+ this.#connected = true;
36
44
  if (this.#observedTargets.has(target)) return;
37
45
  target.addEventListener("compositionstart", this.#handleStart);
38
46
  target.addEventListener("compositionend", this.#handleEnd);
39
- target.addEventListener("keydown", this.#handleKeydown);
47
+ target.addEventListener("keydown", this.#handleKeydown, true);
48
+ target.addEventListener("beforeinput", this.#handleBeforeInput, true);
49
+ target.addEventListener("input", this.#handleInput, true);
50
+ target.addEventListener("blur", this.#handleBlur, true);
51
+ target.addEventListener(BEFORE_MORPH_ATTRIBUTE, this.#handleBeforeMorphAttribute);
40
52
  this.#observedTargets.add(target);
41
53
  }
42
- /** Stops tracking one target and clears any active composition it owned. */
43
54
  unobserve(target) {
44
55
  if (!this.#observedTargets.delete(target)) return;
45
- target.removeEventListener("compositionstart", this.#handleStart);
46
- target.removeEventListener("compositionend", this.#handleEnd);
47
- target.removeEventListener("keydown", this.#handleKeydown);
48
- this.#activeTargets.delete(target);
56
+ this.#release(target);
57
+ if (this.#composition?.target === target) this.#letGo();
49
58
  if (this.#confirmedTarget === target) this.#confirmedTarget = null;
50
59
  }
51
- /** Releases every listener and clears state so reconnect starts cleanly. */
52
60
  disconnect() {
53
- for (const target of this.#observedTargets) {
54
- target.removeEventListener("compositionstart", this.#handleStart);
55
- target.removeEventListener("compositionend", this.#handleEnd);
56
- target.removeEventListener("keydown", this.#handleKeydown);
57
- }
61
+ this.#connected = false;
62
+ for (const target of this.#observedTargets) this.#release(target);
58
63
  this.#observedTargets.clear();
59
- this.#activeTargets.clear();
64
+ this.#letGo();
60
65
  this.#confirmedTarget = null;
66
+ this.#keyComposing = false;
67
+ this.#abandoned = null;
61
68
  }
62
- /** True when lifecycle tracking or the current event reports composition. */
63
69
  isComposing(event) {
64
- return this.#activeTargets.size > 0 || event?.isComposing === true;
65
- }
66
- /**
67
- * Whether `event` is the `input` echoing the composition just confirmed.
68
- *
69
- * Asking closes the window either way, so one confirmation is folded at most
70
- * once and a consumer asks once per `input`.
71
- */
70
+ this.#settle();
71
+ return event?.isComposing === true || this.#composition !== null;
72
+ }
73
+ composingElement() {
74
+ this.#settle();
75
+ return this.#composition?.element ?? null;
76
+ }
77
+ owns(event) {
78
+ const element = this.composingElement();
79
+ return event.isComposing === true || element !== null && element === event.target;
80
+ }
72
81
  consumesConfirmedInput(event) {
73
82
  const confirmed = this.#confirmedTarget;
74
83
  this.#confirmedTarget = null;
75
84
  if (confirmed === null || confirmed !== event.target) return false;
76
85
  const inputType = event.inputType;
77
- return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);
86
+ return !inputType || isCompositionInputType(inputType);
87
+ }
88
+ #release(target) {
89
+ target.removeEventListener("compositionstart", this.#handleStart);
90
+ target.removeEventListener("compositionend", this.#handleEnd);
91
+ target.removeEventListener("keydown", this.#handleKeydown, true);
92
+ target.removeEventListener("beforeinput", this.#handleBeforeInput, true);
93
+ target.removeEventListener("input", this.#handleInput, true);
94
+ target.removeEventListener("blur", this.#handleBlur, true);
95
+ target.removeEventListener(BEFORE_MORPH_ATTRIBUTE, this.#handleBeforeMorphAttribute);
96
+ }
97
+ #letGo() {
98
+ const composition = this.#composition;
99
+ if (composition === null) return;
100
+ if (!holdsFocus(composition.element)) {
101
+ this.#composition = null;
102
+ return;
103
+ }
104
+ queueMicrotask(() => {
105
+ if (this.#composition !== composition || this.#observedTargets.has(composition.target)) {
106
+ return;
107
+ }
108
+ this.#composition = null;
109
+ if (!this.#connected) return;
110
+ this.#abandoned = { element: composition.element, inPlace: true };
111
+ this.#runAbandon();
112
+ });
78
113
  }
114
+ #settle() {
115
+ const composition = this.#composition;
116
+ if (composition === null) return;
117
+ const { target, element, text } = composition;
118
+ if (target instanceof Node && element instanceof Node && !target.contains(element)) {
119
+ this.#composition = null;
120
+ } else if (textOf(element) !== text) {
121
+ this.#abandon(false);
122
+ }
123
+ }
124
+ #abandon(inPlace) {
125
+ this.#owe(inPlace);
126
+ this.#composition = null;
127
+ queueMicrotask(this.#runAbandon);
128
+ }
129
+ #owe(inPlace) {
130
+ const composition = this.#composition;
131
+ if (composition !== null) this.#abandoned = { element: composition.element, inPlace };
132
+ }
133
+ #runAbandon = () => {
134
+ const abandoned = this.#abandoned;
135
+ if (abandoned === null) return;
136
+ this.#abandoned = null;
137
+ this.#onAbandon?.(abandoned.element, abandoned.inPlace);
138
+ };
79
139
  #handleStart = (event) => {
80
140
  this.#confirmedTarget = null;
81
- if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
141
+ this.#settle();
142
+ this.#owe(true);
143
+ const target = event.currentTarget;
144
+ const element = event.target;
145
+ this.#composition = { target, element, text: textOf(element) };
146
+ this.#runAbandon();
82
147
  this.#onStart?.(event);
83
148
  };
84
149
  #handleEnd = (event) => {
85
- if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
150
+ this.#runAbandon();
151
+ if (this.#composition?.target === event.currentTarget) this.#composition = null;
86
152
  this.#confirmedTarget = event.target;
87
153
  this.#onEnd?.(event);
88
154
  };
89
- /** A key on an observed field opens an edit of its own, so no echo is owed. */
90
- #handleKeydown = () => {
155
+ #handleKeydown = (event) => {
156
+ this.#settle();
157
+ this.#runAbandon();
91
158
  this.#confirmedTarget = null;
159
+ this.#keyComposing = event.isComposing === true;
160
+ };
161
+ #handleBeforeInput = (event) => {
162
+ const keyComposing = this.#keyComposing;
163
+ this.#keyComposing = false;
164
+ this.#settle();
165
+ const { isComposing, inputType } = event;
166
+ if (this.#composition !== null && !keyComposing && !isComposing && inputType && !isCompositionInputType(inputType)) {
167
+ this.#abandon(true);
168
+ }
169
+ this.#runAbandon();
170
+ };
171
+ #handleInput = (event) => {
172
+ const composition = this.#composition;
173
+ const { isTrusted, isComposing, inputType } = event;
174
+ if (composition !== null && event.target === composition.element && (isTrusted || isComposing || isCompositionInputType(inputType))) {
175
+ composition.text = textOf(composition.element);
176
+ } else {
177
+ this.#settle();
178
+ }
179
+ this.#runAbandon();
180
+ };
181
+ #handleBlur = (event) => {
182
+ this.#settle();
183
+ this.#runAbandon();
184
+ const composition = this.#composition;
185
+ if (composition === null || composition.target === composition.element) return;
186
+ const { element } = composition;
187
+ if (!(element instanceof Node) || event.target !== element) return;
188
+ const root = element.getRootNode();
189
+ if (root.activeElement !== element) this.#composition = null;
190
+ };
191
+ #handleBeforeMorphAttribute = (event) => {
192
+ this.#settle();
193
+ const composition = this.#composition;
194
+ if (composition?.target !== event.currentTarget || event.target !== composition.element) return;
195
+ const detail = event.detail;
196
+ if (detail?.attributeName === "value") event.preventDefault();
92
197
  };
93
198
  };
94
199
 
95
- // src/utils/event_owner.ts
96
200
  function ownerIndex(candidates, node) {
97
201
  if (!(node instanceof Node)) return -1;
98
202
  return candidates.findIndex((candidate) => candidate.contains(node));
99
203
  }
100
204
 
101
- // src/utils/focus_candidate.ts
102
205
  function inheritsFieldsetDisabled(control) {
103
206
  let fieldset = control.closest("fieldset[disabled]");
104
207
  while (fieldset) {
@@ -109,31 +212,84 @@ function inheritsFieldsetDisabled(control) {
109
212
  return false;
110
213
  }
111
214
 
112
- // src/utils/roving_tabindex.ts
215
+ var ListenerSet = class {
216
+ #abort = new AbortController();
217
+ add(target, type, handler, options) {
218
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
219
+ }
220
+ dispose() {
221
+ this.#abort.abort();
222
+ this.#abort = new AbortController();
223
+ }
224
+ };
225
+
226
+ var MicrotaskCoalescer = class {
227
+ #run;
228
+ #queued = false;
229
+ #active = false;
230
+ #generation = 0;
231
+ constructor(run) {
232
+ this.#run = run;
233
+ }
234
+ activate() {
235
+ this.#active = true;
236
+ }
237
+ cancel() {
238
+ this.#active = false;
239
+ this.#queued = false;
240
+ this.#generation += 1;
241
+ }
242
+ get pending() {
243
+ return this.#queued;
244
+ }
245
+ schedule() {
246
+ if (!this.#active || this.#queued) return;
247
+ this.#queued = true;
248
+ const generation = this.#generation;
249
+ queueMicrotask(() => {
250
+ if (generation !== this.#generation || !this.#queued || !this.#active) return;
251
+ this.#queued = false;
252
+ this.#run();
253
+ });
254
+ }
255
+ };
256
+
257
+ var MorphRenderWatcher = class {
258
+ #listeners = new ListenerSet();
259
+ #pass;
260
+ #element = null;
261
+ #onMorph = () => this.#pass.schedule();
262
+ constructor(run) {
263
+ this.#pass = new MicrotaskCoalescer(run);
264
+ }
265
+ observe(element) {
266
+ if (this.#element === element) return;
267
+ this.disconnect();
268
+ this.#element = element;
269
+ this.#pass.activate();
270
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
271
+ }
272
+ schedule() {
273
+ this.#pass.schedule();
274
+ }
275
+ get pending() {
276
+ return this.#pass.pending;
277
+ }
278
+ disconnect() {
279
+ this.#listeners.dispose();
280
+ this.#pass.cancel();
281
+ this.#element = null;
282
+ }
283
+ };
284
+
113
285
  var RovingTabindex = class {
114
- /** Returns the current ordered item elements; called on every operation. */
115
286
  #getItems;
116
- /**
117
- * @param getItems - Returns the current ordered item elements. Called on every
118
- * operation so the live target list is always used.
119
- */
120
287
  constructor(getItems) {
121
288
  this.#getItems = getItems;
122
289
  }
123
- /** Index of the currently tabbable item (`tabindex="0"`), or `-1` if none. */
124
290
  get activeIndex() {
125
291
  return this.#getItems().findIndex((item) => item.tabIndex === 0);
126
292
  }
127
- /**
128
- * Makes exactly the item at `index` tabbable (`tabindex="0"`) and removes every
129
- * other item from the Tab sequence (`tabindex="-1"`). An out-of-range `index`
130
- * (e.g. `-1`) leaves all items at `-1`, which a controller can use to express
131
- * "nothing is currently tabbable".
132
- *
133
- * @param index - Position of the item to make tabbable.
134
- * @param options - Pass `{ focus: true }` to also move DOM focus to that item,
135
- * and `items` to reuse an event-scoped collection snapshot.
136
- */
137
293
  setActive(index, options = {}) {
138
294
  const { focus = false } = options;
139
295
  const items = options.items ?? this.#getItems();
@@ -150,7 +306,6 @@ function rovingMove(current, length, delta, wrap) {
150
306
  return Math.min(length - 1, Math.max(0, next));
151
307
  }
152
308
 
153
- // src/controllers/toolbar_controller.ts
154
309
  var STATE_ATTRIBUTES = ["disabled", "hidden"];
155
310
  var ToolbarController = class extends Controller {
156
311
  static targets = ["control"];
@@ -162,20 +317,17 @@ var ToolbarController = class extends Controller {
162
317
  #roving = new RovingTabindex(() => this.controlTargets);
163
318
  #composition = new CompositionTracker();
164
319
  #observer = null;
165
- /**
166
- * Live between `connect()` and `disconnect()`. Stimulus reports the *initial*
167
- * targets before `connect()` and re-reports them as disconnected after
168
- * `disconnect()`; gating on this keeps the target callbacks from clobbering
169
- * the authored Tab stop on mount and from resurrecting one after teardown.
170
- */
171
320
  #connected = false;
321
+ #morph = new MorphRenderWatcher(() => this.#ensureTabStop());
322
+ #listeners = new ListenerSet();
172
323
  connect() {
324
+ this.#morph.observe(this.element);
173
325
  this.#ensureTabStop();
174
- this.element.addEventListener("keydown", this.#onKeydown);
175
- this.element.addEventListener("focusin", this.#onFocusin);
326
+ this.#listeners.add(this.element, "keydown", this.#onKeydown);
327
+ this.#listeners.add(this.element, "focusin", this.#onFocusin);
176
328
  this.#composition.observe(this.element);
177
329
  if (typeof MutationObserver !== "undefined") {
178
- const observer = new MutationObserver(() => this.#ensureTabStop());
330
+ const observer = new MutationObserver(() => this.#morph.schedule());
179
331
  observer.observe(this.element, {
180
332
  subtree: true,
181
333
  childList: true,
@@ -190,48 +342,28 @@ var ToolbarController = class extends Controller {
190
342
  this.#connected = true;
191
343
  }
192
344
  disconnect() {
345
+ this.#morph.disconnect();
193
346
  this.#connected = false;
194
- this.element.removeEventListener("keydown", this.#onKeydown);
195
- this.element.removeEventListener("focusin", this.#onFocusin);
347
+ this.#listeners.dispose();
196
348
  this.#composition.disconnect();
197
349
  this.#observer?.disconnect();
198
350
  this.#observer = null;
199
351
  }
200
- /**
201
- * A control added at runtime is dropped out of the Tab sequence first — a
202
- * fresh `<button>` is tabbable by default, which would make the group two Tab
203
- * stops — and then the lone stop is re-established.
204
- */
205
352
  controlTargetConnected(control) {
206
353
  if (!this.#connected) return;
207
354
  control.tabIndex = -1;
208
355
  this.#ensureTabStop();
209
356
  }
210
- /** Removing the control that held the Tab stop must not orphan the group. */
211
357
  controlTargetDisconnected() {
212
358
  if (!this.#connected) return;
213
359
  this.#ensureTabStop();
214
360
  }
215
- /**
216
- * Arrow/Home/End move focus and the single tab stop.
217
- *
218
- * Binding this per control with `data-action` is **optional** — the same
219
- * handling runs from the container's delegated listener. It stays a declared
220
- * action so per-control wiring keeps working; wiring both does not
221
- * double-move, because the delegated pass then yields on `defaultPrevented`.
222
- */
223
361
  onKeydown(event) {
224
362
  this.#handleKeydown(event);
225
363
  }
226
- /** Delegated counterpart of {@link onKeydown}; bound on the container. */
227
364
  #onKeydown = (event) => {
228
365
  this.#handleKeydown(event);
229
366
  };
230
- /**
231
- * Syncs the Tab stop to a control focused by other means (click, programmatic
232
- * `focus()`) so Tab re-entry returns there. A non-navigable control is ignored:
233
- * the lone Tab stop must never sit on something the user cannot operate.
234
- */
235
367
  #onFocusin = (event) => {
236
368
  const index = this.#indexOf(event.target);
237
369
  const control = index === -1 ? void 0 : this.controlTargets[index];
@@ -241,6 +373,7 @@ var ToolbarController = class extends Controller {
241
373
  #handleKeydown(event) {
242
374
  if (event.defaultPrevented) return;
243
375
  if (isReservedArrowChord(event)) return;
376
+ if ((event.key === "Home" || event.key === "End") && hasModifierChord(event)) return;
244
377
  if (this.#composition.isComposing(event)) return;
245
378
  this.#ensureTabStop();
246
379
  const from = this.#indexOf(event.target);
@@ -265,13 +398,6 @@ var ToolbarController = class extends Controller {
265
398
  event.preventDefault();
266
399
  if (target) this.#roving.setActive(this.controlTargets.indexOf(target), { focus: true });
267
400
  }
268
- /**
269
- * Re-establishes the single Tab stop: keep the current one while it is still
270
- * navigable, else hand it to the first navigable control. `-1` (no Tab stop at
271
- * all) is reached only when every control is unavailable, and is recovered
272
- * from as soon as one becomes navigable again. Idempotent by construction, so
273
- * connect, the target callbacks, the observer, and keydown can all call it.
274
- */
275
401
  #ensureTabStop() {
276
402
  const active = this.#roving.activeIndex;
277
403
  const activeEl = active === -1 ? null : this.controlTargets[active];
@@ -282,13 +408,6 @@ var ToolbarController = class extends Controller {
282
408
  const first = this.#navigableControls[0];
283
409
  this.#roving.setActive(first ? this.controlTargets.indexOf(first) : -1);
284
410
  }
285
- /**
286
- * First navigable control strictly in the `delta` direction from `fromIndex`,
287
- * scanning the **full** control list rather than the navigable subset so a
288
- * non-navigable origin is never a dead end. When the direction yields nothing
289
- * the origin keeps focus; when the origin itself is not navigable the first
290
- * navigable control is used instead, so an arrow key always escapes.
291
- */
292
411
  #nextNavigable(fromIndex, delta) {
293
412
  const controls = this.controlTargets;
294
413
  const length = controls.length;
@@ -304,37 +423,18 @@ var ToolbarController = class extends Controller {
304
423
  const origin = controls[fromIndex];
305
424
  return origin && this.#isNavigable(origin) ? origin : this.#navigableControls[0];
306
425
  }
307
- /** Index in `controlTargets` of the control owning `target` (it or a descendant). */
308
426
  #indexOf(target) {
309
427
  return ownerIndex(this.controlTargets, target);
310
428
  }
311
- /** Controls eligible for the roving tab stop (excludes native disabled / hidden). */
312
429
  get #navigableControls() {
313
430
  return this.controlTargets.filter((control) => this.#isNavigable(control));
314
431
  }
315
- /**
316
- * A control can hold the tab stop unless it is `hidden` or a disabled form
317
- * control. `aria-disabled` remains navigable per the APG; activation suppression
318
- * belongs to the control itself or its owning behavior. Native disabledness is
319
- * read from the `disabled` property, narrowed by an `in` check rather than
320
- * asserted since `<a>` and `<div role="button">` are legitimate controls that
321
- * are unaffected by a disabled fieldset. CSS-only visibility cannot be detected
322
- * headlessly and stays the consumer's responsibility.
323
- */
324
432
  #isNavigable(control) {
325
433
  if (this.#isHidden(control)) return false;
326
434
  if (!("disabled" in control)) return true;
327
435
  if (control.disabled === true) return false;
328
436
  return !inheritsFieldsetDisabled(control);
329
437
  }
330
- /**
331
- * Whether `control`, or anything between it and the toolbar root, is `hidden`.
332
- *
333
- * Reading only the control's own attribute lets an invisible control hold the
334
- * single Tab stop, which takes the *whole* toolbar out of the Tab sequence — an
335
- * ordinary `hidden` wrapper is enough. The walk stops at the root: a toolbar
336
- * inside a `hidden` region is already out of the page's Tab order.
337
- */
338
438
  #isHidden(control) {
339
439
  let node = control;
340
440
  while (node && node !== this.element) {
@@ -346,5 +446,4 @@ var ToolbarController = class extends Controller {
346
446
  };
347
447
 
348
448
  export { ToolbarController };
349
- //# sourceMappingURL=toolbar_controller.js.map
350
449
  //# sourceMappingURL=toolbar_controller.js.map