stimeo-ui 0.17.0 → 0.18.1

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 (299) hide show
  1. package/CHANGELOG.md +136 -2
  2. package/README.md +19 -10
  3. package/dist/cable/index.d.ts +11 -9
  4. package/dist/cable/index.js +22 -14
  5. package/dist/cable/index.js.map +1 -1
  6. package/dist/coerce-JtZVNG5h.d.ts +20 -0
  7. package/dist/controllers/accordion_controller.js +14 -6
  8. package/dist/controllers/accordion_controller.js.map +1 -1
  9. package/dist/controllers/alert_dialog_controller.js +145 -118
  10. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  11. package/dist/controllers/announcer_controller.d.ts +5 -3
  12. package/dist/controllers/announcer_controller.js +113 -17
  13. package/dist/controllers/announcer_controller.js.map +1 -1
  14. package/dist/controllers/auto_submit_controller.js +185 -60
  15. package/dist/controllers/auto_submit_controller.js.map +1 -1
  16. package/dist/controllers/avatar_controller.js +55 -43
  17. package/dist/controllers/avatar_controller.js.map +1 -1
  18. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  19. package/dist/controllers/bulk_select_controller.d.ts +3 -3
  20. package/dist/controllers/bulk_select_controller.js +64 -50
  21. package/dist/controllers/bulk_select_controller.js.map +1 -1
  22. package/dist/controllers/calendar_controller.d.ts +15 -12
  23. package/dist/controllers/calendar_controller.js +55 -53
  24. package/dist/controllers/calendar_controller.js.map +1 -1
  25. package/dist/controllers/carousel_controller.d.ts +9 -5
  26. package/dist/controllers/carousel_controller.js +124 -86
  27. package/dist/controllers/carousel_controller.js.map +1 -1
  28. package/dist/controllers/character_counter_controller.d.ts +27 -6
  29. package/dist/controllers/character_counter_controller.js +291 -95
  30. package/dist/controllers/character_counter_controller.js.map +1 -1
  31. package/dist/controllers/checkbox_controller.js +6 -0
  32. package/dist/controllers/checkbox_controller.js.map +1 -1
  33. package/dist/controllers/clipboard_controller.js +118 -27
  34. package/dist/controllers/clipboard_controller.js.map +1 -1
  35. package/dist/controllers/collapsible_controller.d.ts +1 -1
  36. package/dist/controllers/collapsible_controller.js +52 -49
  37. package/dist/controllers/collapsible_controller.js.map +1 -1
  38. package/dist/controllers/color_picker_controller.js +29 -7
  39. package/dist/controllers/color_picker_controller.js.map +1 -1
  40. package/dist/controllers/combobox_controller.d.ts +1 -1
  41. package/dist/controllers/combobox_controller.js +210 -71
  42. package/dist/controllers/combobox_controller.js.map +1 -1
  43. package/dist/controllers/command_palette_controller.d.ts +9 -9
  44. package/dist/controllers/command_palette_controller.js +290 -140
  45. package/dist/controllers/command_palette_controller.js.map +1 -1
  46. package/dist/controllers/conditional_fields_controller.js +145 -58
  47. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  48. package/dist/controllers/confirm_controller.js +145 -118
  49. package/dist/controllers/confirm_controller.js.map +1 -1
  50. package/dist/controllers/context_menu_controller.d.ts +49 -14
  51. package/dist/controllers/context_menu_controller.js +138 -73
  52. package/dist/controllers/context_menu_controller.js.map +1 -1
  53. package/dist/controllers/count_up_controller.js.map +1 -1
  54. package/dist/controllers/countdown_controller.d.ts +2 -2
  55. package/dist/controllers/countdown_controller.js +109 -21
  56. package/dist/controllers/countdown_controller.js.map +1 -1
  57. package/dist/controllers/currency_input_controller.d.ts +75 -11
  58. package/dist/controllers/currency_input_controller.js +446 -72
  59. package/dist/controllers/currency_input_controller.js.map +1 -1
  60. package/dist/controllers/data_grid_controller.d.ts +10 -8
  61. package/dist/controllers/data_grid_controller.js +134 -28
  62. package/dist/controllers/data_grid_controller.js.map +1 -1
  63. package/dist/controllers/date_range_picker_controller.d.ts +12 -9
  64. package/dist/controllers/date_range_picker_controller.js +93 -72
  65. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  66. package/dist/controllers/dialog_controller.d.ts +4 -5
  67. package/dist/controllers/dialog_controller.js +145 -118
  68. package/dist/controllers/dialog_controller.js.map +1 -1
  69. package/dist/controllers/direct_upload_controller.d.ts +1 -1
  70. package/dist/controllers/direct_upload_controller.js +24 -14
  71. package/dist/controllers/direct_upload_controller.js.map +1 -1
  72. package/dist/controllers/dirty_form_controller.d.ts +6 -1
  73. package/dist/controllers/dirty_form_controller.js +68 -31
  74. package/dist/controllers/dirty_form_controller.js.map +1 -1
  75. package/dist/controllers/dismissible_controller.js +66 -46
  76. package/dist/controllers/dismissible_controller.js.map +1 -1
  77. package/dist/controllers/drawer_controller.js +145 -118
  78. package/dist/controllers/drawer_controller.js.map +1 -1
  79. package/dist/controllers/dropdown_controller.js +76 -63
  80. package/dist/controllers/dropdown_controller.js.map +1 -1
  81. package/dist/controllers/editable_controller.js +200 -62
  82. package/dist/controllers/editable_controller.js.map +1 -1
  83. package/dist/controllers/empty_state_controller.js +13 -6
  84. package/dist/controllers/empty_state_controller.js.map +1 -1
  85. package/dist/controllers/file_dropzone_controller.d.ts +3 -0
  86. package/dist/controllers/file_dropzone_controller.js +55 -48
  87. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  88. package/dist/controllers/filter_controller.js +9 -2
  89. package/dist/controllers/filter_controller.js.map +1 -1
  90. package/dist/controllers/flash_controller.d.ts +4 -4
  91. package/dist/controllers/flash_controller.js +66 -33
  92. package/dist/controllers/flash_controller.js.map +1 -1
  93. package/dist/controllers/focus_controller.d.ts +3 -3
  94. package/dist/controllers/focus_controller.js +39 -14
  95. package/dist/controllers/focus_controller.js.map +1 -1
  96. package/dist/controllers/form_field_controller.d.ts +2 -1
  97. package/dist/controllers/form_field_controller.js +52 -43
  98. package/dist/controllers/form_field_controller.js.map +1 -1
  99. package/dist/controllers/form_validation_controller.d.ts +3 -3
  100. package/dist/controllers/form_validation_controller.js +18 -11
  101. package/dist/controllers/form_validation_controller.js.map +1 -1
  102. package/dist/controllers/frame_loading_controller.d.ts +13 -12
  103. package/dist/controllers/frame_loading_controller.js +55 -48
  104. package/dist/controllers/frame_loading_controller.js.map +1 -1
  105. package/dist/controllers/highlight_controller.d.ts +5 -4
  106. package/dist/controllers/highlight_controller.js.map +1 -1
  107. package/dist/controllers/hover_card_controller.d.ts +4 -1
  108. package/dist/controllers/hover_card_controller.js +67 -61
  109. package/dist/controllers/hover_card_controller.js.map +1 -1
  110. package/dist/controllers/idle_controller.d.ts +6 -5
  111. package/dist/controllers/idle_controller.js +10 -6
  112. package/dist/controllers/idle_controller.js.map +1 -1
  113. package/dist/controllers/input_mask_controller.d.ts +31 -13
  114. package/dist/controllers/input_mask_controller.js +434 -24
  115. package/dist/controllers/input_mask_controller.js.map +1 -1
  116. package/dist/controllers/intersection_controller.js +3 -0
  117. package/dist/controllers/intersection_controller.js.map +1 -1
  118. package/dist/controllers/lazy_frame_controller.d.ts +3 -2
  119. package/dist/controllers/lazy_frame_controller.js +15 -3
  120. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  121. package/dist/controllers/listbox_controller.d.ts +10 -7
  122. package/dist/controllers/listbox_controller.js +260 -79
  123. package/dist/controllers/listbox_controller.js.map +1 -1
  124. package/dist/controllers/local_time_controller.d.ts +8 -6
  125. package/dist/controllers/local_time_controller.js +48 -13
  126. package/dist/controllers/local_time_controller.js.map +1 -1
  127. package/dist/controllers/masonry_controller.js +11 -4
  128. package/dist/controllers/masonry_controller.js.map +1 -1
  129. package/dist/controllers/menu_controller.d.ts +24 -5
  130. package/dist/controllers/menu_controller.js +138 -82
  131. package/dist/controllers/menu_controller.js.map +1 -1
  132. package/dist/controllers/menubar_controller.d.ts +49 -7
  133. package/dist/controllers/menubar_controller.js +65 -19
  134. package/dist/controllers/menubar_controller.js.map +1 -1
  135. package/dist/controllers/meter_controller.d.ts +13 -20
  136. package/dist/controllers/meter_controller.js +270 -24
  137. package/dist/controllers/meter_controller.js.map +1 -1
  138. package/dist/controllers/multi_select_controller.d.ts +19 -16
  139. package/dist/controllers/multi_select_controller.js +272 -95
  140. package/dist/controllers/multi_select_controller.js.map +1 -1
  141. package/dist/controllers/navigation_menu_controller.d.ts +19 -5
  142. package/dist/controllers/navigation_menu_controller.js +67 -19
  143. package/dist/controllers/navigation_menu_controller.js.map +1 -1
  144. package/dist/controllers/nested_form_controller.js +98 -64
  145. package/dist/controllers/nested_form_controller.js.map +1 -1
  146. package/dist/controllers/network_status_controller.js +70 -52
  147. package/dist/controllers/network_status_controller.js.map +1 -1
  148. package/dist/controllers/number_input_controller.d.ts +24 -6
  149. package/dist/controllers/number_input_controller.js +344 -94
  150. package/dist/controllers/number_input_controller.js.map +1 -1
  151. package/dist/controllers/optimistic_controller.d.ts +8 -5
  152. package/dist/controllers/optimistic_controller.js +29 -11
  153. package/dist/controllers/optimistic_controller.js.map +1 -1
  154. package/dist/controllers/otp_controller.d.ts +35 -4
  155. package/dist/controllers/otp_controller.js +502 -104
  156. package/dist/controllers/otp_controller.js.map +1 -1
  157. package/dist/controllers/overflow_indicator_controller.js +6 -0
  158. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  159. package/dist/controllers/overflow_menu_controller.d.ts +10 -9
  160. package/dist/controllers/overflow_menu_controller.js +136 -21
  161. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  162. package/dist/controllers/pagination_controller.d.ts +3 -2
  163. package/dist/controllers/pagination_controller.js +47 -14
  164. package/dist/controllers/pagination_controller.js.map +1 -1
  165. package/dist/controllers/password_reveal_controller.d.ts +5 -4
  166. package/dist/controllers/password_reveal_controller.js +52 -49
  167. package/dist/controllers/password_reveal_controller.js.map +1 -1
  168. package/dist/controllers/password_strength_controller.d.ts +1 -1
  169. package/dist/controllers/password_strength_controller.js +13 -6
  170. package/dist/controllers/password_strength_controller.js.map +1 -1
  171. package/dist/controllers/persist_controller.js +48 -128
  172. package/dist/controllers/persist_controller.js.map +1 -1
  173. package/dist/controllers/pointer_drag_controller.d.ts +20 -8
  174. package/dist/controllers/pointer_drag_controller.js +255 -24
  175. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  176. package/dist/controllers/popover_controller.d.ts +9 -5
  177. package/dist/controllers/popover_controller.js +91 -65
  178. package/dist/controllers/popover_controller.js.map +1 -1
  179. package/dist/controllers/portal_controller.js +6 -0
  180. package/dist/controllers/portal_controller.js.map +1 -1
  181. package/dist/controllers/preview_guard_controller.js +14 -3
  182. package/dist/controllers/preview_guard_controller.js.map +1 -1
  183. package/dist/controllers/progress_controller.d.ts +9 -19
  184. package/dist/controllers/progress_controller.js +271 -27
  185. package/dist/controllers/progress_controller.js.map +1 -1
  186. package/dist/controllers/radio_group_controller.d.ts +11 -8
  187. package/dist/controllers/radio_group_controller.js +167 -80
  188. package/dist/controllers/radio_group_controller.js.map +1 -1
  189. package/dist/controllers/range_slider_controller.d.ts +4 -3
  190. package/dist/controllers/range_slider_controller.js +27 -7
  191. package/dist/controllers/range_slider_controller.js.map +1 -1
  192. package/dist/controllers/rating_controller.js +93 -57
  193. package/dist/controllers/rating_controller.js.map +1 -1
  194. package/dist/controllers/read_more_controller.js +10 -6
  195. package/dist/controllers/read_more_controller.js.map +1 -1
  196. package/dist/controllers/reading_progress_controller.js +22 -5
  197. package/dist/controllers/reading_progress_controller.js.map +1 -1
  198. package/dist/controllers/relative_time_controller.d.ts +4 -4
  199. package/dist/controllers/relative_time_controller.js +75 -11
  200. package/dist/controllers/relative_time_controller.js.map +1 -1
  201. package/dist/controllers/reset_on_restore_controller.d.ts +3 -3
  202. package/dist/controllers/reset_on_restore_controller.js +6 -0
  203. package/dist/controllers/reset_on_restore_controller.js.map +1 -1
  204. package/dist/controllers/resizable_controller.d.ts +3 -0
  205. package/dist/controllers/resizable_controller.js +55 -22
  206. package/dist/controllers/resizable_controller.js.map +1 -1
  207. package/dist/controllers/roving_controller.d.ts +3 -3
  208. package/dist/controllers/roving_controller.js +10 -4
  209. package/dist/controllers/roving_controller.js.map +1 -1
  210. package/dist/controllers/scroll_area_controller.js +129 -88
  211. package/dist/controllers/scroll_area_controller.js.map +1 -1
  212. package/dist/controllers/scroll_restore_controller.d.ts +3 -3
  213. package/dist/controllers/scroll_restore_controller.js +14 -2
  214. package/dist/controllers/scroll_restore_controller.js.map +1 -1
  215. package/dist/controllers/scroll_visibility_controller.js +93 -60
  216. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  217. package/dist/controllers/scrollspy_controller.js +11 -5
  218. package/dist/controllers/scrollspy_controller.js.map +1 -1
  219. package/dist/controllers/separator_controller.d.ts +1 -1
  220. package/dist/controllers/separator_controller.js +59 -43
  221. package/dist/controllers/separator_controller.js.map +1 -1
  222. package/dist/controllers/sidebar_controller.d.ts +1 -1
  223. package/dist/controllers/sidebar_controller.js +150 -122
  224. package/dist/controllers/sidebar_controller.js.map +1 -1
  225. package/dist/controllers/skeleton_controller.js +55 -48
  226. package/dist/controllers/skeleton_controller.js.map +1 -1
  227. package/dist/controllers/slider_controller.d.ts +2 -1
  228. package/dist/controllers/slider_controller.js +27 -6
  229. package/dist/controllers/slider_controller.js.map +1 -1
  230. package/dist/controllers/smart_sticky_header_controller.d.ts +1 -1
  231. package/dist/controllers/smart_sticky_header_controller.js +13 -5
  232. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  233. package/dist/controllers/sortable_controller.d.ts +4 -4
  234. package/dist/controllers/sortable_controller.js +27 -14
  235. package/dist/controllers/sortable_controller.js.map +1 -1
  236. package/dist/controllers/spinner_controller.js +55 -48
  237. package/dist/controllers/spinner_controller.js.map +1 -1
  238. package/dist/controllers/step_indicator_controller.d.ts +4 -4
  239. package/dist/controllers/step_indicator_controller.js +6 -0
  240. package/dist/controllers/step_indicator_controller.js.map +1 -1
  241. package/dist/controllers/stepper_controller.d.ts +2 -3
  242. package/dist/controllers/stepper_controller.js +6 -0
  243. package/dist/controllers/stepper_controller.js.map +1 -1
  244. package/dist/controllers/stick_to_bottom_controller.d.ts +3 -1
  245. package/dist/controllers/stick_to_bottom_controller.js +19 -8
  246. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  247. package/dist/controllers/sticky_observer_controller.js +49 -43
  248. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  249. package/dist/controllers/submit_once_controller.d.ts +2 -1
  250. package/dist/controllers/submit_once_controller.js +145 -85
  251. package/dist/controllers/submit_once_controller.js.map +1 -1
  252. package/dist/controllers/switch_controller.d.ts +7 -4
  253. package/dist/controllers/switch_controller.js +172 -28
  254. package/dist/controllers/switch_controller.js.map +1 -1
  255. package/dist/controllers/tabs_controller.d.ts +7 -4
  256. package/dist/controllers/tabs_controller.js +6 -1
  257. package/dist/controllers/tabs_controller.js.map +1 -1
  258. package/dist/controllers/tags_input_controller.d.ts +4 -3
  259. package/dist/controllers/tags_input_controller.js +157 -28
  260. package/dist/controllers/tags_input_controller.js.map +1 -1
  261. package/dist/controllers/textarea_autosize_controller.d.ts +1 -1
  262. package/dist/controllers/textarea_autosize_controller.js +40 -19
  263. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  264. package/dist/controllers/theme_controller.d.ts +3 -2
  265. package/dist/controllers/theme_controller.js +10 -4
  266. package/dist/controllers/theme_controller.js.map +1 -1
  267. package/dist/controllers/time_picker_controller.d.ts +38 -5
  268. package/dist/controllers/time_picker_controller.js +192 -54
  269. package/dist/controllers/time_picker_controller.js.map +1 -1
  270. package/dist/controllers/toast_controller.d.ts +4 -5
  271. package/dist/controllers/toast_controller.js +58 -27
  272. package/dist/controllers/toast_controller.js.map +1 -1
  273. package/dist/controllers/toggle_group_controller.d.ts +2 -3
  274. package/dist/controllers/toggle_group_controller.js +158 -56
  275. package/dist/controllers/toggle_group_controller.js.map +1 -1
  276. package/dist/controllers/toolbar_controller.d.ts +8 -6
  277. package/dist/controllers/toolbar_controller.js +150 -21
  278. package/dist/controllers/toolbar_controller.js.map +1 -1
  279. package/dist/controllers/tooltip_controller.d.ts +4 -1
  280. package/dist/controllers/tooltip_controller.js.map +1 -1
  281. package/dist/controllers/transition_controller.js.map +1 -1
  282. package/dist/controllers/tree_view_controller.d.ts +1 -1
  283. package/dist/controllers/tree_view_controller.js +134 -25
  284. package/dist/controllers/tree_view_controller.js.map +1 -1
  285. package/dist/index.d.ts +4 -1
  286. package/dist/index.js +2515 -1315
  287. package/dist/index.js.map +1 -1
  288. package/dist/inspector/cli.d.ts +25 -12
  289. package/dist/inspector/cli.js +21 -3
  290. package/dist/inspector/cli.js.map +1 -1
  291. package/dist/inspector/cli_bin.d.ts +1 -1
  292. package/dist/inspector/cli_bin.js +62 -8
  293. package/dist/inspector/cli_bin.js.map +1 -1
  294. package/dist/inspector/examples.json +13 -13
  295. package/dist/inspector/manifest.json +19 -5
  296. package/dist/positioning/index.d.ts +9 -8
  297. package/dist/positioning/index.js +6 -0
  298. package/dist/positioning/index.js.map +1 -1
  299. package/package.json +11 -10
@@ -26,13 +26,15 @@ import { Controller } from '@hotwired/stimulus';
26
26
  * (`0` for the active tab, `-1` for the rest), and the panel `hidden` attribute;
27
27
  * the consumer owns all styling. The required `list` target marks the tablist
28
28
  * container as part of the semantic contract — that container carries
29
- * `role="tablist"` and an accessible name; the controller performs no runtime
30
- * work on it.
29
+ * `role="tablist"` and an accessible name. The controller writes nothing to it;
30
+ * it reads the container's writing direction for the arrow keys, falling back to
31
+ * the controller element when the target is absent.
31
32
  *
32
33
  * Behavior provided:
33
34
  * - Click a tab to select it.
34
- * - `ArrowRight`/`ArrowLeft` move to and select the next/previous tab (wrapping);
35
- * `Home`/`End` select the first/last tab.
35
+ * - The forward horizontal arrow (`ArrowRight`, or `ArrowLeft` when the tablist is
36
+ * right-to-left) moves to and selects the next tab, and the backward one the
37
+ * previous (wrapping); `Home`/`End` select the first/last tab.
36
38
  * - Every selection move is reported: `stimeo--tabs:change` dispatches
37
39
  * `{ index: number, total: number, previous: number, reason: StateReason }`, after
38
40
  * `aria-selected`, the roving `tabindex` and the panels' `hidden` are written
@@ -46,6 +48,7 @@ declare class TabsController extends Controller<HTMLElement> {
46
48
  static actions: readonly ["onKeydown", "select"];
47
49
  static events: readonly ["change"];
48
50
  readonly listTarget: HTMLElement;
51
+ readonly hasListTarget: boolean;
49
52
  readonly tabTargets: HTMLButtonElement[];
50
53
  readonly panelTargets: HTMLElement[];
51
54
  /**
@@ -30,6 +30,9 @@ function isReservedArrowChord(event, allow = []) {
30
30
  if (!event.key.startsWith("Arrow")) return false;
31
31
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
32
32
  }
33
+ function hasModifierChord(event) {
34
+ return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
35
+ }
33
36
 
34
37
  function targetSelector(identifier, name) {
35
38
  return `[data-${identifier}-target~="${name}"]`;
@@ -62,11 +65,13 @@ var TabsController = class extends Controller {
62
65
  onKeydown(event) {
63
66
  if (event.defaultPrevented) return;
64
67
  if (isReservedArrowChord(event)) return;
68
+ if ((event.key === "Home" || event.key === "End") && hasModifierChord(event)) return;
65
69
  const tabs = this.tabTargets;
66
70
  const currentIndex = tabs.indexOf(event.currentTarget);
67
71
  if (currentIndex === -1) return;
68
72
  let nextIndex = null;
69
- const step = isRtl(this.element) ? -1 : 1;
73
+ const directionElement = this.hasListTarget ? this.listTarget : this.element;
74
+ const step = isRtl(directionElement) ? -1 : 1;
70
75
  switch (event.key) {
71
76
  case "ArrowRight":
72
77
  nextIndex = (currentIndex + step + tabs.length) % tabs.length;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/state_reason.ts","../../src/utils/action_source.ts","../../src/utils/logical_scroll.ts","../../src/utils/arrow_step.ts","../../src/utils/target_selector.ts","../../src/controllers/tabs_controller.ts"],"names":[],"mappings":"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","file":"tabs_controller.js","sourcesContent":["/**\n * Why a component's public state moved, carried as `detail.reason`\n * on the state event that reports the move.\n *\n * The attribute a component publishes (`aria-expanded`, `hidden`, `data-state`)\n * only says *what* the state is now. A subscriber that mirrors the state\n * elsewhere, or reports it, needs *why*: closing on `Escape` and closing because\n * the consumer called the action are the same attribute write and different\n * events to the page around it.\n *\n * `\"api\"` is the one every subscriber has to look at. A close the consumer asked\n * for arrives back at their own listener, so a listener that closes something\n * else on `close` loops unless it ignores its own calls.\n *\n * | Reason | The state moved because |\n * | --- | --- |\n * | `\"user\"` | a control of this component was operated |\n * | `\"select\"` | an item inside it was activated |\n * | `\"escape\"` | `Escape` was pressed and this layer owned it |\n * | `\"outside\"` | a pointer landed outside it, or on its backdrop |\n * | `\"focus\"` | focus entered or left it |\n * | `\"pointer\"` | the pointer entered or left it |\n * | `\"scroll\"` | a tracked scroll container scrolled |\n * | `\"api\"` | a public action was called with no DOM event |\n * | `\"timeout\"` | an automatic timer advanced or dismissed the state |\n * | `\"limit\"` | a capacity limit dismissed an item |\n * | `\"media\"` | an operating-system media preference changed |\n */\nexport type StateReason =\n | \"user\"\n | \"select\"\n | \"escape\"\n | \"outside\"\n | \"focus\"\n | \"pointer\"\n | \"scroll\"\n | \"api\"\n | \"timeout\"\n | \"limit\"\n | \"media\";\n\n/** Focus modality: the events a focus move delivers to an action. */\nconst FOCUS_EVENTS = new Set([\"blur\", \"focus\", \"focusin\", \"focusout\"]);\n\n/** Pointer modality: the crossing events an action is bound to for hover. */\nconst POINTER_EVENTS = new Set([\"mouseenter\", \"mouseleave\", \"pointerenter\", \"pointerleave\"]);\n\n/**\n * Reads the interaction behind a **public action** from the DOM event it was\n * handed.\n *\n * Stimulus always passes the event to an action it invoked from `data-action`,\n * and a consumer calling the method themselves passes nothing — which is what\n * separates `\"api\"` from the rest.\n *\n * Only an action entry may resolve a reason this way. Inside the component the\n * event type no longer identifies the interaction: one `click` is the trigger\n * being pressed, the page outside being pressed, and an item being activated,\n * and those paths pass their own reason instead.\n *\n * @param event - The event the action received, if any.\n * @returns `\"api\"` with no event, else the modality the event type names, else\n * `\"user\"`.\n *\n * @example\n * ```ts\n * toggle(event?: Event): void {\n * this.#apply(!this.#isOpen, stateReasonFor(event));\n * }\n * ```\n */\nexport function stateReasonFor(event?: Event | null): \"user\" | \"focus\" | \"pointer\" | \"api\" {\n if (!event) return \"api\";\n if (FOCUS_EVENTS.has(event.type)) return \"focus\";\n if (POINTER_EVENTS.has(event.type)) return \"pointer\";\n return \"user\";\n}\n","import { stateReasonFor } from \"./state_reason\";\n\n/** The DOM binding and origin of a public action, or its explicit API element. */\nexport interface ActionSource {\n readonly event: Event | null;\n readonly host: Element | null;\n readonly origin: Element | null;\n readonly reason: ReturnType<typeof stateReasonFor>;\n}\n\n/**\n * Normalizes an action's DOM event or explicit element without choosing an owned target.\n *\n * Event hosts and origins stay separate so consumers can yield to nested controls.\n * An explicit element supplies both, with no DOM event and an `\"api\"` reason.\n * Consumers resolve current targets and enforce their own scope and interaction rules.\n *\n * @param source - The DOM event, explicit element, or absent optional argument.\n * @returns The source elements, original event and public interaction reason.\n */\nexport function actionSource(source?: Event | HTMLElement): ActionSource {\n if (source instanceof HTMLElement) {\n return { event: null, host: source, origin: source, reason: \"api\" };\n }\n const event = source ?? null;\n return {\n event,\n host: event?.currentTarget instanceof Element ? event.currentTarget : null,\n origin: event?.target instanceof Element ? event.target : null,\n reason: stateReasonFor(event),\n };\n}\n","/** Normalized scroll position and maximum distance on one logical axis. */\nexport interface LogicalScrollMetrics {\n position: number;\n max: number;\n}\n\n/**\n * Whether horizontal scrolling on `element` follows right-to-left inline flow.\n *\n * Resolved from the **computed** `direction`, so the authoring contract is the\n * usual `dir=\"rtl\"` (or a stylesheet) on the element or any ancestor.\n *\n * Scope: horizontal writing modes. A vertical writing mode (`writing-mode:\n * vertical-rl`) also inverts the horizontal axis, which this check does not\n * model — vertical writing modes are out of scope for the scroll utilities\n * (their consumers describe axes as horizontal/vertical, not inline/block).\n */\nexport function isRtl(element: Element): boolean {\n return window.getComputedStyle(element).direction === \"rtl\";\n}\n\n/**\n * Returns scroll distance from the logical start edge.\n *\n * CSSOM View exposes standards-mode RTL horizontal offsets as `0` at the inline\n * start (right) and increasingly negative values toward the inline end (left).\n * The normalized position is always clamped to `[0, max]`, which also absorbs\n * Safari's elastic overscroll values.\n */\nexport function logicalScrollMetrics(\n element: HTMLElement,\n horizontal: boolean,\n): LogicalScrollMetrics {\n const max = Math.max(\n 0,\n horizontal\n ? element.scrollWidth - element.clientWidth\n : element.scrollHeight - element.clientHeight,\n );\n const raw = horizontal ? element.scrollLeft : element.scrollTop;\n const position = horizontal && isRtl(element) ? -raw : raw;\n return { position: Math.min(max, Math.max(0, position)), max };\n}\n\n/**\n * Converts a logical start/end delta to the physical value accepted by\n * `Element.scrollBy`.\n */\nexport function physicalScrollDelta(\n element: HTMLElement,\n horizontal: boolean,\n logicalDelta: number,\n): number {\n return horizontal && isRtl(element) ? -logicalDelta : logicalDelta;\n}\n","import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n\n/**\n * True when a press arrived carrying any modifier, for the keys the browser and\n * the OS own outright: return without calling `preventDefault()` and without\n * moving any state.\n *\n * `Control+Home` and `Control+End` jump the document to its ends, and a widget\n * that swallows them makes the shortcut work or not depending on where focus\n * happens to sit. {@link isReservedArrowChord} answers the same question for the\n * arrows, but returns `false` for every other key so that this one can decide.\n *\n * There is no `allow` list here on purpose. APG assigns no modifier chord to\n * `Home`/`End`, so a widget that wanted one would be claiming a combination the\n * pattern never gave it.\n *\n * @example\n * ```ts\n * case \"Home\":\n * case \"End\":\n * if (hasModifierChord(event)) return;\n * ```\n */\nexport function hasModifierChord(event: KeyboardEvent): boolean {\n return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;\n}\n","/**\n * The attribute selector for one declared target, in a controller's own namespace.\n *\n * Stimulus reads `data-<identifier>-target` as a **space-separated token list**, so a\n * single element can carry several target names at once. Matching the attribute value\n * whole would reach only the elements that declare exactly one name, which is why the\n * name is paired with `~=`.\n *\n * `identifier` is the name the controller is registered under, so the selector reaches\n * the markup that registration renders rather than one fixed spelling.\n */\nexport function targetSelector(identifier: string, name: string): string {\n return `[data-${identifier}-target~=\"${name}\"]`;\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { actionSource } from \"../utils/action_source\";\nimport { isReservedArrowChord } from \"../utils/arrow_step\";\nimport { isRtl } from \"../utils/logical_scroll\";\nimport type { StateReason } from \"../utils/state_reason\";\nimport { targetSelector } from \"../utils/target_selector\";\n\n/**\n * Headless, accessible tabs behavior.\n *\n * Markup contract (identifier: `stimeo--tabs`):\n * <div data-controller=\"stimeo--tabs\">\n * <div role=\"tablist\" aria-label=\"…\" data-stimeo--tabs-target=\"list\">\n * <button role=\"tab\" id=\"tab-1\" aria-controls=\"panel-1\"\n * data-stimeo--tabs-target=\"tab\"\n * data-action=\"click->stimeo--tabs#select\n * keydown->stimeo--tabs#onKeydown\">Tab 1</button>\n * <!-- more tabs -->\n * </div>\n * <div role=\"tabpanel\" id=\"panel-1\" aria-labelledby=\"tab-1\"\n * data-stimeo--tabs-target=\"panel\">…</div>\n * <!-- more panels -->\n * </div>\n *\n * Implements the WAI-ARIA APG **Tabs** pattern with **automatic activation**:\n * moving focus with the arrow keys immediately selects the focused tab. Tabs are\n * paired to panels by index (the Nth tab controls the Nth panel).\n *\n * @remarks\n * Behavior only. State is exposed through `aria-selected`, roving `tabindex`\n * (`0` for the active tab, `-1` for the rest), and the panel `hidden` attribute;\n * the consumer owns all styling. The required `list` target marks the tablist\n * container as part of the semantic contract — that container carries\n * `role=\"tablist\"` and an accessible name; the controller performs no runtime\n * work on it.\n *\n * Behavior provided:\n * - Click a tab to select it.\n * - `ArrowRight`/`ArrowLeft` move to and select the next/previous tab (wrapping);\n * `Home`/`End` select the first/last tab.\n * - Every selection move is reported: `stimeo--tabs:change` dispatches\n * `{ index: number, total: number, previous: number, reason: StateReason }`, after\n * `aria-selected`, the roving `tabindex` and the panels' `hidden` are written\n * and before focus moves. It is informational, so it is not cancelable.\n * Reselecting the tab that is already selected, the normalization in\n * {@link connect}, and {@link disconnect} are all silent.\n */\nexport class TabsController extends Controller<HTMLElement> {\n static override targets = [\"tab\", \"panel\", \"list\"];\n static actions = [\"onKeydown\", \"select\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly listTarget: HTMLElement;\n declare readonly tabTargets: HTMLButtonElement[];\n declare readonly panelTargets: HTMLElement[];\n\n /** Whether selection moves are reported: set once `connect()` settled the baseline. */\n #reporting = false;\n\n /**\n * Selects the initially active tab: the pre-selected one, else the first.\n *\n * `findIndex` makes this first-wins when the author marked several — the first\n * in DOM order is the only deterministic reading of \"which one did they mean\" —\n * and `#selectIndex` then writes an explicit value onto every tab, so a\n * forgotten `aria-selected` cannot leave a tab looking unselectable.\n */\n override connect(): void {\n const preselected = this.tabTargets.findIndex(\n (tab) => tab.getAttribute(\"aria-selected\") === \"true\",\n );\n this.#selectIndex(preselected === -1 ? 0 : preselected, { focus: false });\n this.#reporting = true;\n }\n\n /**\n * Stops reporting until the next `connect()` has settled its baseline, so that\n * normalization stays silent.\n */\n override disconnect(): void {\n this.#reporting = false;\n }\n\n /** Selects the clicked tab. Bound via `data-action` (click). */\n select(source: Event | HTMLElement): void {\n const { host, origin, reason } = actionSource(source);\n const tab = host?.closest<HTMLButtonElement>(targetSelector(this.identifier, \"tab\"));\n if (!tab || !this.tabTargets.includes(tab)) return;\n if (origin && origin.closest(`[data-controller~=\"${this.identifier}\"]`) !== this.element)\n return;\n const index = this.tabTargets.indexOf(tab);\n this.#selectIndex(index, { focus: false }, reason);\n }\n\n /** Implements arrow/Home/End navigation with automatic activation. */\n onKeydown(event: KeyboardEvent): void {\n // A descendant widget that already claimed the key (a grabbed drag handle, a\n // nested menu) must not ALSO act on it — composition depends on this yield.\n if (event.defaultPrevented) return;\n if (isReservedArrowChord(event)) return;\n const tabs = this.tabTargets;\n const currentIndex = tabs.indexOf(event.currentTarget as HTMLButtonElement);\n if (currentIndex === -1) return;\n\n let nextIndex: number | null = null;\n // Logical, not physical. APG defines these as \"next / previous control\", and\n // says a vertical arrangement swaps in Down/Up for the same meaning — so the\n // pair is one axis's spelling of an order, and the order reverses with the\n // writing direction. Read from the controller element: the container is what\n // lays the items out, and a child may carry its own `dir` (an LTR input\n // inside an RTL form is ordinary authoring).\n const step = isRtl(this.element) ? -1 : 1;\n switch (event.key) {\n case \"ArrowRight\":\n nextIndex = (currentIndex + step + tabs.length) % tabs.length;\n break;\n case \"ArrowLeft\":\n nextIndex = (currentIndex - step + tabs.length) % tabs.length;\n break;\n case \"Home\":\n nextIndex = 0;\n break;\n case \"End\":\n nextIndex = tabs.length - 1;\n break;\n default:\n return;\n }\n\n event.preventDefault();\n this.#selectIndex(nextIndex, { focus: true });\n }\n\n /**\n * Activates the tab/panel pair at `index`: updates `aria-selected`, the roving\n * `tabindex`, and panel visibility. Optionally moves focus to the new tab.\n */\n #selectIndex(index: number, { focus }: { focus: boolean }, reason: StateReason = \"user\"): void {\n const tabs = this.tabTargets;\n const previous = tabs.findIndex((tab) => tab.getAttribute(\"aria-selected\") === \"true\");\n tabs.forEach((tab, i) => {\n const selected = i === index;\n tab.setAttribute(\"aria-selected\", selected ? \"true\" : \"false\");\n tab.tabIndex = selected ? 0 : -1;\n });\n this.panelTargets.forEach((panel, i) => {\n panel.hidden = i !== index;\n });\n if (previous !== index && this.#reporting) {\n this.dispatch(\"change\", {\n detail: { index, total: tabs.length, previous, reason },\n cancelable: false,\n });\n }\n // A subscriber may select a different tab from the handler above; focusing\n // then leaves the caret on a tab that is no longer the selected one.\n if (focus && tabs[index]?.getAttribute(\"aria-selected\") === \"true\") tabs[index]?.focus();\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/state_reason.ts","../../src/utils/action_source.ts","../../src/utils/logical_scroll.ts","../../src/utils/arrow_step.ts","../../src/utils/target_selector.ts","../../src/controllers/tabs_controller.ts"],"names":[],"mappings":"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","file":"tabs_controller.js","sourcesContent":["/**\n * Why a component's public state moved, carried as `detail.reason`\n * on the state event that reports the move.\n *\n * The attribute a component publishes (`aria-expanded`, `hidden`, `data-state`)\n * only says *what* the state is now. A subscriber that mirrors the state\n * elsewhere, or reports it, needs *why*: closing on `Escape` and closing because\n * the consumer called the action are the same attribute write and different\n * events to the page around it.\n *\n * `\"api\"` is the one every subscriber has to look at. A close the consumer asked\n * for arrives back at their own listener, so a listener that closes something\n * else on `close` loops unless it ignores its own calls.\n *\n * | Reason | The state moved because |\n * | --- | --- |\n * | `\"user\"` | a control of this component was operated |\n * | `\"select\"` | an item inside it was activated |\n * | `\"escape\"` | `Escape` was pressed and this layer owned it |\n * | `\"outside\"` | a pointer landed outside it, or on its backdrop |\n * | `\"focus\"` | focus entered or left it |\n * | `\"pointer\"` | the pointer entered or left it |\n * | `\"scroll\"` | a tracked scroll container scrolled |\n * | `\"api\"` | a public action was called with no DOM event |\n * | `\"timeout\"` | an automatic timer advanced or dismissed the state |\n * | `\"limit\"` | a capacity limit dismissed an item |\n * | `\"media\"` | an operating-system media preference changed |\n */\nexport type StateReason =\n | \"user\"\n | \"select\"\n | \"escape\"\n | \"outside\"\n | \"focus\"\n | \"pointer\"\n | \"scroll\"\n | \"api\"\n | \"timeout\"\n | \"limit\"\n | \"media\";\n\n/** Focus modality: the events a focus move delivers to an action. */\nconst FOCUS_EVENTS = new Set([\"blur\", \"focus\", \"focusin\", \"focusout\"]);\n\n/** Pointer modality: the crossing events an action is bound to for hover. */\nconst POINTER_EVENTS = new Set([\"mouseenter\", \"mouseleave\", \"pointerenter\", \"pointerleave\"]);\n\n/**\n * Reads the interaction behind a **public action** from the DOM event it was\n * handed.\n *\n * Stimulus always passes the event to an action it invoked from `data-action`,\n * and a consumer calling the method themselves passes nothing — which is what\n * separates `\"api\"` from the rest.\n *\n * Only an action entry may resolve a reason this way. Inside the component the\n * event type no longer identifies the interaction: one `click` is the trigger\n * being pressed, the page outside being pressed, and an item being activated,\n * and those paths pass their own reason instead.\n *\n * @param event - The event the action received, if any.\n * @returns `\"api\"` with no event, else the modality the event type names, else\n * `\"user\"`.\n *\n * @example\n * ```ts\n * toggle(event?: Event): void {\n * this.#apply(!this.#isOpen, stateReasonFor(event));\n * }\n * ```\n */\nexport function stateReasonFor(event?: Event | null): \"user\" | \"focus\" | \"pointer\" | \"api\" {\n if (!event) return \"api\";\n if (FOCUS_EVENTS.has(event.type)) return \"focus\";\n if (POINTER_EVENTS.has(event.type)) return \"pointer\";\n return \"user\";\n}\n","import { stateReasonFor } from \"./state_reason\";\n\n/** The DOM binding and origin of a public action, or its explicit API element. */\nexport interface ActionSource {\n readonly event: Event | null;\n readonly host: Element | null;\n readonly origin: Element | null;\n readonly reason: ReturnType<typeof stateReasonFor>;\n}\n\n/**\n * Normalizes an action's DOM event or explicit element without choosing an owned target.\n *\n * Event hosts and origins stay separate so consumers can yield to nested controls.\n * An explicit element supplies both, with no DOM event and an `\"api\"` reason.\n * Consumers resolve current targets and enforce their own scope and interaction rules.\n *\n * @param source - The DOM event, explicit element, or absent optional argument.\n * @returns The source elements, original event and public interaction reason.\n */\nexport function actionSource(source?: Event | HTMLElement): ActionSource {\n if (source instanceof HTMLElement) {\n return { event: null, host: source, origin: source, reason: \"api\" };\n }\n const event = source ?? null;\n return {\n event,\n host: event?.currentTarget instanceof Element ? event.currentTarget : null,\n origin: event?.target instanceof Element ? event.target : null,\n reason: stateReasonFor(event),\n };\n}\n","/** Normalized scroll position and maximum distance on one logical axis. */\nexport interface LogicalScrollMetrics {\n position: number;\n max: number;\n}\n\n/**\n * Whether horizontal scrolling on `element` follows right-to-left inline flow.\n *\n * Resolved from the **computed** `direction`, so the authoring contract is the\n * usual `dir=\"rtl\"` (or a stylesheet) on the element or any ancestor.\n *\n * Scope: horizontal writing modes. A vertical writing mode (`writing-mode:\n * vertical-rl`) also inverts the horizontal axis, which this check does not\n * model — vertical writing modes are out of scope for the scroll utilities\n * (their consumers describe axes as horizontal/vertical, not inline/block).\n */\nexport function isRtl(element: Element): boolean {\n return window.getComputedStyle(element).direction === \"rtl\";\n}\n\n/**\n * Returns scroll distance from the logical start edge.\n *\n * CSSOM View exposes standards-mode RTL horizontal offsets as `0` at the inline\n * start (right) and increasingly negative values toward the inline end (left).\n * The normalized position is always clamped to `[0, max]`, which also absorbs\n * Safari's elastic overscroll values.\n */\nexport function logicalScrollMetrics(\n element: HTMLElement,\n horizontal: boolean,\n): LogicalScrollMetrics {\n const max = Math.max(\n 0,\n horizontal\n ? element.scrollWidth - element.clientWidth\n : element.scrollHeight - element.clientHeight,\n );\n const raw = horizontal ? element.scrollLeft : element.scrollTop;\n const position = horizontal && isRtl(element) ? -raw : raw;\n return { position: Math.min(max, Math.max(0, position)), max };\n}\n\n/**\n * Converts a logical start/end delta to the physical value accepted by\n * `Element.scrollBy`.\n */\nexport function physicalScrollDelta(\n element: HTMLElement,\n horizontal: boolean,\n logicalDelta: number,\n): number {\n return horizontal && isRtl(element) ? -logicalDelta : logicalDelta;\n}\n","import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/**\n * The element whose writing direction orders a grid's columns: the nearest\n * `<table>` holding `grid` inside `root`, else `grid` itself. Table columns follow\n * the table's direction, so a direction declared only on a row group such as\n * `<tbody>` does not reorder them and must not reverse the arrows either.\n */\nexport function gridDirectionElement(grid: Element, root: Element): Element {\n const table = grid.closest(\"table\");\n return table !== null && root.contains(table) ? table : grid;\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — for example `Alt+Down`/`Alt+Up`, which the APG Combobox\n * pattern makes optional and `stimeo--combobox` implements.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n\n/**\n * True when a press carries Alt, Control, Meta, or Shift, regardless of its key.\n *\n * This predicate reports modifier presence only; key ownership stays with the\n * caller. Check the widget's implemented key/modifier bindings and their state\n * conditions before using this result to yield a chord. An unimplemented chord\n * must not fall through to the bare-key behavior.\n *\n * Dedicated bindings, such as a grid's Control+Home/End navigation, belong in\n * the caller. Unlike {@link isReservedArrowChord}, this function examines every\n * key and has no arrow-specific reservation or `allow` list.\n *\n * @example\n * ```ts\n * // A Home/End handler with no modifier bindings yields every modified press.\n * case \"Home\":\n * case \"End\":\n * if (hasModifierChord(event)) return;\n * ```\n */\nexport function hasModifierChord(event: KeyboardEvent): boolean {\n return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;\n}\n","/**\n * The attribute selector for one declared target, in a controller's own namespace.\n *\n * Stimulus reads `data-<identifier>-target` as a **space-separated token list**, so a\n * single element can carry several target names at once. Matching the attribute value\n * whole would reach only the elements that declare exactly one name, which is why the\n * name is paired with `~=`.\n *\n * `identifier` is the name the controller is registered under, so the selector reaches\n * the markup that registration renders rather than one fixed spelling.\n */\nexport function targetSelector(identifier: string, name: string): string {\n return `[data-${identifier}-target~=\"${name}\"]`;\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { actionSource } from \"../utils/action_source\";\nimport { hasModifierChord, isReservedArrowChord } from \"../utils/arrow_step\";\nimport { isRtl } from \"../utils/logical_scroll\";\nimport type { StateReason } from \"../utils/state_reason\";\nimport { targetSelector } from \"../utils/target_selector\";\n\n/**\n * Headless, accessible tabs behavior.\n *\n * Markup contract (identifier: `stimeo--tabs`):\n * <div data-controller=\"stimeo--tabs\">\n * <div role=\"tablist\" aria-label=\"…\" data-stimeo--tabs-target=\"list\">\n * <button role=\"tab\" id=\"tab-1\" aria-controls=\"panel-1\"\n * data-stimeo--tabs-target=\"tab\"\n * data-action=\"click->stimeo--tabs#select\n * keydown->stimeo--tabs#onKeydown\">Tab 1</button>\n * <!-- more tabs -->\n * </div>\n * <div role=\"tabpanel\" id=\"panel-1\" aria-labelledby=\"tab-1\"\n * data-stimeo--tabs-target=\"panel\">…</div>\n * <!-- more panels -->\n * </div>\n *\n * Implements the WAI-ARIA APG **Tabs** pattern with **automatic activation**:\n * moving focus with the arrow keys immediately selects the focused tab. Tabs are\n * paired to panels by index (the Nth tab controls the Nth panel).\n *\n * @remarks\n * Behavior only. State is exposed through `aria-selected`, roving `tabindex`\n * (`0` for the active tab, `-1` for the rest), and the panel `hidden` attribute;\n * the consumer owns all styling. The required `list` target marks the tablist\n * container as part of the semantic contract — that container carries\n * `role=\"tablist\"` and an accessible name. The controller writes nothing to it;\n * it reads the container's writing direction for the arrow keys, falling back to\n * the controller element when the target is absent.\n *\n * Behavior provided:\n * - Click a tab to select it.\n * - The forward horizontal arrow (`ArrowRight`, or `ArrowLeft` when the tablist is\n * right-to-left) moves to and selects the next tab, and the backward one the\n * previous (wrapping); `Home`/`End` select the first/last tab.\n * - Every selection move is reported: `stimeo--tabs:change` dispatches\n * `{ index: number, total: number, previous: number, reason: StateReason }`, after\n * `aria-selected`, the roving `tabindex` and the panels' `hidden` are written\n * and before focus moves. It is informational, so it is not cancelable.\n * Reselecting the tab that is already selected, the normalization in\n * {@link connect}, and {@link disconnect} are all silent.\n */\nexport class TabsController extends Controller<HTMLElement> {\n static override targets = [\"tab\", \"panel\", \"list\"];\n static actions = [\"onKeydown\", \"select\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly listTarget: HTMLElement;\n declare readonly hasListTarget: boolean;\n declare readonly tabTargets: HTMLButtonElement[];\n declare readonly panelTargets: HTMLElement[];\n\n /** Whether selection moves are reported: set once `connect()` settled the baseline. */\n #reporting = false;\n\n /**\n * Selects the initially active tab: the pre-selected one, else the first.\n *\n * `findIndex` makes this first-wins when the author marked several — the first\n * in DOM order is the only deterministic reading of \"which one did they mean\" —\n * and `#selectIndex` then writes an explicit value onto every tab, so a\n * forgotten `aria-selected` cannot leave a tab looking unselectable.\n */\n override connect(): void {\n const preselected = this.tabTargets.findIndex(\n (tab) => tab.getAttribute(\"aria-selected\") === \"true\",\n );\n this.#selectIndex(preselected === -1 ? 0 : preselected, { focus: false });\n this.#reporting = true;\n }\n\n /**\n * Stops reporting until the next `connect()` has settled its baseline, so that\n * normalization stays silent.\n */\n override disconnect(): void {\n this.#reporting = false;\n }\n\n /** Selects the clicked tab. Bound via `data-action` (click). */\n select(source: Event | HTMLElement): void {\n const { host, origin, reason } = actionSource(source);\n const tab = host?.closest<HTMLButtonElement>(targetSelector(this.identifier, \"tab\"));\n if (!tab || !this.tabTargets.includes(tab)) return;\n if (origin && origin.closest(`[data-controller~=\"${this.identifier}\"]`) !== this.element)\n return;\n const index = this.tabTargets.indexOf(tab);\n this.#selectIndex(index, { focus: false }, reason);\n }\n\n /** Implements arrow/Home/End navigation with automatic activation. */\n onKeydown(event: KeyboardEvent): void {\n // A descendant widget that already claimed the key (a grabbed drag handle, a\n // nested menu) must not ALSO act on it — composition depends on this yield.\n if (event.defaultPrevented) return;\n if (isReservedArrowChord(event)) return;\n if ((event.key === \"Home\" || event.key === \"End\") && hasModifierChord(event)) return;\n const tabs = this.tabTargets;\n const currentIndex = tabs.indexOf(event.currentTarget as HTMLButtonElement);\n if (currentIndex === -1) return;\n\n let nextIndex: number | null = null;\n /**\n * Horizontal navigation follows the current tablist's computed direction.\n * A missing list uses the controller element, while a tab's own direction\n * never changes the sequence.\n */\n const directionElement = this.hasListTarget ? this.listTarget : this.element;\n const step = isRtl(directionElement) ? -1 : 1;\n switch (event.key) {\n case \"ArrowRight\":\n nextIndex = (currentIndex + step + tabs.length) % tabs.length;\n break;\n case \"ArrowLeft\":\n nextIndex = (currentIndex - step + tabs.length) % tabs.length;\n break;\n case \"Home\":\n nextIndex = 0;\n break;\n case \"End\":\n nextIndex = tabs.length - 1;\n break;\n default:\n return;\n }\n\n event.preventDefault();\n this.#selectIndex(nextIndex, { focus: true });\n }\n\n /**\n * Activates the tab/panel pair at `index`: updates `aria-selected`, the roving\n * `tabindex`, and panel visibility. Optionally moves focus to the new tab.\n */\n #selectIndex(index: number, { focus }: { focus: boolean }, reason: StateReason = \"user\"): void {\n const tabs = this.tabTargets;\n const previous = tabs.findIndex((tab) => tab.getAttribute(\"aria-selected\") === \"true\");\n tabs.forEach((tab, i) => {\n const selected = i === index;\n tab.setAttribute(\"aria-selected\", selected ? \"true\" : \"false\");\n tab.tabIndex = selected ? 0 : -1;\n });\n this.panelTargets.forEach((panel, i) => {\n panel.hidden = i !== index;\n });\n if (previous !== index && this.#reporting) {\n this.dispatch(\"change\", {\n detail: { index, total: tabs.length, previous, reason },\n cancelable: false,\n });\n }\n // A subscriber may select a different tab from the handler above; focusing\n // then leaves the caret on a tab that is no longer the selected one.\n if (focus && tabs[index]?.getAttribute(\"aria-selected\") === \"true\") tabs[index]?.focus();\n }\n}\n"]}
@@ -29,8 +29,8 @@ import { Controller } from '@hotwired/stimulus';
29
29
  *
30
30
  * There is no single established APG pattern; this composes a labeled text input
31
31
  * with a roving-tabindex list of removable chips, mapping to WCAG 2.1.1, 2.4.7,
32
- * 4.1.2, 4.1.3, and 1.3.1. Unlike {@link MultiSelectController} (pick from a
33
- * candidate list) the user types arbitrary strings.
32
+ * 4.1.2, 4.1.3, and 1.3.1. Unlike `stimeo--multi-select` (pick from a candidate
33
+ * list) the user types arbitrary strings.
34
34
  *
35
35
  * Behavior provided:
36
36
  * - `Enter` or the configured delimiter commits the trimmed input as a tag.
@@ -48,8 +48,9 @@ import { Controller } from '@hotwired/stimulus';
48
48
  * DOM/Turbo tag changes dispatch `stimeo--tags-input:reconcile`; authored
49
49
  * templates route localized addition/removal text to the shared announcer.
50
50
  * - The remove buttons form one roving Tab stop: `ArrowLeft`/`ArrowRight` move
51
- * between them (right past the end returns to the input), `Delete`/`Backspace`
51
+ * between them (past the logical end returns to the input), `Delete`/`Backspace`
52
52
  * delete the focused tag, and `Backspace` on an empty input deletes the last.
53
+ * The current `tags` container defines horizontal direction for the chips and input boundary.
53
54
  * - Removing from a chip moves focus to its neighbor (else the input); removing
54
55
  * via empty-input `Backspace` keeps focus in the input for continued editing.
55
56
  * `change` and `reconcile` dispatch `{ tags: string[] }`; `reject` dispatches
@@ -1,5 +1,12 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
+ function fillTemplate(template, values) {
4
+ return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
5
+ const replacement = Object.hasOwn(values, name) ? values[name] : void 0;
6
+ return replacement === void 0 ? match : String(replacement);
7
+ });
8
+ }
9
+
3
10
  function announce(message, options = {}) {
4
11
  const text = message.trim();
5
12
  if (text.length === 0) return;
@@ -9,12 +16,6 @@ function announce(message, options = {}) {
9
16
  })
10
17
  );
11
18
  }
12
- function fillTemplate(template, values) {
13
- return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
14
- const replacement = values[name];
15
- return replacement === void 0 ? match : String(replacement);
16
- });
17
- }
18
19
 
19
20
  function isRtl(element) {
20
21
  return window.getComputedStyle(element).direction === "rtl";
@@ -67,7 +68,7 @@ var RovingTabindex = class {
67
68
  };
68
69
 
69
70
  var ChipRow = class {
70
- #directionElement;
71
+ #getDirectionElement;
71
72
  #getItems;
72
73
  #getButton;
73
74
  #onRemove;
@@ -75,7 +76,7 @@ var ChipRow = class {
75
76
  #roving = new RovingTabindex(() => this.buttons);
76
77
  #container = null;
77
78
  constructor(options) {
78
- this.#directionElement = options.directionElement;
79
+ this.#getDirectionElement = options.getDirectionElement;
79
80
  this.#getItems = options.getItems;
80
81
  this.#getButton = options.getButton ?? ((item) => matchingPart(item, "button"));
81
82
  this.#onRemove = options.onRemove;
@@ -156,7 +157,7 @@ var ChipRow = class {
156
157
  if (!entry) return;
157
158
  const buttons = entry.buttons;
158
159
  const index = entry.buttonIndex;
159
- switch (logicalArrowKey(event.key, this.#directionElement)) {
160
+ switch (logicalArrowKey(event.key, this.#getDirectionElement())) {
160
161
  case "ArrowLeft":
161
162
  if (index > 0) {
162
163
  event.preventDefault();
@@ -186,63 +187,182 @@ var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
186
187
  "deleteCompositionText",
187
188
  "deleteByComposition"
188
189
  ]);
190
+ function isCompositionInputType(inputType) {
191
+ return inputType !== void 0 && COMPOSITION_INPUT_TYPES.has(inputType);
192
+ }
193
+ var BEFORE_MORPH_ATTRIBUTE = "turbo:before-morph-attribute";
194
+ var textOf = (element) => element.value;
195
+ var holdsFocus = (element) => element instanceof Node && element.getRootNode().activeElement === element;
189
196
  var CompositionTracker = class {
190
197
  #observedTargets = /* @__PURE__ */ new Set();
191
- #activeTargets = /* @__PURE__ */ new Set();
198
+ #composition = null;
192
199
  #onStart;
193
200
  #onEnd;
201
+ #onAbandon;
194
202
  #confirmedTarget = null;
203
+ #keyComposing = false;
204
+ #connected = false;
205
+ #abandoned = null;
195
206
  constructor(options = {}) {
196
207
  this.#onStart = options.onStart;
197
208
  this.#onEnd = options.onEnd;
209
+ this.#onAbandon = options.onAbandon;
198
210
  }
199
211
  observe(target) {
212
+ this.#connected = true;
200
213
  if (this.#observedTargets.has(target)) return;
201
214
  target.addEventListener("compositionstart", this.#handleStart);
202
215
  target.addEventListener("compositionend", this.#handleEnd);
203
- target.addEventListener("keydown", this.#handleKeydown);
216
+ target.addEventListener("keydown", this.#handleKeydown, true);
217
+ target.addEventListener("beforeinput", this.#handleBeforeInput, true);
218
+ target.addEventListener("input", this.#handleInput, true);
219
+ target.addEventListener("blur", this.#handleBlur, true);
220
+ target.addEventListener(BEFORE_MORPH_ATTRIBUTE, this.#handleBeforeMorphAttribute);
204
221
  this.#observedTargets.add(target);
205
222
  }
206
223
  unobserve(target) {
207
224
  if (!this.#observedTargets.delete(target)) return;
208
- target.removeEventListener("compositionstart", this.#handleStart);
209
- target.removeEventListener("compositionend", this.#handleEnd);
210
- target.removeEventListener("keydown", this.#handleKeydown);
211
- this.#activeTargets.delete(target);
225
+ this.#release(target);
226
+ if (this.#composition?.target === target) this.#letGo();
212
227
  if (this.#confirmedTarget === target) this.#confirmedTarget = null;
213
228
  }
214
229
  disconnect() {
215
- for (const target of this.#observedTargets) {
216
- target.removeEventListener("compositionstart", this.#handleStart);
217
- target.removeEventListener("compositionend", this.#handleEnd);
218
- target.removeEventListener("keydown", this.#handleKeydown);
219
- }
230
+ this.#connected = false;
231
+ for (const target of this.#observedTargets) this.#release(target);
220
232
  this.#observedTargets.clear();
221
- this.#activeTargets.clear();
233
+ this.#letGo();
222
234
  this.#confirmedTarget = null;
235
+ this.#keyComposing = false;
236
+ this.#abandoned = null;
223
237
  }
224
238
  isComposing(event) {
225
- return this.#activeTargets.size > 0 || event?.isComposing === true;
239
+ this.#settle();
240
+ return event?.isComposing === true || this.#composition !== null;
241
+ }
242
+ composingElement() {
243
+ this.#settle();
244
+ return this.#composition?.element ?? null;
245
+ }
246
+ owns(event) {
247
+ const element = this.composingElement();
248
+ return event.isComposing === true || element !== null && element === event.target;
226
249
  }
227
250
  consumesConfirmedInput(event) {
228
251
  const confirmed = this.#confirmedTarget;
229
252
  this.#confirmedTarget = null;
230
253
  if (confirmed === null || confirmed !== event.target) return false;
231
254
  const inputType = event.inputType;
232
- return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);
255
+ return !inputType || isCompositionInputType(inputType);
233
256
  }
257
+ #release(target) {
258
+ target.removeEventListener("compositionstart", this.#handleStart);
259
+ target.removeEventListener("compositionend", this.#handleEnd);
260
+ target.removeEventListener("keydown", this.#handleKeydown, true);
261
+ target.removeEventListener("beforeinput", this.#handleBeforeInput, true);
262
+ target.removeEventListener("input", this.#handleInput, true);
263
+ target.removeEventListener("blur", this.#handleBlur, true);
264
+ target.removeEventListener(BEFORE_MORPH_ATTRIBUTE, this.#handleBeforeMorphAttribute);
265
+ }
266
+ #letGo() {
267
+ const composition = this.#composition;
268
+ if (composition === null) return;
269
+ if (!holdsFocus(composition.element)) {
270
+ this.#composition = null;
271
+ return;
272
+ }
273
+ queueMicrotask(() => {
274
+ if (this.#composition !== composition || this.#observedTargets.has(composition.target)) {
275
+ return;
276
+ }
277
+ this.#composition = null;
278
+ if (!this.#connected) return;
279
+ this.#abandoned = { element: composition.element, inPlace: true };
280
+ this.#runAbandon();
281
+ });
282
+ }
283
+ #settle() {
284
+ const composition = this.#composition;
285
+ if (composition === null) return;
286
+ const { target, element, text } = composition;
287
+ if (target instanceof Node && element instanceof Node && !target.contains(element)) {
288
+ this.#composition = null;
289
+ } else if (textOf(element) !== text) {
290
+ this.#abandon(false);
291
+ }
292
+ }
293
+ #abandon(inPlace) {
294
+ this.#owe(inPlace);
295
+ this.#composition = null;
296
+ queueMicrotask(this.#runAbandon);
297
+ }
298
+ #owe(inPlace) {
299
+ const composition = this.#composition;
300
+ if (composition !== null) this.#abandoned = { element: composition.element, inPlace };
301
+ }
302
+ #runAbandon = () => {
303
+ const abandoned = this.#abandoned;
304
+ if (abandoned === null) return;
305
+ this.#abandoned = null;
306
+ this.#onAbandon?.(abandoned.element, abandoned.inPlace);
307
+ };
234
308
  #handleStart = (event) => {
235
309
  this.#confirmedTarget = null;
236
- if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
310
+ this.#settle();
311
+ this.#owe(true);
312
+ const target = event.currentTarget;
313
+ const element = event.target;
314
+ this.#composition = { target, element, text: textOf(element) };
315
+ this.#runAbandon();
237
316
  this.#onStart?.(event);
238
317
  };
239
318
  #handleEnd = (event) => {
240
- if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
319
+ this.#runAbandon();
320
+ if (this.#composition?.target === event.currentTarget) this.#composition = null;
241
321
  this.#confirmedTarget = event.target;
242
322
  this.#onEnd?.(event);
243
323
  };
244
- #handleKeydown = () => {
324
+ #handleKeydown = (event) => {
325
+ this.#settle();
326
+ this.#runAbandon();
245
327
  this.#confirmedTarget = null;
328
+ this.#keyComposing = event.isComposing === true;
329
+ };
330
+ #handleBeforeInput = (event) => {
331
+ const keyComposing = this.#keyComposing;
332
+ this.#keyComposing = false;
333
+ this.#settle();
334
+ const { isComposing, inputType } = event;
335
+ if (this.#composition !== null && !keyComposing && !isComposing && inputType && !isCompositionInputType(inputType)) {
336
+ this.#abandon(true);
337
+ }
338
+ this.#runAbandon();
339
+ };
340
+ #handleInput = (event) => {
341
+ const composition = this.#composition;
342
+ const { isTrusted, isComposing, inputType } = event;
343
+ if (composition !== null && event.target === composition.element && (isTrusted || isComposing || isCompositionInputType(inputType))) {
344
+ composition.text = textOf(composition.element);
345
+ } else {
346
+ this.#settle();
347
+ }
348
+ this.#runAbandon();
349
+ };
350
+ #handleBlur = (event) => {
351
+ this.#settle();
352
+ this.#runAbandon();
353
+ const composition = this.#composition;
354
+ if (composition === null || composition.target === composition.element) return;
355
+ const { element } = composition;
356
+ if (!(element instanceof Node) || event.target !== element) return;
357
+ const root = element.getRootNode();
358
+ if (root.activeElement !== element) this.#composition = null;
359
+ };
360
+ #handleBeforeMorphAttribute = (event) => {
361
+ this.#settle();
362
+ const composition = this.#composition;
363
+ if (composition?.target !== event.currentTarget || event.target !== composition.element) return;
364
+ const detail = event.detail;
365
+ if (detail?.attributeName === "value") event.preventDefault();
246
366
  };
247
367
  };
248
368
 
@@ -295,6 +415,9 @@ var MicrotaskCoalescer = class {
295
415
  this.#queued = false;
296
416
  this.#generation += 1;
297
417
  }
418
+ get pending() {
419
+ return this.#queued;
420
+ }
298
421
  schedule() {
299
422
  if (!this.#active || this.#queued) return;
300
423
  this.#queued = true;
@@ -325,6 +448,9 @@ var MorphRenderWatcher = class {
325
448
  schedule() {
326
449
  this.#pass.schedule();
327
450
  }
451
+ get pending() {
452
+ return this.#pass.pending;
453
+ }
328
454
  disconnect() {
329
455
  this.#listeners.dispose();
330
456
  this.#pass.cancel();
@@ -481,7 +607,7 @@ var TagsInputController = class _TagsInputController extends Controller {
481
607
  });
482
608
  #reconcile = new MorphRenderWatcher(() => this.#reconcileTags());
483
609
  #chipRow = new ChipRow({
484
- directionElement: this.element,
610
+ getDirectionElement: () => this.#chipDirectionElement,
485
611
  getItems: () => this.tagTargets,
486
612
  getButton: (tag) => matchingPart(tag, `button${this.#rows.selector("remove")}`),
487
613
  onRemove: (index) => this.#removeAt(index),
@@ -554,7 +680,7 @@ var TagsInputController = class _TagsInputController extends Controller {
554
680
  }
555
681
  return;
556
682
  }
557
- if (logicalArrowKey(event.key, this.element) === "ArrowLeft" && this.inputTarget.value === "") {
683
+ if (logicalArrowKey(event.key, this.#chipDirectionElement) === "ArrowLeft" && this.inputTarget.value === "") {
558
684
  if (this.#chipRow.length > 0) {
559
685
  event.preventDefault();
560
686
  this.#chipRow.focusLast();
@@ -607,6 +733,9 @@ var TagsInputController = class _TagsInputController extends Controller {
607
733
  }
608
734
  if (!this.#chipRow.focusAfterRemoval(index)) this.#focusInput();
609
735
  }
736
+ get #chipDirectionElement() {
737
+ return this.hasTagsTarget ? this.tagsTarget : this.element;
738
+ }
610
739
  #focusInput() {
611
740
  if (this.hasInputTarget) this.inputTarget.focus();
612
741
  }