stimeo-ui 0.14.0 → 0.16.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 (279) hide show
  1. package/CHANGELOG.md +212 -0
  2. package/README.md +120 -0
  3. package/dist/cable/index.js +123 -29
  4. package/dist/cable/index.js.map +1 -1
  5. package/dist/controllers/accordion_controller.d.ts +30 -3
  6. package/dist/controllers/accordion_controller.js +98 -9
  7. package/dist/controllers/accordion_controller.js.map +1 -1
  8. package/dist/controllers/alert_dialog_controller.d.ts +8 -7
  9. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  10. package/dist/controllers/announcer_controller.d.ts +3 -2
  11. package/dist/controllers/announcer_controller.js +96 -62
  12. package/dist/controllers/announcer_controller.js.map +1 -1
  13. package/dist/controllers/auto_submit_controller.js +83 -7
  14. package/dist/controllers/auto_submit_controller.js.map +1 -1
  15. package/dist/controllers/avatar_controller.js +1 -1
  16. package/dist/controllers/avatar_controller.js.map +1 -1
  17. package/dist/controllers/breadcrumb_controller.d.ts +10 -7
  18. package/dist/controllers/breadcrumb_controller.js +38 -11
  19. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  20. package/dist/controllers/bulk_select_controller.d.ts +4 -4
  21. package/dist/controllers/bulk_select_controller.js +7 -6
  22. package/dist/controllers/bulk_select_controller.js.map +1 -1
  23. package/dist/controllers/calendar_controller.d.ts +101 -23
  24. package/dist/controllers/calendar_controller.js +340 -123
  25. package/dist/controllers/calendar_controller.js.map +1 -1
  26. package/dist/controllers/carousel_controller.d.ts +67 -22
  27. package/dist/controllers/carousel_controller.js +263 -38
  28. package/dist/controllers/carousel_controller.js.map +1 -1
  29. package/dist/controllers/character_counter_controller.d.ts +1 -1
  30. package/dist/controllers/character_counter_controller.js +40 -2
  31. package/dist/controllers/character_counter_controller.js.map +1 -1
  32. package/dist/controllers/checkbox_controller.js +81 -12
  33. package/dist/controllers/checkbox_controller.js.map +1 -1
  34. package/dist/controllers/clipboard_controller.d.ts +28 -2
  35. package/dist/controllers/clipboard_controller.js +63 -5
  36. package/dist/controllers/clipboard_controller.js.map +1 -1
  37. package/dist/controllers/collapsible_controller.d.ts +25 -1
  38. package/dist/controllers/collapsible_controller.js +99 -14
  39. package/dist/controllers/collapsible_controller.js.map +1 -1
  40. package/dist/controllers/color_picker_controller.d.ts +29 -8
  41. package/dist/controllers/color_picker_controller.js +80 -34
  42. package/dist/controllers/color_picker_controller.js.map +1 -1
  43. package/dist/controllers/combobox_controller.d.ts +10 -1
  44. package/dist/controllers/combobox_controller.js +106 -16
  45. package/dist/controllers/combobox_controller.js.map +1 -1
  46. package/dist/controllers/command_palette_controller.d.ts +3 -3
  47. package/dist/controllers/command_palette_controller.js +35 -3
  48. package/dist/controllers/command_palette_controller.js.map +1 -1
  49. package/dist/controllers/conditional_fields_controller.js +85 -17
  50. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  51. package/dist/controllers/confirm_controller.js +3 -0
  52. package/dist/controllers/confirm_controller.js.map +1 -1
  53. package/dist/controllers/context_menu_controller.d.ts +6 -0
  54. package/dist/controllers/context_menu_controller.js +32 -12
  55. package/dist/controllers/context_menu_controller.js.map +1 -1
  56. package/dist/controllers/count_up_controller.js.map +1 -1
  57. package/dist/controllers/countdown_controller.d.ts +30 -1
  58. package/dist/controllers/countdown_controller.js +129 -26
  59. package/dist/controllers/countdown_controller.js.map +1 -1
  60. package/dist/controllers/currency_input_controller.d.ts +77 -16
  61. package/dist/controllers/currency_input_controller.js +221 -67
  62. package/dist/controllers/currency_input_controller.js.map +1 -1
  63. package/dist/controllers/data_grid_controller.d.ts +63 -18
  64. package/dist/controllers/data_grid_controller.js +195 -29
  65. package/dist/controllers/data_grid_controller.js.map +1 -1
  66. package/dist/controllers/date_range_picker_controller.d.ts +41 -6
  67. package/dist/controllers/date_range_picker_controller.js +151 -30
  68. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  69. package/dist/controllers/dialog_controller.d.ts +10 -3
  70. package/dist/controllers/dialog_controller.js +35 -8
  71. package/dist/controllers/dialog_controller.js.map +1 -1
  72. package/dist/controllers/direct_upload_controller.js +22 -4
  73. package/dist/controllers/direct_upload_controller.js.map +1 -1
  74. package/dist/controllers/dirty_form_controller.d.ts +2 -2
  75. package/dist/controllers/dirty_form_controller.js +46 -13
  76. package/dist/controllers/dirty_form_controller.js.map +1 -1
  77. package/dist/controllers/dismissible_controller.js +1 -0
  78. package/dist/controllers/dismissible_controller.js.map +1 -1
  79. package/dist/controllers/drawer_controller.d.ts +18 -10
  80. package/dist/controllers/drawer_controller.js +54 -19
  81. package/dist/controllers/drawer_controller.js.map +1 -1
  82. package/dist/controllers/dropdown_controller.d.ts +9 -3
  83. package/dist/controllers/dropdown_controller.js +36 -9
  84. package/dist/controllers/dropdown_controller.js.map +1 -1
  85. package/dist/controllers/editable_controller.js +34 -0
  86. package/dist/controllers/editable_controller.js.map +1 -1
  87. package/dist/controllers/file_dropzone_controller.js +144 -51
  88. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  89. package/dist/controllers/filter_controller.d.ts +10 -4
  90. package/dist/controllers/filter_controller.js +20 -6
  91. package/dist/controllers/filter_controller.js.map +1 -1
  92. package/dist/controllers/flash_controller.d.ts +26 -6
  93. package/dist/controllers/flash_controller.js +432 -71
  94. package/dist/controllers/flash_controller.js.map +1 -1
  95. package/dist/controllers/focus_controller.js +1 -0
  96. package/dist/controllers/focus_controller.js.map +1 -1
  97. package/dist/controllers/form_field_controller.js +7 -5
  98. package/dist/controllers/form_field_controller.js.map +1 -1
  99. package/dist/controllers/form_validation_controller.js +19 -13
  100. package/dist/controllers/form_validation_controller.js.map +1 -1
  101. package/dist/controllers/frame_loading_controller.js +45 -8
  102. package/dist/controllers/frame_loading_controller.js.map +1 -1
  103. package/dist/controllers/highlight_controller.js +82 -25
  104. package/dist/controllers/highlight_controller.js.map +1 -1
  105. package/dist/controllers/hover_card_controller.d.ts +10 -2
  106. package/dist/controllers/hover_card_controller.js +40 -14
  107. package/dist/controllers/hover_card_controller.js.map +1 -1
  108. package/dist/controllers/idle_controller.d.ts +16 -3
  109. package/dist/controllers/idle_controller.js +90 -5
  110. package/dist/controllers/idle_controller.js.map +1 -1
  111. package/dist/controllers/input_mask_controller.d.ts +5 -2
  112. package/dist/controllers/input_mask_controller.js +65 -9
  113. package/dist/controllers/input_mask_controller.js.map +1 -1
  114. package/dist/controllers/intersection_controller.js +3 -0
  115. package/dist/controllers/intersection_controller.js.map +1 -1
  116. package/dist/controllers/lazy_frame_controller.js +11 -2
  117. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  118. package/dist/controllers/listbox_controller.d.ts +50 -8
  119. package/dist/controllers/listbox_controller.js +203 -45
  120. package/dist/controllers/listbox_controller.js.map +1 -1
  121. package/dist/controllers/local_time_controller.js +10 -5
  122. package/dist/controllers/local_time_controller.js.map +1 -1
  123. package/dist/controllers/masonry_controller.d.ts +3 -3
  124. package/dist/controllers/masonry_controller.js +31 -15
  125. package/dist/controllers/masonry_controller.js.map +1 -1
  126. package/dist/controllers/menu_controller.d.ts +9 -3
  127. package/dist/controllers/menu_controller.js +45 -16
  128. package/dist/controllers/menu_controller.js.map +1 -1
  129. package/dist/controllers/menubar_controller.d.ts +11 -0
  130. package/dist/controllers/menubar_controller.js +58 -24
  131. package/dist/controllers/menubar_controller.js.map +1 -1
  132. package/dist/controllers/meter_controller.js +9 -5
  133. package/dist/controllers/meter_controller.js.map +1 -1
  134. package/dist/controllers/multi_select_controller.d.ts +18 -3
  135. package/dist/controllers/multi_select_controller.js +278 -104
  136. package/dist/controllers/multi_select_controller.js.map +1 -1
  137. package/dist/controllers/navigation_menu_controller.d.ts +11 -0
  138. package/dist/controllers/navigation_menu_controller.js +48 -15
  139. package/dist/controllers/navigation_menu_controller.js.map +1 -1
  140. package/dist/controllers/nested_form_controller.js +37 -8
  141. package/dist/controllers/nested_form_controller.js.map +1 -1
  142. package/dist/controllers/network_status_controller.js +9 -1
  143. package/dist/controllers/network_status_controller.js.map +1 -1
  144. package/dist/controllers/number_input_controller.d.ts +36 -8
  145. package/dist/controllers/number_input_controller.js +124 -21
  146. package/dist/controllers/number_input_controller.js.map +1 -1
  147. package/dist/controllers/optimistic_controller.js +42 -5
  148. package/dist/controllers/optimistic_controller.js.map +1 -1
  149. package/dist/controllers/otp_controller.d.ts +22 -7
  150. package/dist/controllers/otp_controller.js +198 -55
  151. package/dist/controllers/otp_controller.js.map +1 -1
  152. package/dist/controllers/overflow_indicator_controller.d.ts +8 -12
  153. package/dist/controllers/overflow_indicator_controller.js +115 -21
  154. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  155. package/dist/controllers/overflow_menu_controller.d.ts +26 -6
  156. package/dist/controllers/overflow_menu_controller.js +141 -44
  157. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  158. package/dist/controllers/pagination_controller.d.ts +19 -11
  159. package/dist/controllers/pagination_controller.js +74 -28
  160. package/dist/controllers/pagination_controller.js.map +1 -1
  161. package/dist/controllers/password_reveal_controller.d.ts +15 -1
  162. package/dist/controllers/password_reveal_controller.js +59 -2
  163. package/dist/controllers/password_reveal_controller.js.map +1 -1
  164. package/dist/controllers/persist_controller.js +30 -8
  165. package/dist/controllers/persist_controller.js.map +1 -1
  166. package/dist/controllers/pointer_drag_controller.js +131 -52
  167. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  168. package/dist/controllers/popover_controller.d.ts +9 -3
  169. package/dist/controllers/popover_controller.js +45 -11
  170. package/dist/controllers/popover_controller.js.map +1 -1
  171. package/dist/controllers/portal_controller.d.ts +1 -1
  172. package/dist/controllers/portal_controller.js +6 -2
  173. package/dist/controllers/portal_controller.js.map +1 -1
  174. package/dist/controllers/preview_guard_controller.js +16 -1
  175. package/dist/controllers/preview_guard_controller.js.map +1 -1
  176. package/dist/controllers/progress_controller.js +8 -4
  177. package/dist/controllers/progress_controller.js.map +1 -1
  178. package/dist/controllers/radio_group_controller.d.ts +6 -4
  179. package/dist/controllers/radio_group_controller.js +42 -17
  180. package/dist/controllers/radio_group_controller.js.map +1 -1
  181. package/dist/controllers/range_slider_controller.d.ts +49 -1
  182. package/dist/controllers/range_slider_controller.js +88 -42
  183. package/dist/controllers/range_slider_controller.js.map +1 -1
  184. package/dist/controllers/rating_controller.d.ts +14 -3
  185. package/dist/controllers/rating_controller.js +39 -15
  186. package/dist/controllers/rating_controller.js.map +1 -1
  187. package/dist/controllers/read_more_controller.d.ts +24 -2
  188. package/dist/controllers/read_more_controller.js +100 -7
  189. package/dist/controllers/read_more_controller.js.map +1 -1
  190. package/dist/controllers/reading_progress_controller.js +65 -19
  191. package/dist/controllers/reading_progress_controller.js.map +1 -1
  192. package/dist/controllers/relative_time_controller.js +10 -5
  193. package/dist/controllers/relative_time_controller.js.map +1 -1
  194. package/dist/controllers/resizable_controller.d.ts +16 -2
  195. package/dist/controllers/resizable_controller.js +82 -22
  196. package/dist/controllers/resizable_controller.js.map +1 -1
  197. package/dist/controllers/scroll_area_controller.js +75 -27
  198. package/dist/controllers/scroll_area_controller.js.map +1 -1
  199. package/dist/controllers/scroll_restore_controller.js +37 -16
  200. package/dist/controllers/scroll_restore_controller.js.map +1 -1
  201. package/dist/controllers/scroll_visibility_controller.js +49 -30
  202. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  203. package/dist/controllers/scrollspy_controller.d.ts +3 -2
  204. package/dist/controllers/scrollspy_controller.js +71 -26
  205. package/dist/controllers/scrollspy_controller.js.map +1 -1
  206. package/dist/controllers/separator_controller.d.ts +41 -14
  207. package/dist/controllers/separator_controller.js +66 -37
  208. package/dist/controllers/separator_controller.js.map +1 -1
  209. package/dist/controllers/sidebar_controller.d.ts +20 -3
  210. package/dist/controllers/sidebar_controller.js +77 -18
  211. package/dist/controllers/sidebar_controller.js.map +1 -1
  212. package/dist/controllers/skeleton_controller.js +6 -1
  213. package/dist/controllers/skeleton_controller.js.map +1 -1
  214. package/dist/controllers/slider_controller.d.ts +45 -7
  215. package/dist/controllers/slider_controller.js +82 -47
  216. package/dist/controllers/slider_controller.js.map +1 -1
  217. package/dist/controllers/smart_sticky_header_controller.js +60 -26
  218. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  219. package/dist/controllers/sortable_controller.js +17 -2
  220. package/dist/controllers/sortable_controller.js.map +1 -1
  221. package/dist/controllers/spinner_controller.js +10 -2
  222. package/dist/controllers/spinner_controller.js.map +1 -1
  223. package/dist/controllers/step_indicator_controller.d.ts +19 -17
  224. package/dist/controllers/step_indicator_controller.js +18 -17
  225. package/dist/controllers/step_indicator_controller.js.map +1 -1
  226. package/dist/controllers/stepper_controller.d.ts +34 -9
  227. package/dist/controllers/stepper_controller.js +101 -19
  228. package/dist/controllers/stepper_controller.js.map +1 -1
  229. package/dist/controllers/stick_to_bottom_controller.d.ts +19 -0
  230. package/dist/controllers/stick_to_bottom_controller.js +104 -8
  231. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  232. package/dist/controllers/submit_once_controller.d.ts +3 -2
  233. package/dist/controllers/submit_once_controller.js +45 -9
  234. package/dist/controllers/submit_once_controller.js.map +1 -1
  235. package/dist/controllers/switch_controller.d.ts +29 -7
  236. package/dist/controllers/switch_controller.js +101 -10
  237. package/dist/controllers/switch_controller.js.map +1 -1
  238. package/dist/controllers/tabs_controller.d.ts +12 -0
  239. package/dist/controllers/tabs_controller.js +21 -2
  240. package/dist/controllers/tabs_controller.js.map +1 -1
  241. package/dist/controllers/tags_input_controller.d.ts +15 -3
  242. package/dist/controllers/tags_input_controller.js +209 -59
  243. package/dist/controllers/tags_input_controller.js.map +1 -1
  244. package/dist/controllers/textarea_autosize_controller.js +29 -3
  245. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  246. package/dist/controllers/theme_controller.d.ts +20 -3
  247. package/dist/controllers/theme_controller.js +64 -14
  248. package/dist/controllers/theme_controller.js.map +1 -1
  249. package/dist/controllers/time_picker_controller.js +23 -8
  250. package/dist/controllers/time_picker_controller.js.map +1 -1
  251. package/dist/controllers/toast_controller.d.ts +55 -15
  252. package/dist/controllers/toast_controller.js +451 -105
  253. package/dist/controllers/toast_controller.js.map +1 -1
  254. package/dist/controllers/toggle_group_controller.d.ts +49 -7
  255. package/dist/controllers/toggle_group_controller.js +159 -23
  256. package/dist/controllers/toggle_group_controller.js.map +1 -1
  257. package/dist/controllers/toolbar_controller.js +32 -0
  258. package/dist/controllers/toolbar_controller.js.map +1 -1
  259. package/dist/controllers/tooltip_controller.d.ts +8 -0
  260. package/dist/controllers/tooltip_controller.js +39 -13
  261. package/dist/controllers/tooltip_controller.js.map +1 -1
  262. package/dist/controllers/transition_controller.js +4 -0
  263. package/dist/controllers/transition_controller.js.map +1 -1
  264. package/dist/controllers/tree_view_controller.d.ts +39 -8
  265. package/dist/controllers/tree_view_controller.js +169 -16
  266. package/dist/controllers/tree_view_controller.js.map +1 -1
  267. package/dist/index.d.ts +28 -1
  268. package/dist/index.js +5002 -1911
  269. package/dist/index.js.map +1 -1
  270. package/dist/inspector/cli.d.ts +72 -6
  271. package/dist/inspector/cli.js +262 -51
  272. package/dist/inspector/cli.js.map +1 -1
  273. package/dist/inspector/cli_bin.js +309 -51
  274. package/dist/inspector/cli_bin.js.map +1 -1
  275. package/dist/inspector/examples.json +40 -40
  276. package/dist/inspector/manifest.json +529 -61
  277. package/dist/positioning/index.js +2 -0
  278. package/dist/positioning/index.js.map +1 -1
  279. package/package.json +2 -2
@@ -18,6 +18,29 @@ function isReservedArrowChord(event, allow = []) {
18
18
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
19
19
  }
20
20
 
21
+ // src/utils/field_mirror.ts
22
+ function writeFields(container, values, { name, form = "" }) {
23
+ const current = [...container.children];
24
+ const submits = current.length === values.length && current.every(
25
+ (child, index) => child instanceof HTMLInputElement && child.type === "hidden" && child.value === values[index] && child.name === name && (child.getAttribute("form") ?? "") === form
26
+ );
27
+ if (submits) return false;
28
+ container.replaceChildren(
29
+ ...values.map((value) => {
30
+ const input = document.createElement("input");
31
+ input.type = "hidden";
32
+ input.name = name;
33
+ input.value = value;
34
+ if (form !== "") input.setAttribute("form", form);
35
+ return input;
36
+ })
37
+ );
38
+ return true;
39
+ }
40
+ function commitField(target) {
41
+ target.dispatchEvent(new Event("change", { bubbles: true }));
42
+ }
43
+
21
44
  // src/utils/interactive_host.ts
22
45
  var INTERACTIVE_HOST_SELECTOR = "button, input, select, textarea, label, a[href], area[href], summary, details, audio[controls], video[controls], iframe, object, embed";
23
46
  function isInteractiveHost(element) {
@@ -70,48 +93,71 @@ var MicrotaskCoalescer = class {
70
93
 
71
94
  // src/controllers/data_grid_controller.ts
72
95
  var SORT_CYCLE = ["none", "ascending", "descending"];
96
+ var OBSERVED_ATTRIBUTES = ["aria-selected", "data-value"];
97
+ function setAttributeIfChanged(element, name, value) {
98
+ if (element.getAttribute(name) !== value) element.setAttribute(name, value);
99
+ }
100
+ function sameSelection(a, b) {
101
+ if (a.size !== b.size) return false;
102
+ for (const [row, value] of a) {
103
+ if (!b.has(row) || b.get(row) !== value) return false;
104
+ }
105
+ return true;
106
+ }
73
107
  function nextSortDirection(current) {
74
108
  const index = SORT_CYCLE.indexOf(current);
75
109
  const from = index < 0 ? 0 : index;
76
110
  return SORT_CYCLE[(from + 1) % SORT_CYCLE.length] ?? "ascending";
77
111
  }
78
112
  var DataGridController = class extends Controller {
79
- static targets = ["columnHeader", "row", "cell"];
113
+ static targets = ["columnHeader", "row", "cell", "fields"];
80
114
  static values = {
81
- selection: { type: String, default: "none" }
115
+ selection: { type: String, default: "none" },
116
+ name: { type: String, default: "rows[]" },
117
+ form: { type: String, default: "" }
82
118
  };
83
119
  static actions = ["onKeydown", "sort", "toggleSelect"];
84
- static events = ["selectionchange", "sort"];
120
+ static events = ["selectionchange", "sort", "reconcile"];
85
121
  /**
86
- * Collapses the per-element target callbacks of one DOM mutation into a single
87
- * baseline pass, and refuses to run before `connect()` or after `disconnect()`.
122
+ * Collapses the target callbacks, Value changes and observed row writes of one
123
+ * DOM mutation into a single pass, and refuses to run before `connect()` or
124
+ * after `disconnect()`.
88
125
  *
89
126
  * Stimulus reports every target one at a time, so an ungated pass would re-walk
90
127
  * the whole grid once per authored cell on mount and once per streamed cell
91
128
  * afterwards — quadratic in the cell count both times.
92
129
  */
93
- #reconcile = new MicrotaskCoalescer(() => this.#restoreBaseline());
130
+ #reconcile = new MicrotaskCoalescer(() => this.#reconcileRows());
131
+ /** Watches the row attributes a page can rewrite in place; set while connected. */
132
+ #observer = null;
133
+ /** The selection last settled: on connect, by the user, or by a reported pass. */
134
+ #settled = /* @__PURE__ */ new Map();
94
135
  /**
95
- * Establishes a single tab stop across all navigable cells/headers and brings
96
- * the rows to their baseline.
136
+ * Establishes a single tab stop across all navigable cells/headers, brings the
137
+ * rows to their baseline, and settles the selection without reporting it.
97
138
  *
98
- * Normalizing here rather than leaving it to {@link selectionValueChanged}
99
- * guarantees exactly one pass per mount: a re-attached element reuses its
100
- * cached Stimulus context, whose value observer already knows the `selection`
101
- * attribute, so the Value callback does not fire a second time.
139
+ * The whole baseline runs here rather than in the Value callbacks: Stimulus
140
+ * delivers those before `connect()`, where the pass is refused, and a
141
+ * re-attached element reuses its cached context, whose value observer does not
142
+ * fire again.
102
143
  */
103
144
  connect() {
104
145
  this.#restoreBaseline();
146
+ this.#settled = this.#selection();
105
147
  this.#reconcile.activate();
148
+ this.#observeRows();
106
149
  }
107
- /** Closes the reconcile window so a queued pass cannot run against a detached tree. */
150
+ /** Releases the row observer and drops a queued pass, so neither outlives the element. */
108
151
  disconnect() {
109
152
  this.#reconcile.cancel();
153
+ this.#observer?.disconnect();
154
+ this.#observer = null;
110
155
  }
111
156
  /**
112
- * Rebuilds both DOM-owned baselines from the live grid: exactly one navigable
113
- * cell is in the Tab sequence, and every selectable row carries an explicit
114
- * `aria-selected`.
157
+ * Rebuilds the DOM-owned baselines from the live grid: exactly one navigable
158
+ * cell is in the Tab sequence, `aria-multiselectable` follows `selection`, every
159
+ * selectable row carries an explicit `aria-selected`, and the fields mirror the
160
+ * selected rows.
115
161
  *
116
162
  * The tab stop keeps whichever cell already holds it, so a rebuild triggered by
117
163
  * an unrelated row arriving does not throw the user's position away; only when
@@ -124,21 +170,60 @@ var DataGridController = class extends Controller {
124
170
  const cells = this.#navigableCells();
125
171
  const active = cells.find((cell) => cell.tabIndex === 0) ?? cells[0];
126
172
  if (active) this.#setActiveCell(active, { focus: false }, cells);
173
+ this.#syncSelectable();
174
+ const pageWrote = this.#takePageRecords();
127
175
  this.#normalizeSelection();
176
+ this.#dropOwnRecords(pageWrote);
177
+ this.#mirrorFields(false);
178
+ }
179
+ /**
180
+ * The pass the page's changes run: the baselines are rebuilt, then a selection
181
+ * that moved is reported. The report comes last, so rows a subscriber rewrites
182
+ * are the next pass's to settle.
183
+ */
184
+ #reconcileRows() {
185
+ this.#restoreBaseline();
186
+ this.#reportMove();
187
+ }
188
+ /**
189
+ * Reports the selection as `reconcile` when it differs from the one last
190
+ * settled. The settled selection is replaced before dispatching, so a move a
191
+ * subscriber makes is measured against what it was told.
192
+ */
193
+ #reportMove() {
194
+ const selection = this.#selection();
195
+ if (sameSelection(selection, this.#settled)) return;
196
+ this.#settled = selection;
197
+ this.dispatch("reconcile", { detail: { rows: [...selection.keys()] } });
128
198
  }
129
199
  /**
130
- * Keeps `aria-multiselectable` in step with the `selection` Value. Fires on connect
131
- * (so it self-heals after a Turbo morph) and on any runtime change, so the ARIA
132
- * never drifts from the selection logic, which reads `selectionValue` live.
200
+ * Re-renders `aria-multiselectable`, the rows and the fields when application
201
+ * code (or a Turbo morph) changes `selection` at runtime; a selection the new
202
+ * mode collapses is reported as `reconcile`.
133
203
  */
134
204
  selectionValueChanged() {
135
- this.#syncSelectable();
136
- this.#normalizeSelection();
205
+ this.#reconcile.schedule();
206
+ }
207
+ /** Rebuilds the submitted fields when the public name changes at runtime. */
208
+ nameValueChanged() {
209
+ this.#reconcile.schedule();
210
+ }
211
+ /** Repoints the submitted fields when the owning form changes at runtime. */
212
+ formValueChanged() {
213
+ this.#reconcile.schedule();
214
+ }
215
+ /** Seeds a fields container inserted after connect from the current selection. */
216
+ fieldsTargetConnected() {
217
+ this.#reconcile.schedule();
137
218
  }
138
219
  /** Re-establishes the baselines for a row added after connect. */
139
220
  rowTargetConnected() {
140
221
  this.#reconcile.schedule();
141
222
  }
223
+ /** Settles the selection again when a row leaves the grid or drops its target token. */
224
+ rowTargetDisconnected() {
225
+ this.#reconcile.schedule();
226
+ }
142
227
  /** Re-establishes the tab stop when a cell joins the grid after connect. */
143
228
  cellTargetConnected() {
144
229
  this.#reconcile.schedule();
@@ -165,6 +250,8 @@ var DataGridController = class extends Controller {
165
250
  * A grid that declares `selection="none"` has no selectable rows, so the
166
251
  * attribute is removed rather than written: in ARIA its absence is what "not
167
252
  * selectable" looks like.
253
+ *
254
+ * @stimeoRenderRoot
168
255
  */
169
256
  #normalizeSelection() {
170
257
  const rows = this.rowTargets;
@@ -176,9 +263,9 @@ var DataGridController = class extends Controller {
176
263
  const first = rows.find((row) => row.getAttribute("aria-selected") === "true");
177
264
  for (const row of rows) {
178
265
  if (single) {
179
- row.setAttribute("aria-selected", row === first ? "true" : "false");
266
+ setAttributeIfChanged(row, "aria-selected", row === first ? "true" : "false");
180
267
  } else if (row.getAttribute("aria-selected") !== "true") {
181
- row.setAttribute("aria-selected", "false");
268
+ setAttributeIfChanged(row, "aria-selected", "false");
182
269
  }
183
270
  }
184
271
  }
@@ -186,10 +273,12 @@ var DataGridController = class extends Controller {
186
273
  * Mirrors `selection="multiple"` onto `aria-multiselectable` (APG Grid) so SRs
187
274
  * announce that more than one row can be selected; cleared for single/none so a
188
275
  * grid never carries a misleading attribute.
276
+ *
277
+ * @stimeoRenderRoot
189
278
  */
190
279
  #syncSelectable() {
191
280
  if (this.selectionValue === "multiple") {
192
- this.element.setAttribute("aria-multiselectable", "true");
281
+ setAttributeIfChanged(this.element, "aria-multiselectable", "true");
193
282
  } else {
194
283
  this.element.removeAttribute("aria-multiselectable");
195
284
  }
@@ -288,7 +377,12 @@ var DataGridController = class extends Controller {
288
377
  if (control && host.contains(control)) return control;
289
378
  return isInteractiveHost(source) ? source : null;
290
379
  }
291
- /** Performs a header's sort or a cell row's selection toggle on activation. */
380
+ /**
381
+ * Performs a header's sort or a cell row's selection toggle on activation.
382
+ *
383
+ * @stimeoRuntimeOnly `selection` decides whether this one activation toggles the row; the sort it
384
+ * cycles does not depend on it.
385
+ */
292
386
  #activate(cell) {
293
387
  if (this.columnHeaderTargets.includes(cell)) {
294
388
  this.#cycleSort(cell);
@@ -306,17 +400,89 @@ var DataGridController = class extends Controller {
306
400
  }
307
401
  this.dispatch("sort", { detail: { column: header, direction } });
308
402
  }
309
- /** Toggles a row's `aria-selected`, honoring single vs. multiple selection. */
403
+ /**
404
+ * Toggles a row's `aria-selected`, honoring single vs. multiple selection.
405
+ *
406
+ * @stimeoRuntimeOnly `selection` decides how this one toggle treats the other rows.
407
+ */
310
408
  #toggleRow(row) {
311
409
  const selected = row.getAttribute("aria-selected") === "true";
410
+ const pageWrote = this.#takePageRecords();
312
411
  if (this.selectionValue === "single") {
313
412
  for (const other of this.rowTargets) {
314
- if (other !== row) other.setAttribute("aria-selected", "false");
413
+ if (other !== row) setAttributeIfChanged(other, "aria-selected", "false");
315
414
  }
316
415
  }
317
416
  row.setAttribute("aria-selected", selected ? "false" : "true");
318
- const rows = this.rowTargets.filter((r) => r.getAttribute("aria-selected") === "true");
319
- this.dispatch("selectionchange", { detail: { rows } });
417
+ this.#dropOwnRecords(pageWrote);
418
+ this.#settled = this.#selection();
419
+ this.#mirrorFields(true);
420
+ this.dispatch("selectionchange", { detail: { rows: [...this.#settled.keys()] } });
421
+ }
422
+ /** The selected rows in DOM order, each with the `data-value` it submits, if any. */
423
+ #selection() {
424
+ return new Map(
425
+ this.rowTargets.filter((row) => row.getAttribute("aria-selected") === "true").map((row) => [row, row.dataset.value])
426
+ );
427
+ }
428
+ /**
429
+ * Watches the rows' `aria-selected` and `data-value`. Every write the grid makes
430
+ * to them sits between a take and a drop of the observer's queue, so each record
431
+ * the callback receives is the page's, and only a record on one of the grid's own
432
+ * rows schedules a pass.
433
+ */
434
+ #observeRows() {
435
+ const observer = new MutationObserver((records) => {
436
+ if (this.#concernsRows(records)) this.#reconcile.schedule();
437
+ });
438
+ observer.observe(this.element, {
439
+ subtree: true,
440
+ attributes: true,
441
+ attributeFilter: OBSERVED_ATTRIBUTES
442
+ });
443
+ this.#observer = observer;
444
+ }
445
+ /**
446
+ * Empties the observer's queue before the grid writes to the rows, and says
447
+ * whether the page had queued anything about them. Those records still owe a
448
+ * pass: a toggle in `multiple` mode rewrites only its own row, so it does not fold
449
+ * in what the page wrote to the others.
450
+ */
451
+ #takePageRecords() {
452
+ const records = this.#observer?.takeRecords() ?? [];
453
+ return records.length > 0 && this.#concernsRows(records);
454
+ }
455
+ /**
456
+ * Whether any record is about one of this grid's own rows. A widget nested in a
457
+ * cell writes the same attributes on its own elements, and those are no reason
458
+ * to reconcile the grid.
459
+ */
460
+ #concernsRows(records) {
461
+ const rows = new Set(this.rowTargets);
462
+ return records.some((record) => rows.has(record.target));
463
+ }
464
+ /**
465
+ * Drops the records the grid's own writes just queued, so the observer never
466
+ * takes them for the page's, then schedules the pass the page's records owe.
467
+ */
468
+ #dropOwnRecords(pageWrote) {
469
+ this.#observer?.takeRecords();
470
+ if (pageWrote) this.#reconcile.schedule();
471
+ }
472
+ /**
473
+ * Mirrors the selected rows' `data-value` into the optional fields container.
474
+ * A selected row without one submits nothing, so a grid whose rows carry no
475
+ * value submits an empty set rather than a row of blanks.
476
+ *
477
+ * @stimeoRenderRoot
478
+ */
479
+ #mirrorFields(notify) {
480
+ if (!this.hasFieldsTarget) return;
481
+ const values = this.rowTargets.filter((row) => row.getAttribute("aria-selected") === "true").map((row) => row.dataset.value).filter((value) => value !== void 0);
482
+ const options = { name: this.nameValue, form: this.formValue };
483
+ if (writeFields(this.fieldsTarget, values, options) && notify) {
484
+ commitField(this.fieldsTarget);
485
+ }
320
486
  }
321
487
  /**
322
488
  * Makes `cell` the single tabbable cell (roving) and optionally focuses it.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/logical_scroll.ts","../../src/utils/arrow_step.ts","../../src/utils/interactive_host.ts","../../src/utils/microtask_coalescer.ts","../../src/controllers/data_grid_controller.ts"],"names":[],"mappings":"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","file":"data_grid_controller.js","sourcesContent":["/** 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 * Native interactive elements whose built-in activation must not be replaced\n * wholesale by a controller. Deliberately excludes `[tabindex]`: a generic\n * element made focusable is the supported host for several headless patterns.\n * Contenteditable is handled by {@link isInteractiveHost} because its state is\n * inherited and its keywords are ASCII-case-insensitive.\n */\nexport const INTERACTIVE_HOST_SELECTOR =\n \"button, input, select, textarea, label, a[href], area[href], summary, details, audio[controls], video[controls], iframe, object, embed\";\n\n/**\n * Whether an element owns native activation or an inherited editing surface.\n *\n * The `contenteditable` missing and invalid states inherit. Walking explicitly\n * also keeps `contenteditable=\"false\"` as a real boundary inside an editable\n * ancestor instead of treating any ancestor attribute as decisive.\n */\nexport function isInteractiveHost(element: HTMLElement): boolean {\n if (element.matches(INTERACTIVE_HOST_SELECTOR)) return true;\n\n let current: HTMLElement | null = element;\n while (current) {\n const raw = current.getAttribute(\"contenteditable\");\n if (raw !== null) {\n const value = raw.trim().toLowerCase();\n if (value === \"false\") return false;\n if (value === \"\" || value === \"true\" || value === \"plaintext-only\") return true;\n }\n current = current.parentElement;\n }\n return false;\n}\n","/**\n * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord, logicalArrowKey } from \"../utils/arrow_step\";\nimport { INTERACTIVE_HOST_SELECTOR, isInteractiveHost } from \"../utils/interactive_host\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\n\n/** Cycle order for a sortable column header's `aria-sort`. */\nconst SORT_CYCLE = [\"none\", \"ascending\", \"descending\"] as const;\ntype SortDirection = (typeof SORT_CYCLE)[number];\n\n/**\n * Returns the next `aria-sort` direction in the cycle after `current`. Unknown or\n * ARIA-only values (e.g. `\"other\"`, or an empty/missing attribute) are treated as\n * `\"none\"`, so the first activation always advances to `\"ascending\"` instead of\n * stalling.\n */\nfunction nextSortDirection(current: string): SortDirection {\n const index = SORT_CYCLE.indexOf(current as SortDirection);\n const from = index < 0 ? 0 : index;\n return SORT_CYCLE[(from + 1) % SORT_CYCLE.length] ?? \"ascending\";\n}\n\n/**\n * Headless, accessible **Data Grid** behavior: column sorting, row selection, and\n * roving keyboard navigation over an interactive table.\n *\n * Markup contract (identifier: `stimeo--data-grid`):\n * <table data-controller=\"stimeo--data-grid\" role=\"grid\" aria-label=\"Users\"\n * data-stimeo--data-grid-selection-value=\"single\">\n * <thead><tr role=\"row\">\n * <th role=\"columnheader\" aria-sort=\"none\" tabindex=\"-1\"\n * data-stimeo--data-grid-target=\"columnHeader\"\n * data-action=\"click->stimeo--data-grid#sort\n * keydown->stimeo--data-grid#onKeydown\">Name</th>\n * </tr></thead>\n * <tbody><tr role=\"row\" aria-selected=\"false\"\n * data-stimeo--data-grid-target=\"row\">\n * <td role=\"gridcell\" tabindex=\"0\" data-stimeo--data-grid-target=\"cell\"\n * data-action=\"keydown->stimeo--data-grid#onKeydown\">Jane</td>\n * </tr></tbody>\n * </table>\n *\n * Implements the WAI-ARIA APG **Grid** pattern plus `aria-sort`. The whole grid is\n * a single Tab stop (roving `tabindex`: exactly one cell/header is `0`, the rest\n * `-1`); arrow keys move both DOM focus and that tabbable position. Sort state is\n * exposed via `aria-sort` on headers, selection via `aria-selected` on rows.\n *\n * `selectionchange` dispatches `{ rows: HTMLElement[] }`; `sort` dispatches\n * `{ column: HTMLElement, direction: \"ascending\" | \"descending\" | \"none\" }`.\n *\n * @remarks\n * Behavior only — the consumer performs the actual data sort/render in response to\n * the `sort` event and owns all styling. No timers or observers are held, so there\n * is nothing to leak across Turbo navigations; `connect()` rebuilds the single tab\n * stop idempotently from the DOM, and the target callbacks rebuild it again after\n * rows or cells are added or removed at runtime.\n *\n * Behavior provided:\n * - `Arrow*` move between cells (clamped at edges); `Home`/`End` to the row's\n * first/last cell; `Ctrl+Home`/`Ctrl+End` to the grid's first/last cell.\n * - `Enter`/`Space` cycles a header's sort (`none→ascending→descending`) or toggles\n * the focused row's selection when selection is enabled.\n *\n * Consumer contract — controls nested inside a cell or header:\n * - APG's grid hosts working controls in its cells, so a keystroke or click that\n * reached one — a nested button, link or form control, an editable host, or any\n * widget that already called `preventDefault()` — is left to it entirely: no\n * move, no sort, no selection. The one control that hands the event on is a\n * sortable header's own `<button>`, whose activation is what the click carries.\n * - A control keeps its own Tab behavior, so give it `tabindex=\"-1\"` to preserve\n * the grid's single Tab stop. A sortable `columnheader` hosting a `<button>` is\n * the common case: the roving position stays on the header, and the button's\n * own activation reaches `sort` through the click that bubbles to it.\n * - Host `role=\"gridcell\"` / `role=\"columnheader\"` on a non-interactive element\n * (`td`, `th`); an interactive host makes the grid stand down on that cell.\n */\nexport class DataGridController extends Controller<HTMLElement> {\n static override targets = [\"columnHeader\", \"row\", \"cell\"];\n static override values = {\n selection: { type: String, default: \"none\" },\n };\n static actions = [\"onKeydown\", \"sort\", \"toggleSelect\"] as const;\n static events = [\"selectionchange\", \"sort\"] as const;\n\n declare readonly columnHeaderTargets: HTMLElement[];\n declare readonly rowTargets: HTMLElement[];\n declare readonly cellTargets: HTMLElement[];\n declare selectionValue: string;\n\n /**\n * Collapses the per-element target callbacks of one DOM mutation into a single\n * baseline pass, and refuses to run before `connect()` or after `disconnect()`.\n *\n * Stimulus reports every target one at a time, so an ungated pass would re-walk\n * the whole grid once per authored cell on mount and once per streamed cell\n * afterwards — quadratic in the cell count both times.\n */\n readonly #reconcile = new MicrotaskCoalescer(() => this.#restoreBaseline());\n\n /**\n * Establishes a single tab stop across all navigable cells/headers and brings\n * the rows to their baseline.\n *\n * Normalizing here rather than leaving it to {@link selectionValueChanged}\n * guarantees exactly one pass per mount: a re-attached element reuses its\n * cached Stimulus context, whose value observer already knows the `selection`\n * attribute, so the Value callback does not fire a second time.\n */\n override connect(): void {\n this.#restoreBaseline();\n this.#reconcile.activate();\n }\n\n /** Closes the reconcile window so a queued pass cannot run against a detached tree. */\n override disconnect(): void {\n this.#reconcile.cancel();\n }\n\n /**\n * Rebuilds both DOM-owned baselines from the live grid: exactly one navigable\n * cell is in the Tab sequence, and every selectable row carries an explicit\n * `aria-selected`.\n *\n * The tab stop keeps whichever cell already holds it, so a rebuild triggered by\n * an unrelated row arriving does not throw the user's position away; only when\n * no cell holds it — the grid is fresh, or the holder was removed — does the\n * first navigable cell take over. Without that fallback a grid whose active row\n * is removed keeps every cell at `-1` and drops out of the Tab sequence\n * entirely.\n */\n #restoreBaseline(): void {\n const cells = this.#navigableCells();\n const active = cells.find((cell) => cell.tabIndex === 0) ?? cells[0];\n if (active) this.#setActiveCell(active, { focus: false }, cells);\n this.#normalizeSelection();\n }\n\n /**\n * Keeps `aria-multiselectable` in step with the `selection` Value. Fires on connect\n * (so it self-heals after a Turbo morph) and on any runtime change, so the ARIA\n * never drifts from the selection logic, which reads `selectionValue` live.\n */\n selectionValueChanged(): void {\n this.#syncSelectable();\n this.#normalizeSelection();\n }\n\n /** Re-establishes the baselines for a row added after connect. */\n rowTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Re-establishes the tab stop when a cell joins the grid after connect. */\n cellTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Re-establishes the tab stop when a cell leaves the grid. */\n cellTargetDisconnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Re-establishes the tab stop when a header joins the grid after connect. */\n columnHeaderTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Re-establishes the tab stop when a header leaves the grid. */\n columnHeaderTargetDisconnected(): void {\n this.#reconcile.schedule();\n }\n\n /**\n * Brings the authored rows to the shape the APG requires, without changing\n * which rows the author chose.\n *\n * Every selectable row gets an explicit value — an absent `aria-selected` means\n * \"not selectable\" in ARIA, so a forgotten attribute hides a selectable row —\n * and a single-select grid keeps at most one `true`, first in DOM order.\n * A grid that declares `selection=\"none\"` has no selectable rows, so the\n * attribute is removed rather than written: in ARIA its absence is what \"not\n * selectable\" looks like.\n */\n #normalizeSelection(): void {\n const rows = this.rowTargets;\n if (this.selectionValue === \"none\") {\n // Reclaim, do not merely skip: a grid that *became* unselectable would\n // otherwise keep announcing rows as selected while the logic refuses to\n // change them. In ARIA the absence of the attribute is what \"not\n // selectable\" looks like, so the rows have to lose it outright.\n for (const row of rows) row.removeAttribute(\"aria-selected\");\n return;\n }\n const single = this.selectionValue === \"single\";\n const first = rows.find((row) => row.getAttribute(\"aria-selected\") === \"true\");\n for (const row of rows) {\n if (single) {\n row.setAttribute(\"aria-selected\", row === first ? \"true\" : \"false\");\n } else if (row.getAttribute(\"aria-selected\") !== \"true\") {\n row.setAttribute(\"aria-selected\", \"false\");\n }\n }\n }\n\n /**\n * Mirrors `selection=\"multiple\"` onto `aria-multiselectable` (APG Grid) so SRs\n * announce that more than one row can be selected; cleared for single/none so a\n * grid never carries a misleading attribute.\n */\n #syncSelectable(): void {\n if (this.selectionValue === \"multiple\") {\n this.element.setAttribute(\"aria-multiselectable\", \"true\");\n } else {\n this.element.removeAttribute(\"aria-multiselectable\");\n }\n }\n\n /** Cycles the activated column header's sort and emits `sort`. */\n sort(event: Event): void {\n const header = event.currentTarget as HTMLElement;\n if (!this.columnHeaderTargets.includes(header)) return;\n if (event.defaultPrevented) return;\n // A sortable header hosts a `<button>`, and that button's activation is\n // exactly what this click carries, so it is the one control that does not\n // take the event away. A link or a field inside the header is its own\n // destination, and sorting on its click would act in parallel.\n const control = this.#claimingControl(event, header);\n if (control && !(control instanceof HTMLButtonElement)) return;\n\n const direction = nextSortDirection(header.getAttribute(\"aria-sort\") ?? \"none\");\n\n // Only one column is sorted at a time: reset the others to `none`.\n for (const other of this.columnHeaderTargets) {\n other.setAttribute(\"aria-sort\", other === header ? direction : \"none\");\n }\n\n this.#setActiveCell(header, { focus: false });\n this.dispatch(\"sort\", { detail: { column: header, direction } });\n }\n\n /** Toggles selection of the row owning the event target. Bound optionally. */\n toggleSelect(event: Event): void {\n // The pointer path guards on `selection=\"none\"` exactly as the keyboard path\n // does, so a grid that declares itself unselectable never grows selected rows.\n if (this.selectionValue === \"none\") return;\n // A widget that handled the click owns it, exactly as the keyboard path\n // stands down on a keystroke a descendant consumed.\n if (event.defaultPrevented) return;\n const host = event.currentTarget as HTMLElement;\n if (this.#claimedByDescendant(event, host)) return;\n const row = host.closest<HTMLElement>(\"[role='row']\");\n if (row && this.rowTargets.includes(row)) this.#toggleRow(row);\n }\n\n /** Grid navigation + sort/select activation. Bound to cells and headers. */\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.isComposing) return;\n const cell = event.currentTarget as HTMLElement;\n // A native control inside the cell never calls `preventDefault()` — its\n // activation IS the default action — so the yield above cannot see it. This\n // recognises those by element shape instead.\n if (this.#claimedByDescendant(event, cell)) return;\n const matrix = this.#matrix();\n const position = this.#locate(matrix, cell);\n if (!position) return;\n const [row, col] = position;\n const rowCells = matrix[row] ?? [];\n\n let target: HTMLElement | undefined;\n // Logical, not physical. The key is normalised rather than the\n // delta negated: these two branches are not mirror images — their guards\n // differ — so swapping the key keeps each guard with its own direction.\n switch (logicalArrowKey(event.key, this.element)) {\n case \"ArrowRight\":\n target = this.#cellInRow(matrix, row, col + 1);\n break;\n case \"ArrowLeft\":\n target = this.#cellInRow(matrix, row, Math.max(col - 1, 0));\n break;\n case \"ArrowDown\":\n target = this.#cellInRow(matrix, Math.min(row + 1, matrix.length - 1), col);\n break;\n case \"ArrowUp\":\n target = this.#cellInRow(matrix, Math.max(row - 1, 0), col);\n break;\n case \"Home\":\n target = event.ctrlKey ? this.#cellInRow(matrix, 0, 0) : rowCells[0];\n break;\n case \"End\":\n target = event.ctrlKey\n ? this.#cellInRow(matrix, matrix.length - 1, Number.POSITIVE_INFINITY)\n : rowCells[rowCells.length - 1];\n break;\n case \"Enter\":\n case \" \":\n this.#activate(cell);\n event.preventDefault();\n return;\n default:\n return;\n }\n\n if (target) {\n event.preventDefault();\n this.#setActiveCell(target, { focus: true }, matrix.flat());\n }\n }\n\n /**\n * Whether the event was addressed to a control inside `host` rather than to the\n * grid.\n *\n * Cells and headers hold consumer markup, and APG's grid pattern expects that\n * markup to include working controls — a row action button, an inline editor.\n * Those own their own keystrokes and clicks, so the grid stands down entirely\n * rather than acting in parallel. An editable host (its `contenteditable` state\n * is inherited, so the walk is explicit) counts the same way.\n */\n #claimedByDescendant(event: Event, host: HTMLElement): boolean {\n return this.#claimingControl(event, host) !== null;\n }\n\n /**\n * The nested control this event belongs to, or `null` when the host owns it.\n *\n * Naming the control, rather than answering yes or no, is what lets the click\n * path treat a sortable header's `<button>` as the activation it is while every\n * other control still takes the event away.\n */\n #claimingControl(event: Event, host: HTMLElement): HTMLElement | null {\n const source = event.target as HTMLElement;\n const control = source.closest<HTMLElement>(INTERACTIVE_HOST_SELECTOR);\n if (control && host.contains(control)) return control;\n return isInteractiveHost(source) ? source : null;\n }\n\n /** Performs a header's sort or a cell row's selection toggle on activation. */\n #activate(cell: HTMLElement): void {\n if (this.columnHeaderTargets.includes(cell)) {\n this.#cycleSort(cell);\n return;\n }\n if (this.selectionValue === \"none\") return;\n const row = cell.closest<HTMLElement>(\"[role='row']\");\n if (row && this.rowTargets.includes(row)) this.#toggleRow(row);\n }\n\n /** Cycles a header's sort on keyboard activation and emits `sort`. */\n #cycleSort(header: HTMLElement): void {\n const direction = nextSortDirection(header.getAttribute(\"aria-sort\") ?? \"none\");\n for (const other of this.columnHeaderTargets) {\n other.setAttribute(\"aria-sort\", other === header ? direction : \"none\");\n }\n this.dispatch(\"sort\", { detail: { column: header, direction } });\n }\n\n /** Toggles a row's `aria-selected`, honoring single vs. multiple selection. */\n #toggleRow(row: HTMLElement): void {\n const selected = row.getAttribute(\"aria-selected\") === \"true\";\n // Enforce single-ness on every toggle, not only when turning a row on:\n // switching one *off* would otherwise leave a second `true` behind until\n // the next baseline pass.\n if (this.selectionValue === \"single\") {\n for (const other of this.rowTargets) {\n if (other !== row) other.setAttribute(\"aria-selected\", \"false\");\n }\n }\n row.setAttribute(\"aria-selected\", selected ? \"false\" : \"true\");\n\n const rows = this.rowTargets.filter((r) => r.getAttribute(\"aria-selected\") === \"true\");\n this.dispatch(\"selectionchange\", { detail: { rows } });\n }\n\n /**\n * Makes `cell` the single tabbable cell (roving) and optionally focuses it.\n *\n * `cells` lets a caller that already walked the grid hand its collection over,\n * so one keystroke rebuilds the matrix once instead of twice. The write is\n * skipped where the attribute already holds the wanted value — comparing the\n * attribute rather than the IDL property, because a cell with no `tabindex` at\n * all reports `-1` and would then never receive the attribute it needs to be\n * focusable.\n */\n #setActiveCell(\n cell: HTMLElement,\n { focus }: { focus: boolean },\n cells?: readonly HTMLElement[],\n ): void {\n for (const candidate of cells ?? this.#navigableCells()) {\n const wanted = candidate === cell ? \"0\" : \"-1\";\n if (candidate.getAttribute(\"tabindex\") !== wanted) {\n candidate.setAttribute(\"tabindex\", wanted);\n }\n }\n if (focus) cell.focus();\n }\n\n /** All navigable elements (headers + cells) in DOM order. */\n #navigableCells(): HTMLElement[] {\n return this.#matrix().flat();\n }\n\n /** The grid as rows of navigable cells, derived from each `role=\"row\"`. */\n #matrix(): HTMLElement[][] {\n const navigable = new Set<HTMLElement>([...this.columnHeaderTargets, ...this.cellTargets]);\n const rows = Array.from(this.element.querySelectorAll<HTMLElement>(\"[role='row']\"));\n return rows\n .map((row) =>\n Array.from(row.children).filter((child): child is HTMLElement =>\n navigable.has(child as HTMLElement),\n ),\n )\n .filter((cells) => cells.length > 0);\n }\n\n /** Finds `[rowIndex, colIndex]` of `cell` within `matrix`, or null. */\n #locate(matrix: HTMLElement[][], cell: HTMLElement): [number, number] | null {\n for (let row = 0; row < matrix.length; row++) {\n const col = (matrix[row] ?? []).indexOf(cell);\n if (col !== -1) return [row, col];\n }\n return null;\n }\n\n /** The cell at `[row, col]`, clamping `col` to that row's last cell. */\n #cellInRow(matrix: HTMLElement[][], row: number, col: number): HTMLElement | undefined {\n const cells = matrix[row];\n if (!cells || cells.length === 0) return undefined;\n return cells[Math.min(col, cells.length - 1)];\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/logical_scroll.ts","../../src/utils/arrow_step.ts","../../src/utils/field_mirror.ts","../../src/utils/interactive_host.ts","../../src/utils/microtask_coalescer.ts","../../src/controllers/data_grid_controller.ts"],"names":[],"mappings":"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","file":"data_grid_controller.js","sourcesContent":["/** 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 hidden form fields a widget keeps in step with the state it owns.\n *\n * A widget whose value lives in ARIA attributes submits nothing on its own, so\n * it mirrors that value into `<input type=\"hidden\">` — one field for a single\n * value, a container of generated fields for a set. The mirror is one-way: the\n * widget's own state stays the source of truth and the field is what the form\n * reads.\n *\n * The write and the report are separate calls because they answer to different\n * rules. A mirror is refreshed on connect, after a morph, and whenever the\n * widget repairs itself — none of which a form should treat as an edit. Only\n * the user's own commit reports one, which is why {@link commitField} is never\n * folded into the writes.\n */\n\n/** How a widget's generated fields identify themselves to the form. */\nexport interface GeneratedFieldOptions {\n /** The `name` every generated input submits under. */\n readonly name: string;\n /** The `form` id, for a widget that sits outside the form it submits to. */\n readonly form?: string;\n}\n\n/**\n * Mirrors one value into a hidden field.\n *\n * @returns Whether the field's value moved.\n *\n * @example\n * ```ts\n * if (writeField(this.fieldTarget, String(value)) && userDriven) {\n * commitField(this.fieldTarget);\n * }\n * ```\n */\nexport function writeField(field: HTMLInputElement, value: string): boolean {\n if (field.value === value) return false;\n field.value = value;\n return true;\n}\n\n/**\n * Mirrors a set of values into `container` as generated hidden fields, in the\n * order given.\n *\n * A container that already submits exactly this set is left untouched, children\n * and all — the mirror is refreshed far more often than it moves (every connect,\n * morph and repair), and a rebuild that changes nothing is still a DOM mutation\n * a consumer's observer and a `:empty` rule can see.\n *\n * @returns Whether the submitted set moved — its values, their order, the\n * `name`, or the `form`.\n */\nexport function writeFields(\n container: HTMLElement,\n values: readonly string[],\n { name, form = \"\" }: GeneratedFieldOptions,\n): boolean {\n const current = [...container.children];\n const submits =\n current.length === values.length &&\n current.every(\n (child, index) =>\n child instanceof HTMLInputElement &&\n child.type === \"hidden\" &&\n child.value === values[index] &&\n child.name === name &&\n (child.getAttribute(\"form\") ?? \"\") === form,\n );\n if (submits) return false;\n\n container.replaceChildren(\n ...values.map((value) => {\n const input = document.createElement(\"input\");\n input.type = \"hidden\";\n input.name = name;\n input.value = value;\n if (form !== \"\") input.setAttribute(\"form\", form);\n return input;\n }),\n );\n return true;\n}\n\n/**\n * Reports a committed value from `target` the way a native form control does:\n * one bubbling, non-cancelable `change`.\n *\n * Call it only for a move the user made. Form-level behaviors listen for this —\n * validation re-checks, auto-submit — and a mirror refreshed by a morph or a\n * repair is not something the user submitted.\n *\n * `target` is the element that carries the value: the hidden field for a single\n * mirrored value, the container for a generated set (whose own inputs are\n * replaced on every write), or a widget's own native control where that is what\n * the form reads.\n */\nexport function commitField(target: HTMLElement): void {\n target.dispatchEvent(new Event(\"change\", { bubbles: true }));\n}\n\n/**\n * Reports an edit a widget made on the user's behalf, the way the browser\n * reports its own: one bubbling `input`, then one bubbling `change`.\n *\n * This is for a widget that owns the stepping or picking its *own* native\n * control would otherwise do — writing `control.value` from script fires\n * nothing, so a form listening for either event never hears the edit. Use\n * {@link commitField} instead where the element is a mirror of state held\n * elsewhere: a hidden field has no native edit to imitate, so it reports the\n * commit alone.\n *\n * Call it only for a move the user made, and only where the widget really did\n * the writing — an edit the browser already reported would be doubled.\n */\nexport function commitEdit(control: HTMLElement): void {\n control.dispatchEvent(new Event(\"input\", { bubbles: true }));\n control.dispatchEvent(new Event(\"change\", { bubbles: true }));\n}\n","/**\n * Native interactive elements whose built-in activation must not be replaced\n * wholesale by a controller. Deliberately excludes `[tabindex]`: a generic\n * element made focusable is the supported host for several headless patterns.\n * Contenteditable is handled by {@link isInteractiveHost} because its state is\n * inherited and its keywords are ASCII-case-insensitive.\n */\nexport const INTERACTIVE_HOST_SELECTOR =\n \"button, input, select, textarea, label, a[href], area[href], summary, details, audio[controls], video[controls], iframe, object, embed\";\n\n/**\n * Whether an element owns native activation or an inherited editing surface.\n *\n * The `contenteditable` missing and invalid states inherit. Walking explicitly\n * also keeps `contenteditable=\"false\"` as a real boundary inside an editable\n * ancestor instead of treating any ancestor attribute as decisive.\n */\nexport function isInteractiveHost(element: HTMLElement): boolean {\n if (element.matches(INTERACTIVE_HOST_SELECTOR)) return true;\n\n let current: HTMLElement | null = element;\n while (current) {\n const raw = current.getAttribute(\"contenteditable\");\n if (raw !== null) {\n const value = raw.trim().toLowerCase();\n if (value === \"false\") return false;\n if (value === \"\" || value === \"true\" || value === \"plaintext-only\") return true;\n }\n current = current.parentElement;\n }\n return false;\n}\n","/**\n * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord, logicalArrowKey } from \"../utils/arrow_step\";\nimport { commitField, writeFields } from \"../utils/field_mirror\";\nimport { INTERACTIVE_HOST_SELECTOR, isInteractiveHost } from \"../utils/interactive_host\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\n\n/** Cycle order for a sortable column header's `aria-sort`. */\nconst SORT_CYCLE = [\"none\", \"ascending\", \"descending\"] as const;\ntype SortDirection = (typeof SORT_CYCLE)[number];\n\n/** Row attributes a page can rewrite in place that move the published selection. */\nconst OBSERVED_ATTRIBUTES = [\"aria-selected\", \"data-value\"];\n\n/** The published selection: each selected row with the value it submits. */\ntype Selection = ReadonlyMap<HTMLElement, string | undefined>;\n\n/**\n * Sets `name` on `element` only when its value differs. A same-value write still\n * queues a mutation record for every observer on the page, so an unchanged state\n * writes nothing.\n */\nfunction setAttributeIfChanged(element: Element, name: string, value: string): void {\n if (element.getAttribute(name) !== value) element.setAttribute(name, value);\n}\n\n/** Whether two selections hold the same rows submitting the same values, in any order. */\nfunction sameSelection(a: Selection, b: Selection): boolean {\n if (a.size !== b.size) return false;\n for (const [row, value] of a) {\n if (!b.has(row) || b.get(row) !== value) return false;\n }\n return true;\n}\n\n/**\n * Returns the next `aria-sort` direction in the cycle after `current`. Unknown or\n * ARIA-only values (e.g. `\"other\"`, or an empty/missing attribute) are treated as\n * `\"none\"`, so the first activation always advances to `\"ascending\"` instead of\n * stalling.\n */\nfunction nextSortDirection(current: string): SortDirection {\n const index = SORT_CYCLE.indexOf(current as SortDirection);\n const from = index < 0 ? 0 : index;\n return SORT_CYCLE[(from + 1) % SORT_CYCLE.length] ?? \"ascending\";\n}\n\n/**\n * Headless, accessible **Data Grid** behavior: column sorting, row selection, and\n * roving keyboard navigation over an interactive table.\n *\n * Markup contract (identifier: `stimeo--data-grid`):\n * <table data-controller=\"stimeo--data-grid\" role=\"grid\" aria-label=\"Users\"\n * data-stimeo--data-grid-selection-value=\"single\"\n * data-stimeo--data-grid-name-value=\"user_id\">\n * <caption><div data-stimeo--data-grid-target=\"fields\"></div></caption>\n * <thead><tr role=\"row\">\n * <th role=\"columnheader\" aria-sort=\"none\" tabindex=\"-1\"\n * data-stimeo--data-grid-target=\"columnHeader\"\n * data-action=\"click->stimeo--data-grid#sort\n * keydown->stimeo--data-grid#onKeydown\">Name</th>\n * </tr></thead>\n * <tbody><tr role=\"row\" aria-selected=\"false\" data-value=\"7\"\n * data-stimeo--data-grid-target=\"row\">\n * <td role=\"gridcell\" tabindex=\"0\" data-stimeo--data-grid-target=\"cell\"\n * data-action=\"keydown->stimeo--data-grid#onKeydown\">Jane</td>\n * </tr></tbody>\n * </table>\n *\n * Implements the WAI-ARIA APG **Grid** pattern plus `aria-sort`. The whole grid is\n * a single Tab stop (roving `tabindex`: exactly one cell/header is `0`, the rest\n * `-1`); arrow keys move both DOM focus and that tabbable position. Sort state is\n * exposed via `aria-sort` on headers, selection via `aria-selected` on rows.\n *\n * `selectionchange` dispatches `{ rows: HTMLElement[] }` when the user changes the\n * selection; `sort` dispatches\n * `{ column: HTMLElement, direction: \"ascending\" | \"descending\" | \"none\" }`.\n *\n * `reconcile` dispatches `{ rows: HTMLElement[] }` — the selected rows, as\n * `selectionchange` carries them — when the page moves the selection instead: a\n * selected row removed or dropped from the targets, a morph or a script writing\n * a row's `aria-selected` or a selected row's `data-value`, a selected row that\n * arrives ahead of the selection in `single` mode, or a runtime `selection` that\n * collapses it. One batch of such changes reports once, and only when the\n * selected rows or the values they submit differ from the selection last settled\n * — on connect, by the user, or by the previous `reconcile` — so connecting\n * reports nothing.\n *\n * With a `fields` target the selected rows are mirrored into `name`d hidden\n * inputs so the selection can be submitted and read server-side — one per\n * selected row that carries a `data-value`, in DOM order; a row without one\n * submits nothing. `form` points them at a `<form>` by id when the container\n * sits outside it. A selection the user made emits a native bubbling `change`\n * from the container, the way a form control does, so `stimeo--auto-submit` and\n * form-level validation hear it. The mirror is refreshed silently on connect,\n * on a replacement container, on row churn, and whenever `selection`, `name` or\n * `form` changes; a selection the page moved is reported by `reconcile` alone.\n *\n * @remarks\n * Behavior only — the consumer performs the actual data sort/render in response to\n * the `sort` event and owns all styling. While connected a `MutationObserver`\n * watches the rows' `aria-selected` and `data-value`, which a morph can rewrite\n * without adding or removing a target; `disconnect()` releases it and drops a\n * pending pass. `connect()` rebuilds the single tab stop idempotently from the\n * DOM, and the target callbacks rebuild it again after rows or cells are added or\n * removed at runtime.\n *\n * Behavior provided:\n * - `Arrow*` move between cells (clamped at edges); `Home`/`End` to the row's\n * first/last cell; `Ctrl+Home`/`Ctrl+End` to the grid's first/last cell.\n * - `Enter`/`Space` cycles a header's sort (`none→ascending→descending`) or toggles\n * the focused row's selection when selection is enabled.\n *\n * Consumer contract — controls nested inside a cell or header:\n * - APG's grid hosts working controls in its cells, so a keystroke or click that\n * reached one — a nested button, link or form control, an editable host, or any\n * widget that already called `preventDefault()` — is left to it entirely: no\n * move, no sort, no selection. The one control that hands the event on is a\n * sortable header's own `<button>`, whose activation is what the click carries.\n * - A control keeps its own Tab behavior, so give it `tabindex=\"-1\"` to preserve\n * the grid's single Tab stop. A sortable `columnheader` hosting a `<button>` is\n * the common case: the roving position stays on the header, and the button's\n * own activation reaches `sort` through the click that bubbles to it.\n * - Host `role=\"gridcell\"` / `role=\"columnheader\"` on a non-interactive element\n * (`td`, `th`); an interactive host makes the grid stand down on that cell.\n */\nexport class DataGridController extends Controller<HTMLElement> {\n static override targets = [\"columnHeader\", \"row\", \"cell\", \"fields\"];\n static override values = {\n selection: { type: String, default: \"none\" },\n name: { type: String, default: \"rows[]\" },\n form: { type: String, default: \"\" },\n };\n static actions = [\"onKeydown\", \"sort\", \"toggleSelect\"] as const;\n static events = [\"selectionchange\", \"sort\", \"reconcile\"] as const;\n\n declare readonly columnHeaderTargets: HTMLElement[];\n declare readonly rowTargets: HTMLElement[];\n declare readonly cellTargets: HTMLElement[];\n declare readonly fieldsTarget: HTMLElement;\n declare readonly hasFieldsTarget: boolean;\n declare selectionValue: string;\n declare nameValue: string;\n declare formValue: string;\n\n /**\n * Collapses the target callbacks, Value changes and observed row writes of one\n * DOM mutation into a single pass, and refuses to run before `connect()` or\n * after `disconnect()`.\n *\n * Stimulus reports every target one at a time, so an ungated pass would re-walk\n * the whole grid once per authored cell on mount and once per streamed cell\n * afterwards — quadratic in the cell count both times.\n */\n readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileRows());\n /** Watches the row attributes a page can rewrite in place; set while connected. */\n #observer: MutationObserver | null = null;\n /** The selection last settled: on connect, by the user, or by a reported pass. */\n #settled: Selection = new Map();\n\n /**\n * Establishes a single tab stop across all navigable cells/headers, brings the\n * rows to their baseline, and settles the selection without reporting it.\n *\n * The whole baseline runs here rather than in the Value callbacks: Stimulus\n * delivers those before `connect()`, where the pass is refused, and a\n * re-attached element reuses its cached context, whose value observer does not\n * fire again.\n */\n override connect(): void {\n this.#restoreBaseline();\n this.#settled = this.#selection();\n this.#reconcile.activate();\n this.#observeRows();\n }\n\n /** Releases the row observer and drops a queued pass, so neither outlives the element. */\n override disconnect(): void {\n this.#reconcile.cancel();\n this.#observer?.disconnect();\n this.#observer = null;\n }\n\n /**\n * Rebuilds the DOM-owned baselines from the live grid: exactly one navigable\n * cell is in the Tab sequence, `aria-multiselectable` follows `selection`, every\n * selectable row carries an explicit `aria-selected`, and the fields mirror the\n * selected rows.\n *\n * The tab stop keeps whichever cell already holds it, so a rebuild triggered by\n * an unrelated row arriving does not throw the user's position away; only when\n * no cell holds it — the grid is fresh, or the holder was removed — does the\n * first navigable cell take over. Without that fallback a grid whose active row\n * is removed keeps every cell at `-1` and drops out of the Tab sequence\n * entirely.\n */\n #restoreBaseline(): void {\n const cells = this.#navigableCells();\n const active = cells.find((cell) => cell.tabIndex === 0) ?? cells[0];\n if (active) this.#setActiveCell(active, { focus: false }, cells);\n this.#syncSelectable();\n const pageWrote = this.#takePageRecords();\n this.#normalizeSelection();\n this.#dropOwnRecords(pageWrote);\n this.#mirrorFields(false);\n }\n\n /**\n * The pass the page's changes run: the baselines are rebuilt, then a selection\n * that moved is reported. The report comes last, so rows a subscriber rewrites\n * are the next pass's to settle.\n */\n #reconcileRows(): void {\n this.#restoreBaseline();\n this.#reportMove();\n }\n\n /**\n * Reports the selection as `reconcile` when it differs from the one last\n * settled. The settled selection is replaced before dispatching, so a move a\n * subscriber makes is measured against what it was told.\n */\n #reportMove(): void {\n const selection = this.#selection();\n if (sameSelection(selection, this.#settled)) return;\n this.#settled = selection;\n this.dispatch(\"reconcile\", { detail: { rows: [...selection.keys()] } });\n }\n\n /**\n * Re-renders `aria-multiselectable`, the rows and the fields when application\n * code (or a Turbo morph) changes `selection` at runtime; a selection the new\n * mode collapses is reported as `reconcile`.\n */\n selectionValueChanged(): void {\n this.#reconcile.schedule();\n }\n\n /** Rebuilds the submitted fields when the public name changes at runtime. */\n nameValueChanged(): void {\n this.#reconcile.schedule();\n }\n\n /** Repoints the submitted fields when the owning form changes at runtime. */\n formValueChanged(): void {\n this.#reconcile.schedule();\n }\n\n /** Seeds a fields container inserted after connect from the current selection. */\n fieldsTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Re-establishes the baselines for a row added after connect. */\n rowTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Settles the selection again when a row leaves the grid or drops its target token. */\n rowTargetDisconnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Re-establishes the tab stop when a cell joins the grid after connect. */\n cellTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Re-establishes the tab stop when a cell leaves the grid. */\n cellTargetDisconnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Re-establishes the tab stop when a header joins the grid after connect. */\n columnHeaderTargetConnected(): void {\n this.#reconcile.schedule();\n }\n\n /** Re-establishes the tab stop when a header leaves the grid. */\n columnHeaderTargetDisconnected(): void {\n this.#reconcile.schedule();\n }\n\n /**\n * Brings the authored rows to the shape the APG requires, without changing\n * which rows the author chose.\n *\n * Every selectable row gets an explicit value — an absent `aria-selected` means\n * \"not selectable\" in ARIA, so a forgotten attribute hides a selectable row —\n * and a single-select grid keeps at most one `true`, first in DOM order.\n * A grid that declares `selection=\"none\"` has no selectable rows, so the\n * attribute is removed rather than written: in ARIA its absence is what \"not\n * selectable\" looks like.\n *\n * @stimeoRenderRoot\n */\n #normalizeSelection(): void {\n const rows = this.rowTargets;\n if (this.selectionValue === \"none\") {\n // Reclaim, do not merely skip: a grid that *became* unselectable would\n // otherwise keep announcing rows as selected while the logic refuses to\n // change them. In ARIA the absence of the attribute is what \"not\n // selectable\" looks like, so the rows have to lose it outright.\n for (const row of rows) row.removeAttribute(\"aria-selected\");\n return;\n }\n const single = this.selectionValue === \"single\";\n const first = rows.find((row) => row.getAttribute(\"aria-selected\") === \"true\");\n for (const row of rows) {\n if (single) {\n setAttributeIfChanged(row, \"aria-selected\", row === first ? \"true\" : \"false\");\n } else if (row.getAttribute(\"aria-selected\") !== \"true\") {\n setAttributeIfChanged(row, \"aria-selected\", \"false\");\n }\n }\n }\n\n /**\n * Mirrors `selection=\"multiple\"` onto `aria-multiselectable` (APG Grid) so SRs\n * announce that more than one row can be selected; cleared for single/none so a\n * grid never carries a misleading attribute.\n *\n * @stimeoRenderRoot\n */\n #syncSelectable(): void {\n if (this.selectionValue === \"multiple\") {\n setAttributeIfChanged(this.element, \"aria-multiselectable\", \"true\");\n } else {\n this.element.removeAttribute(\"aria-multiselectable\");\n }\n }\n\n /** Cycles the activated column header's sort and emits `sort`. */\n sort(event: Event): void {\n const header = event.currentTarget as HTMLElement;\n if (!this.columnHeaderTargets.includes(header)) return;\n if (event.defaultPrevented) return;\n // A sortable header hosts a `<button>`, and that button's activation is\n // exactly what this click carries, so it is the one control that does not\n // take the event away. A link or a field inside the header is its own\n // destination, and sorting on its click would act in parallel.\n const control = this.#claimingControl(event, header);\n if (control && !(control instanceof HTMLButtonElement)) return;\n\n const direction = nextSortDirection(header.getAttribute(\"aria-sort\") ?? \"none\");\n\n // Only one column is sorted at a time: reset the others to `none`.\n for (const other of this.columnHeaderTargets) {\n other.setAttribute(\"aria-sort\", other === header ? direction : \"none\");\n }\n\n this.#setActiveCell(header, { focus: false });\n this.dispatch(\"sort\", { detail: { column: header, direction } });\n }\n\n /** Toggles selection of the row owning the event target. Bound optionally. */\n toggleSelect(event: Event): void {\n // The pointer path guards on `selection=\"none\"` exactly as the keyboard path\n // does, so a grid that declares itself unselectable never grows selected rows.\n if (this.selectionValue === \"none\") return;\n // A widget that handled the click owns it, exactly as the keyboard path\n // stands down on a keystroke a descendant consumed.\n if (event.defaultPrevented) return;\n const host = event.currentTarget as HTMLElement;\n if (this.#claimedByDescendant(event, host)) return;\n const row = host.closest<HTMLElement>(\"[role='row']\");\n if (row && this.rowTargets.includes(row)) this.#toggleRow(row);\n }\n\n /** Grid navigation + sort/select activation. Bound to cells and headers. */\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.isComposing) return;\n const cell = event.currentTarget as HTMLElement;\n // A native control inside the cell never calls `preventDefault()` — its\n // activation IS the default action — so the yield above cannot see it. This\n // recognises those by element shape instead.\n if (this.#claimedByDescendant(event, cell)) return;\n const matrix = this.#matrix();\n const position = this.#locate(matrix, cell);\n if (!position) return;\n const [row, col] = position;\n const rowCells = matrix[row] ?? [];\n\n let target: HTMLElement | undefined;\n // Logical, not physical. The key is normalised rather than the\n // delta negated: these two branches are not mirror images — their guards\n // differ — so swapping the key keeps each guard with its own direction.\n switch (logicalArrowKey(event.key, this.element)) {\n case \"ArrowRight\":\n target = this.#cellInRow(matrix, row, col + 1);\n break;\n case \"ArrowLeft\":\n target = this.#cellInRow(matrix, row, Math.max(col - 1, 0));\n break;\n case \"ArrowDown\":\n target = this.#cellInRow(matrix, Math.min(row + 1, matrix.length - 1), col);\n break;\n case \"ArrowUp\":\n target = this.#cellInRow(matrix, Math.max(row - 1, 0), col);\n break;\n case \"Home\":\n target = event.ctrlKey ? this.#cellInRow(matrix, 0, 0) : rowCells[0];\n break;\n case \"End\":\n target = event.ctrlKey\n ? this.#cellInRow(matrix, matrix.length - 1, Number.POSITIVE_INFINITY)\n : rowCells[rowCells.length - 1];\n break;\n case \"Enter\":\n case \" \":\n this.#activate(cell);\n event.preventDefault();\n return;\n default:\n return;\n }\n\n if (target) {\n event.preventDefault();\n this.#setActiveCell(target, { focus: true }, matrix.flat());\n }\n }\n\n /**\n * Whether the event was addressed to a control inside `host` rather than to the\n * grid.\n *\n * Cells and headers hold consumer markup, and APG's grid pattern expects that\n * markup to include working controls — a row action button, an inline editor.\n * Those own their own keystrokes and clicks, so the grid stands down entirely\n * rather than acting in parallel. An editable host (its `contenteditable` state\n * is inherited, so the walk is explicit) counts the same way.\n */\n #claimedByDescendant(event: Event, host: HTMLElement): boolean {\n return this.#claimingControl(event, host) !== null;\n }\n\n /**\n * The nested control this event belongs to, or `null` when the host owns it.\n *\n * Naming the control, rather than answering yes or no, is what lets the click\n * path treat a sortable header's `<button>` as the activation it is while every\n * other control still takes the event away.\n */\n #claimingControl(event: Event, host: HTMLElement): HTMLElement | null {\n const source = event.target as HTMLElement;\n const control = source.closest<HTMLElement>(INTERACTIVE_HOST_SELECTOR);\n if (control && host.contains(control)) return control;\n return isInteractiveHost(source) ? source : null;\n }\n\n /**\n * Performs a header's sort or a cell row's selection toggle on activation.\n *\n * @stimeoRuntimeOnly `selection` decides whether this one activation toggles the row; the sort it\n * cycles does not depend on it.\n */\n #activate(cell: HTMLElement): void {\n if (this.columnHeaderTargets.includes(cell)) {\n this.#cycleSort(cell);\n return;\n }\n if (this.selectionValue === \"none\") return;\n const row = cell.closest<HTMLElement>(\"[role='row']\");\n if (row && this.rowTargets.includes(row)) this.#toggleRow(row);\n }\n\n /** Cycles a header's sort on keyboard activation and emits `sort`. */\n #cycleSort(header: HTMLElement): void {\n const direction = nextSortDirection(header.getAttribute(\"aria-sort\") ?? \"none\");\n for (const other of this.columnHeaderTargets) {\n other.setAttribute(\"aria-sort\", other === header ? direction : \"none\");\n }\n this.dispatch(\"sort\", { detail: { column: header, direction } });\n }\n\n /**\n * Toggles a row's `aria-selected`, honoring single vs. multiple selection.\n *\n * @stimeoRuntimeOnly `selection` decides how this one toggle treats the other rows.\n */\n #toggleRow(row: HTMLElement): void {\n const selected = row.getAttribute(\"aria-selected\") === \"true\";\n const pageWrote = this.#takePageRecords();\n // Enforce single-ness on every toggle, not only when turning a row on:\n // switching one *off* would otherwise leave a second `true` behind until\n // the next baseline pass.\n if (this.selectionValue === \"single\") {\n for (const other of this.rowTargets) {\n if (other !== row) setAttributeIfChanged(other, \"aria-selected\", \"false\");\n }\n }\n row.setAttribute(\"aria-selected\", selected ? \"false\" : \"true\");\n this.#dropOwnRecords(pageWrote);\n\n // Settled before anything is reported, so a listener that moves the rows\n // again is measured against this selection.\n this.#settled = this.#selection();\n this.#mirrorFields(true);\n this.dispatch(\"selectionchange\", { detail: { rows: [...this.#settled.keys()] } });\n }\n\n /** The selected rows in DOM order, each with the `data-value` it submits, if any. */\n #selection(): Selection {\n return new Map(\n this.rowTargets\n .filter((row) => row.getAttribute(\"aria-selected\") === \"true\")\n .map((row) => [row, row.dataset.value] as const),\n );\n }\n\n /**\n * Watches the rows' `aria-selected` and `data-value`. Every write the grid makes\n * to them sits between a take and a drop of the observer's queue, so each record\n * the callback receives is the page's, and only a record on one of the grid's own\n * rows schedules a pass.\n */\n #observeRows(): void {\n const observer = new MutationObserver((records) => {\n if (this.#concernsRows(records)) this.#reconcile.schedule();\n });\n observer.observe(this.element, {\n subtree: true,\n attributes: true,\n attributeFilter: OBSERVED_ATTRIBUTES,\n });\n this.#observer = observer;\n }\n\n /**\n * Empties the observer's queue before the grid writes to the rows, and says\n * whether the page had queued anything about them. Those records still owe a\n * pass: a toggle in `multiple` mode rewrites only its own row, so it does not fold\n * in what the page wrote to the others.\n */\n #takePageRecords(): boolean {\n const records = this.#observer?.takeRecords() ?? [];\n return records.length > 0 && this.#concernsRows(records);\n }\n\n /**\n * Whether any record is about one of this grid's own rows. A widget nested in a\n * cell writes the same attributes on its own elements, and those are no reason\n * to reconcile the grid.\n */\n #concernsRows(records: readonly MutationRecord[]): boolean {\n const rows = new Set<Node>(this.rowTargets);\n return records.some((record) => rows.has(record.target));\n }\n\n /**\n * Drops the records the grid's own writes just queued, so the observer never\n * takes them for the page's, then schedules the pass the page's records owe.\n */\n #dropOwnRecords(pageWrote: boolean): void {\n this.#observer?.takeRecords();\n if (pageWrote) this.#reconcile.schedule();\n }\n\n /**\n * Mirrors the selected rows' `data-value` into the optional fields container.\n * A selected row without one submits nothing, so a grid whose rows carry no\n * value submits an empty set rather than a row of blanks.\n *\n * @stimeoRenderRoot\n */\n #mirrorFields(notify: boolean): void {\n if (!this.hasFieldsTarget) return;\n const values = this.rowTargets\n .filter((row) => row.getAttribute(\"aria-selected\") === \"true\")\n .map((row) => row.dataset.value)\n .filter((value): value is string => value !== undefined);\n const options = { name: this.nameValue, form: this.formValue };\n if (writeFields(this.fieldsTarget, values, options) && notify) {\n commitField(this.fieldsTarget);\n }\n }\n\n /**\n * Makes `cell` the single tabbable cell (roving) and optionally focuses it.\n *\n * `cells` lets a caller that already walked the grid hand its collection over,\n * so one keystroke rebuilds the matrix once instead of twice. The write is\n * skipped where the attribute already holds the wanted value — comparing the\n * attribute rather than the IDL property, because a cell with no `tabindex` at\n * all reports `-1` and would then never receive the attribute it needs to be\n * focusable.\n */\n #setActiveCell(\n cell: HTMLElement,\n { focus }: { focus: boolean },\n cells?: readonly HTMLElement[],\n ): void {\n for (const candidate of cells ?? this.#navigableCells()) {\n const wanted = candidate === cell ? \"0\" : \"-1\";\n if (candidate.getAttribute(\"tabindex\") !== wanted) {\n candidate.setAttribute(\"tabindex\", wanted);\n }\n }\n if (focus) cell.focus();\n }\n\n /** All navigable elements (headers + cells) in DOM order. */\n #navigableCells(): HTMLElement[] {\n return this.#matrix().flat();\n }\n\n /** The grid as rows of navigable cells, derived from each `role=\"row\"`. */\n #matrix(): HTMLElement[][] {\n const navigable = new Set<HTMLElement>([...this.columnHeaderTargets, ...this.cellTargets]);\n const rows = Array.from(this.element.querySelectorAll<HTMLElement>(\"[role='row']\"));\n return rows\n .map((row) =>\n Array.from(row.children).filter((child): child is HTMLElement =>\n navigable.has(child as HTMLElement),\n ),\n )\n .filter((cells) => cells.length > 0);\n }\n\n /** Finds `[rowIndex, colIndex]` of `cell` within `matrix`, or null. */\n #locate(matrix: HTMLElement[][], cell: HTMLElement): [number, number] | null {\n for (let row = 0; row < matrix.length; row++) {\n const col = (matrix[row] ?? []).indexOf(cell);\n if (col !== -1) return [row, col];\n }\n return null;\n }\n\n /** The cell at `[row, col]`, clamping `col` to that row's last cell. */\n #cellInRow(matrix: HTMLElement[][], row: number, col: number): HTMLElement | undefined {\n const cells = matrix[row];\n if (!cells || cells.length === 0) return undefined;\n return cells[Math.min(col, cells.length - 1)];\n }\n}\n"]}