@zeyos/zx 4.4.0 → 5.2.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 (252) hide show
  1. package/CHANGELOG.md +447 -0
  2. package/README.md +44 -11
  3. package/dist/types/components/account-menu/account-menu.d.ts +8 -0
  4. package/dist/types/components/activity-list/activity-list.d.ts +31 -1
  5. package/dist/types/components/app-icon/app-icon.d.ts +14 -2
  6. package/dist/types/components/app-sidebar/app-sidebar.d.ts +293 -17
  7. package/dist/types/components/avatar/avatar.d.ts +46 -7
  8. package/dist/types/components/badge/badge.d.ts +21 -1
  9. package/dist/types/components/button/button.d.ts +15 -0
  10. package/dist/types/components/calendar/calendar.d.ts +67 -5
  11. package/dist/types/components/card-view/card-view.d.ts +7 -4
  12. package/dist/types/components/card-view/record-card.d.ts +11 -0
  13. package/dist/types/components/chart/chart.d.ts +10 -0
  14. package/dist/types/components/checklist/checklist.d.ts +9 -0
  15. package/dist/types/components/code-editor/code-editor.d.ts +6 -17
  16. package/dist/types/components/context-menu/context-menu.d.ts +6 -2
  17. package/dist/types/components/data-filter/data-filter.d.ts +4 -4
  18. package/dist/types/components/datebox/date-range-box.d.ts +5 -1
  19. package/dist/types/components/datebox/datebox.d.ts +7 -2
  20. package/dist/types/components/dialog/dialog.d.ts +46 -6
  21. package/dist/types/components/dropdown/dropdown.d.ts +24 -9
  22. package/dist/types/components/entity-ref/entity-ref.d.ts +2 -2
  23. package/dist/types/components/field/field.d.ts +54 -2
  24. package/dist/types/components/fieldset/external-adapter.d.ts +83 -0
  25. package/dist/types/components/fieldset/fieldset.d.ts +26 -1
  26. package/dist/types/components/file-item/file-item.d.ts +34 -6
  27. package/dist/types/components/file-list/file-list.d.ts +35 -3
  28. package/dist/types/components/filter/filter.d.ts +127 -3
  29. package/dist/types/components/filter-panel/filter-panel.d.ts +36 -7
  30. package/dist/types/components/form/form.d.ts +12 -1
  31. package/dist/types/components/kanban-view/kanban-card.d.ts +8 -0
  32. package/dist/types/components/launcher/launcher.d.ts +268 -15
  33. package/dist/types/components/list-toolbar/list-toolbar.d.ts +16 -7
  34. package/dist/types/components/menu-button/menu-button.d.ts +39 -5
  35. package/dist/types/components/message/message.d.ts +132 -17
  36. package/dist/types/components/modal/modal.d.ts +43 -2
  37. package/dist/types/components/module-bar/module-bar.d.ts +395 -0
  38. package/dist/types/components/pagination/pagination.d.ts +17 -3
  39. package/dist/types/components/select/color.d.ts +12 -0
  40. package/dist/types/components/select/glyphs.d.ts +124 -0
  41. package/dist/types/components/select/select.d.ts +17 -3
  42. package/dist/types/components/sheet/sheet.d.ts +15 -0
  43. package/dist/types/components/stat-tile/stat-tile.d.ts +16 -5
  44. package/dist/types/components/tabbox/tabbox.d.ts +9 -0
  45. package/dist/types/components/table/table.d.ts +311 -18
  46. package/dist/types/components/tag-picker/tag-picker.d.ts +21 -4
  47. package/dist/types/components/tree/tree.d.ts +13 -0
  48. package/dist/types/components/view/record-view.d.ts +59 -2
  49. package/dist/types/components/view-settings/view-settings.d.ts +879 -0
  50. package/dist/types/components/workspace-switcher/workspace-switcher.d.ts +327 -0
  51. package/dist/types/core/breakpoint.d.ts +1 -1
  52. package/dist/types/core/component.d.ts +12 -2
  53. package/dist/types/core/i18n.d.ts +27 -3
  54. package/dist/types/core/icons.d.ts +23 -25
  55. package/dist/types/core/keyboard.d.ts +98 -1
  56. package/dist/types/core/messages.d.ts +382 -0
  57. package/dist/types/core/motion.d.ts +59 -0
  58. package/dist/types/core/overlay-host.d.ts +42 -1
  59. package/dist/types/core/overlay-stack.d.ts +260 -0
  60. package/dist/types/index.d.ts +10 -5
  61. package/dist/types/internal/app-rail.d.ts +38 -0
  62. package/dist/types/internal/menu-items.d.ts +71 -3
  63. package/dist/types/zeyos/connect.d.ts +6 -0
  64. package/dist/types/zeyos/form.d.ts +50 -9
  65. package/dist/types/zeyos/index.d.ts +2 -1
  66. package/dist/types/zeyos/launcher.d.ts +9 -0
  67. package/dist/types/zeyos/module-bar.d.ts +176 -0
  68. package/dist/types/zeyos/query.d.ts +110 -5
  69. package/dist/types/zeyos/schema.d.ts +24 -1
  70. package/dist/types/zeyos/table.d.ts +345 -22
  71. package/dist/types/zeyos/view.d.ts +26 -2
  72. package/dist/zx-zeyos.esm.js +6178 -1008
  73. package/dist/zx-zeyos.esm.min.js +53 -5
  74. package/dist/zx.css +2861 -827
  75. package/dist/zx.esm.js +8909 -1480
  76. package/dist/zx.esm.min.js +55 -7
  77. package/dist/zx.global.js +8911 -1482
  78. package/dist/zx.global.min.js +55 -7
  79. package/dist/zx.min.css +1 -1
  80. package/docs/api.json +1079 -117
  81. package/docs/llms.md +956 -154
  82. package/docs/llms.txt +9 -5
  83. package/package.json +1 -1
  84. package/src/components/account-menu/account-menu.css +35 -10
  85. package/src/components/account-menu/account-menu.js +12 -0
  86. package/src/components/activity-item/activity-item.css +1 -1
  87. package/src/components/activity-item/activity-item.js +3 -0
  88. package/src/components/activity-list/activity-list.css +38 -3
  89. package/src/components/activity-list/activity-list.js +61 -1
  90. package/src/components/app-icon/app-icon.css +14 -3
  91. package/src/components/app-icon/app-icon.js +11 -1
  92. package/src/components/app-rail/app-rail.css +59 -79
  93. package/src/components/app-sidebar/app-sidebar.css +304 -168
  94. package/src/components/app-sidebar/app-sidebar.js +667 -46
  95. package/src/components/aurora/aurora.css +5 -3
  96. package/src/components/avatar/avatar.css +32 -7
  97. package/src/components/avatar/avatar.js +72 -8
  98. package/src/components/badge/badge.css +54 -2
  99. package/src/components/badge/badge.js +29 -2
  100. package/src/components/breadcrumb/breadcrumb.css +1 -1
  101. package/src/components/button/button.css +67 -9
  102. package/src/components/button/button.js +38 -0
  103. package/src/components/calendar/calendar.css +47 -47
  104. package/src/components/calendar/calendar.js +26 -52
  105. package/src/components/card/card.css +4 -4
  106. package/src/components/card-view/card-view.css +14 -12
  107. package/src/components/card-view/card-view.js +13 -9
  108. package/src/components/card-view/record-card.js +14 -3
  109. package/src/components/chart/chart.css +4 -1
  110. package/src/components/chart/chart.js +12 -0
  111. package/src/components/check-button/check-button.css +10 -2
  112. package/src/components/checklist/checklist.css +9 -2
  113. package/src/components/checklist/checklist.js +11 -5
  114. package/src/components/checklist/field-adapter.js +3 -1
  115. package/src/components/code-editor/code-editor.css +7 -6
  116. package/src/components/code-editor/code-editor.js +19 -1
  117. package/src/components/code-editor/field-adapter.js +9 -2
  118. package/src/components/context-menu/context-menu.css +9 -4
  119. package/src/components/context-menu/context-menu.js +8 -2
  120. package/src/components/copy/copy-button.css +6 -0
  121. package/src/components/copy/copy-input.css +9 -4
  122. package/src/components/data-filter/data-filter.css +5 -4
  123. package/src/components/data-filter/data-filter.js +2 -2
  124. package/src/components/date-picker/date-picker.css +6 -6
  125. package/src/components/date-picker/date-range-picker.css +3 -3
  126. package/src/components/date-picker/time-picker.css +3 -3
  127. package/src/components/datebox/date-range-box.css +79 -2
  128. package/src/components/datebox/date-range-box.js +13 -2
  129. package/src/components/datebox/datebox.css +79 -2
  130. package/src/components/datebox/datebox.js +13 -2
  131. package/src/components/dialog/dialog.css +26 -0
  132. package/src/components/dialog/dialog.js +99 -17
  133. package/src/components/dock/dock.css +5 -4
  134. package/src/components/dropdown/dropdown.css +1 -1
  135. package/src/components/dropdown/dropdown.js +159 -72
  136. package/src/components/empty-state/empty-state.css +2 -2
  137. package/src/components/entity-ref/entity-ref.css +1 -1
  138. package/src/components/entity-ref/entity-ref.js +1 -1
  139. package/src/components/field/field.css +28 -9
  140. package/src/components/field/field.js +101 -29
  141. package/src/components/field-upload/field-upload.css +1 -1
  142. package/src/components/fieldset/external-adapter.js +363 -0
  143. package/src/components/fieldset/fieldset.css +5 -3
  144. package/src/components/fieldset/fieldset.js +38 -64
  145. package/src/components/file-item/file-item.js +144 -10
  146. package/src/components/file-list/file-list.js +144 -21
  147. package/src/components/filter/filter.css +86 -2
  148. package/src/components/filter/filter.js +307 -11
  149. package/src/components/filter-panel/filter-panel.css +1 -1
  150. package/src/components/filter-panel/filter-panel.js +149 -32
  151. package/src/components/finder/finder.css +16 -14
  152. package/src/components/form/form.css +5 -4
  153. package/src/components/form/form.js +11 -2
  154. package/src/components/grid/grid.css +1 -1
  155. package/src/components/groupbox/groupbox.css +1 -1
  156. package/src/components/kanban-view/kanban-card.js +11 -3
  157. package/src/components/kanban-view/kanban-view.css +42 -18
  158. package/src/components/kanban-view/kanban-view.js +9 -6
  159. package/src/components/launcher/launcher.css +108 -26
  160. package/src/components/launcher/launcher.js +530 -42
  161. package/src/components/list-toolbar/list-toolbar.css +13 -8
  162. package/src/components/list-toolbar/list-toolbar.js +40 -14
  163. package/src/components/master-panel/master-panel.css +2 -0
  164. package/src/components/master-panel/master-panel.js +0 -1
  165. package/src/components/menu-button/menu-button.css +72 -4
  166. package/src/components/menu-button/menu-button.js +160 -18
  167. package/src/components/message/message.css +33 -3
  168. package/src/components/message/message.js +264 -22
  169. package/src/components/modal/modal.css +1 -1
  170. package/src/components/modal/modal.js +83 -15
  171. package/src/components/module-bar/module-bar.css +486 -0
  172. package/src/components/module-bar/module-bar.js +1074 -0
  173. package/src/components/multi-value-editor/multi-value-editor.css +1 -1
  174. package/src/components/navigation-bar/navigation-bar.css +18 -9
  175. package/src/components/number-field/field-adapter.js +3 -1
  176. package/src/components/number-field/number-field.css +70 -0
  177. package/src/components/number-field/number-field.js +2 -0
  178. package/src/components/pagination/pagination.css +13 -11
  179. package/src/components/pagination/pagination.js +20 -4
  180. package/src/components/panel/panel.css +4 -3
  181. package/src/components/questionnaire/questionnaire.css +4 -4
  182. package/src/components/rating/field-adapter.js +2 -1
  183. package/src/components/rating/rating.css +8 -1
  184. package/src/components/search/search.css +4 -1
  185. package/src/components/select/color.js +22 -0
  186. package/src/components/select/field-adapter.js +2 -0
  187. package/src/components/select/glyphs.js +165 -0
  188. package/src/components/select/select.css +138 -45
  189. package/src/components/select/select.js +92 -47
  190. package/src/components/sheet/sheet.css +12 -9
  191. package/src/components/sheet/sheet.js +74 -32
  192. package/src/components/slider/field-adapter.js +6 -1
  193. package/src/components/slider/slider.css +18 -4
  194. package/src/components/slider/slider.js +1 -1
  195. package/src/components/sort-control/sort-control.css +4 -1
  196. package/src/components/split-view/split-view.css +1 -1
  197. package/src/components/stat-tile/stat-tile.css +42 -47
  198. package/src/components/stat-tile/stat-tile.js +17 -6
  199. package/src/components/stepper/stepper.css +1 -1
  200. package/src/components/tabbox/tabbox.css +41 -32
  201. package/src/components/tabbox/tabbox.js +5 -1
  202. package/src/components/table/table.css +187 -37
  203. package/src/components/table/table.js +718 -73
  204. package/src/components/tag-picker/field-adapter.js +2 -1
  205. package/src/components/tag-picker/tag-picker.css +83 -5
  206. package/src/components/tag-picker/tag-picker.js +25 -8
  207. package/src/components/timebox/timebox.css +22 -2
  208. package/src/components/toggle/toggle.css +12 -1
  209. package/src/components/toolbar/toolbar.js +2 -2
  210. package/src/components/tooltip/tooltip.css +4 -0
  211. package/src/components/tooltip/tooltip.js +44 -19
  212. package/src/components/tree/tree.css +26 -17
  213. package/src/components/tree/tree.js +31 -0
  214. package/src/components/value-list/value-list.css +3 -3
  215. package/src/components/view/record-view.css +1 -1
  216. package/src/components/view/record-view.js +178 -18
  217. package/src/components/view-settings/view-settings.css +285 -0
  218. package/src/components/view-settings/view-settings.js +1830 -0
  219. package/src/components/workspace-switcher/workspace-switcher.css +188 -0
  220. package/src/components/workspace-switcher/workspace-switcher.js +463 -0
  221. package/src/core/breakpoint.js +1 -1
  222. package/src/core/component.js +88 -17
  223. package/src/core/fontawesome.js +5 -1
  224. package/src/core/i18n.js +47 -4
  225. package/src/core/icons.js +204 -61
  226. package/src/core/keyboard.js +206 -11
  227. package/src/core/messages.js +427 -0
  228. package/src/core/motion.js +147 -0
  229. package/src/core/overlay-host.js +77 -3
  230. package/src/core/overlay-stack.js +487 -0
  231. package/src/core/position.js +22 -3
  232. package/src/index.js +10 -5
  233. package/src/internal/app-rail.js +135 -55
  234. package/src/internal/menu-items.js +61 -3
  235. package/src/zeyos/connect.js +6 -2
  236. package/src/zeyos/form.js +69 -27
  237. package/src/zeyos/index.js +4 -1
  238. package/src/zeyos/launcher.js +8 -3
  239. package/src/zeyos/module-bar.js +181 -0
  240. package/src/zeyos/query.js +197 -13
  241. package/src/zeyos/schema.js +144 -27
  242. package/src/zeyos/table.js +287 -84
  243. package/src/zeyos/view.js +49 -93
  244. package/styles/base.css +72 -1
  245. package/styles/icons.css +11 -1
  246. package/styles/print.css +1 -1
  247. package/styles/tokens/dark.css +117 -37
  248. package/styles/tokens/global.css +121 -27
  249. package/styles/tokens/semantic.css +140 -43
  250. package/styles/tokens/themes.css +7 -6
  251. package/styles/zx.css +3 -1
  252. package/styles/tokens/density.css +0 -9
@@ -5,10 +5,18 @@ const FOCUSABLE_SELECTOR = [
5
5
  '[contenteditable="true"]', '[tabindex]:not([tabindex="-1"])'
6
6
  ].join(',');
7
7
 
8
+ /** Attributes that change whether a roving item is available; nothing else is observed. */
9
+ const AVAILABILITY_ATTRIBUTES = ['hidden', 'disabled', 'aria-disabled', 'aria-hidden'];
10
+ /** `<input>` types without a caret, which a roving group may move focus away from freely. */
11
+ const CARETLESS_INPUT_TYPES = new Set(['button', 'checkbox', 'color', 'file', 'image', 'reset', 'submit']);
12
+
8
13
  /**
9
14
  * @typedef {Object} RovingOptions
10
15
  * @property {'vertical'|'horizontal'} [orientation='vertical'] Arrow-key axis.
11
16
  * @property {boolean} [wrap=true] Whether navigation wraps at either end.
17
+ * @property {'auto'|boolean} [rtl='auto'] Whether ArrowLeft and ArrowRight swap meaning in a
18
+ * horizontal group. `'auto'` reads the container's computed `direction` on every key press, so
19
+ * a group inside `dir="rtl"` moves to the visually next item; `true`/`false` force it.
12
20
  */
13
21
  /**
14
22
  * @typedef {Object} FocusTrapController
@@ -73,12 +81,23 @@ export function focusTrap(container) {
73
81
 
74
82
  /**
75
83
  * Applies the ARIA roving-tabindex keyboard pattern to matching descendants.
84
+ *
85
+ * Editable items keep the keys they need, as the APG toolbar pattern asks: Home and End always
86
+ * stay with a text field, select, or contenteditable region, and so do the arrow keys — except
87
+ * that a text field whose caret already sits at the edge the arrow points past hands focus on to
88
+ * the neighbouring item, so a reader is never trapped in it. Under `dir="rtl"` the horizontal
89
+ * arrows follow the visual order.
90
+ *
91
+ * The group re-reads its items when children are added or removed and when an availability
92
+ * attribute (`hidden`, `disabled`, `aria-disabled`, `aria-hidden`) changes on an item or on an
93
+ * element inside the container that holds items — a `<fieldset disabled>` toggled around them; its
94
+ * own `tabindex` writes and unrelated attribute churn are not observed.
76
95
  * @param {Element} container Event and query container.
77
96
  * @param {string} itemSelector Selector for roving items.
78
97
  * @param {RovingOptions} [options={}] Behavior options.
79
98
  * @returns {RovingController}
80
99
  */
81
- export function rovingTabindex(container, itemSelector, { orientation = 'vertical', wrap = true } = {}) {
100
+ export function rovingTabindex(container, itemSelector, { orientation = 'vertical', wrap = true, rtl = 'auto' } = {}) {
82
101
  const controller = new AbortController();
83
102
  const originals = new Map();
84
103
  let destroyed = false;
@@ -117,14 +136,13 @@ export function rovingTabindex(container, itemSelector, { orientation = 'vertica
117
136
  const index = available.indexOf(current);
118
137
  if (index < 0) return;
119
138
 
120
- const previousKey = orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp';
121
- const nextKey = orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown';
122
- let nextIndex = null;
123
- if (event.key === 'Home') nextIndex = 0;
124
- else if (event.key === 'End') nextIndex = available.length - 1;
125
- else if (event.key === previousKey) nextIndex = index - 1;
126
- else if (event.key === nextKey) nextIndex = index + 1;
127
- if (nextIndex === null || available.length === 0) return;
139
+ const reversed = orientation === 'horizontal' && isRightToLeft(container, rtl);
140
+ const step = rovingStep(event.key, orientation, reversed);
141
+ if (step === null) return;
142
+ if (!editableAllowsRoving(/** @type {Element|null} */ (event.target), step)) return;
143
+ let nextIndex = step === 'first' ? 0 : step === 'last' ? available.length - 1
144
+ : step === 'previous' ? index - 1 : index + 1;
145
+ if (available.length === 0) return;
128
146
 
129
147
  if (wrap) nextIndex = (nextIndex + available.length) % available.length;
130
148
  else nextIndex = Math.max(0, Math.min(available.length - 1, nextIndex));
@@ -143,10 +161,38 @@ export function rovingTabindex(container, itemSelector, { orientation = 'vertica
143
161
 
144
162
  container.addEventListener('keydown', onKeydown, { signal: controller.signal });
145
163
  container.addEventListener('focusin', onFocus, { signal: controller.signal });
146
- const observer = typeof MutationObserver === 'function' ? new MutationObserver(() => update()) : null;
147
- observer?.observe(container, { childList: true, subtree: true, attributes: true });
164
+ const observer = typeof MutationObserver === 'function'
165
+ ? new MutationObserver((records) => { if (records.some(concernsItems)) update(); })
166
+ : null;
167
+ observer?.observe(container, {
168
+ childList: true, subtree: true, attributes: true, attributeFilter: AVAILABILITY_ATTRIBUTES
169
+ });
148
170
  update();
149
171
 
172
+ /**
173
+ * Whether a mutation can change the item set: an item's availability attribute, or an added or
174
+ * removed subtree that is, holds, or held an item. Content churn inside an item is ignored.
175
+ * @param {MutationRecord} record Observed mutation.
176
+ * @returns {boolean}
177
+ */
178
+ function concernsItems(record) {
179
+ if (record.type === 'attributes') {
180
+ // An item's own attribute, or one on an element that holds items: a `<fieldset disabled>`
181
+ // disables every control inside it without touching their attributes.
182
+ return record.target instanceof Element
183
+ && (record.target.matches(itemSelector) || record.target.querySelector(itemSelector) !== null);
184
+ }
185
+ for (const node of record.addedNodes) {
186
+ if (node instanceof Element && (node.matches(itemSelector) || node.querySelector(itemSelector))) return true;
187
+ }
188
+ for (const node of record.removedNodes) {
189
+ if (!(node instanceof Element)) continue;
190
+ if (originals.has(/** @type {HTMLElement} */ (node))) return true;
191
+ for (const item of originals.keys()) if (node.contains(item)) return true;
192
+ }
193
+ return false;
194
+ }
195
+
150
196
  return {
151
197
  /** Focuses the first available item. @returns {void} */
152
198
  focusFirst() {
@@ -182,6 +228,77 @@ export function rovingTabindex(container, itemSelector, { orientation = 'vertica
182
228
  };
183
229
  }
184
230
 
231
+ /**
232
+ * Where focus sat inside one keyed item of a collection, recorded before the collection re-renders.
233
+ * @typedef {Object} ItemFocusMemo
234
+ * @property {unknown} key The item's key.
235
+ * @property {boolean} self Whether the item element itself held focus.
236
+ * @property {number} index Position of the focused control among the item's focusable controls.
237
+ * @property {string} signature Tag, data attributes, and name of the focused control, which
238
+ * identify its replacement after a rebuild.
239
+ */
240
+
241
+ /**
242
+ * Records which control of which keyed item holds focus, so a collection that rebuilds an item
243
+ * can put focus back on the equivalent control afterwards. Returns null when focus is elsewhere.
244
+ * @param {Element} root Collection root.
245
+ * @param {(element: Element) => ({key: unknown, element: Element}|null)} locate Finds the keyed
246
+ * item an element belongs to.
247
+ * @returns {ItemFocusMemo|null}
248
+ */
249
+ export function captureItemFocus(root, locate) {
250
+ const active = root?.ownerDocument?.activeElement ?? null;
251
+ if (!active || active === root || !root.contains(active)) return null;
252
+ const item = locate(active);
253
+ if (!item) return null;
254
+ const controls = itemControls(item.element);
255
+ return {
256
+ key: item.key,
257
+ self: active === item.element,
258
+ index: controls.indexOf(/** @type {HTMLElement} */ (active)),
259
+ signature: controlSignature(active)
260
+ };
261
+ }
262
+
263
+ /**
264
+ * Puts focus back on the control a {@link captureItemFocus} memo describes, unless focus is still
265
+ * on a live element inside the collection (nothing was lost). The replacement is the control with
266
+ * the same signature, else the one at the same position, else the item's last control, else the
267
+ * item itself when it is focusable. Scrolling is suppressed: the reader did not move.
268
+ * @param {Element} root Collection root.
269
+ * @param {ItemFocusMemo|null} memo Recorded focus.
270
+ * @param {(key: unknown) => Element|null} resolve Finds the item element for a key after the update.
271
+ * @returns {boolean} Whether focus was moved.
272
+ */
273
+ export function restoreItemFocus(root, memo, resolve) {
274
+ if (!memo || !root) return false;
275
+ const active = root.ownerDocument?.activeElement ?? null;
276
+ if (active && active !== root.ownerDocument?.body && active.isConnected && root.contains(active)) return false;
277
+ const element = resolve(memo.key);
278
+ if (!element) return false;
279
+ const controls = itemControls(element);
280
+ const target = memo.self ? /** @type {HTMLElement} */ (element)
281
+ : controls.find((control) => controlSignature(control) === memo.signature)
282
+ ?? controls[memo.index] ?? controls.at(-1) ?? null;
283
+ if (!target || typeof target.focus !== 'function') return false;
284
+ target.focus({ preventScroll: true });
285
+ return root.ownerDocument?.activeElement === target;
286
+ }
287
+
288
+ /** @param {Element} element @returns {HTMLElement[]} Focusable, enabled controls inside an item. */
289
+ function itemControls(element) {
290
+ return /** @type {HTMLElement[]} */ (Array.from(element.querySelectorAll(`${FOCUSABLE_SELECTOR},[tabindex="-1"]`)))
291
+ .filter((control) => isAvailable(control));
292
+ }
293
+
294
+ /** @param {Element} element @returns {string} A rebuild-stable description of a control. */
295
+ function controlSignature(element) {
296
+ const data = Object.entries(/** @type {HTMLElement} */ (element).dataset ?? {})
297
+ .map(([name, value]) => `${name}=${value}`).sort().join('&');
298
+ const name = element.getAttribute('aria-label') ?? element.textContent?.trim() ?? '';
299
+ return `${element.tagName}|${data}|${name}`;
300
+ }
301
+
185
302
  /**
186
303
  * Creates a 500 ms buffered, case-insensitive first-letter typeahead handler.
187
304
  * @template T
@@ -212,6 +329,84 @@ export function typeahead(getItems, onMatch) {
212
329
  };
213
330
  }
214
331
 
332
+ /**
333
+ * Maps a key to a roving step, or null for a key the group does not handle.
334
+ * @param {string} key `KeyboardEvent.key`.
335
+ * @param {'vertical'|'horizontal'} orientation Arrow-key axis.
336
+ * @param {boolean} reversed Whether the horizontal arrows are swapped (right-to-left).
337
+ * @returns {'first'|'last'|'previous'|'next'|null}
338
+ */
339
+ export function rovingStep(key, orientation, reversed = false) {
340
+ if (key === 'Home') return 'first';
341
+ if (key === 'End') return 'last';
342
+ if (orientation === 'horizontal') {
343
+ if (key === 'ArrowLeft') return reversed ? 'next' : 'previous';
344
+ if (key === 'ArrowRight') return reversed ? 'previous' : 'next';
345
+ return null;
346
+ }
347
+ if (key === 'ArrowUp') return 'previous';
348
+ if (key === 'ArrowDown') return 'next';
349
+ return null;
350
+ }
351
+
352
+ /**
353
+ * Whether an element takes keyboard text input, so that arrow keys and Home/End move its caret
354
+ * or its value: a text-like `<input>`, `<textarea>`, `<select>`, or a contenteditable region.
355
+ * @param {Element|null} element Candidate, normally a key event's target.
356
+ * @returns {boolean}
357
+ */
358
+ export function isEditableTarget(element) {
359
+ if (!element || typeof element !== 'object') return false;
360
+ const tag = String(element.tagName ?? '').toUpperCase();
361
+ if (tag === 'TEXTAREA' || tag === 'SELECT') return true;
362
+ if (tag === 'INPUT') {
363
+ return !CARETLESS_INPUT_TYPES.has(String(/** @type {HTMLInputElement} */ (element).type ?? 'text').toLowerCase());
364
+ }
365
+ return Boolean(/** @type {HTMLElement} */ (element).isContentEditable);
366
+ }
367
+
368
+ /**
369
+ * Whether a roving step may leave the event target. Non-editable targets always may. An editable
370
+ * one keeps Home/End, and keeps the arrows unless it is a single-line text field whose collapsed
371
+ * caret already sits at the edge the step points past: position 0 for `previous`, the end of the
372
+ * value for `next`. The steps are logical, so under right-to-left the caret check still matches
373
+ * the direction the caret would have moved.
374
+ * @param {Element|null} target Key event target.
375
+ * @param {'first'|'last'|'previous'|'next'} step Roving step.
376
+ * @returns {boolean}
377
+ */
378
+ export function editableAllowsRoving(target, step) {
379
+ if (!isEditableTarget(target)) return true;
380
+ if (step === 'first' || step === 'last') return false;
381
+ if (String(target?.tagName ?? '').toUpperCase() !== 'INPUT') return false;
382
+ const input = /** @type {HTMLInputElement} */ (target);
383
+ let start;
384
+ let end;
385
+ try {
386
+ start = input.selectionStart;
387
+ end = input.selectionEnd;
388
+ } catch {
389
+ return false;
390
+ }
391
+ // Types without a selection API (number, date, range …) own their arrows outright.
392
+ if (typeof start !== 'number' || typeof end !== 'number' || start !== end) return false;
393
+ return step === 'previous' ? start === 0 : end === String(input.value ?? '').length;
394
+ }
395
+
396
+ /**
397
+ * @param {Element} container Group container.
398
+ * @param {'auto'|boolean} rtl Configured direction handling.
399
+ * @returns {boolean}
400
+ */
401
+ function isRightToLeft(container, rtl) {
402
+ if (rtl === true || rtl === false) return rtl;
403
+ try {
404
+ return globalThis.getComputedStyle?.(container)?.direction === 'rtl';
405
+ } catch {
406
+ return false;
407
+ }
408
+ }
409
+
215
410
  /** @param {Element} container @returns {HTMLElement[]} */
216
411
  function focusableItems(container) {
217
412
  return /** @type {HTMLElement[]} */ (Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR)))