@proyecto-viviana/solidaria 0.2.2 → 0.2.3

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 (210) hide show
  1. package/dist/autocomplete/createAutocomplete.d.ts +2 -2
  2. package/dist/autocomplete/createAutocomplete.d.ts.map +1 -1
  3. package/dist/index.js +233 -234
  4. package/dist/index.js.map +2 -2
  5. package/dist/index.ssr.js +233 -234
  6. package/dist/index.ssr.js.map +2 -2
  7. package/dist/interactions/PressEvent.d.ts +13 -10
  8. package/dist/interactions/PressEvent.d.ts.map +1 -1
  9. package/dist/interactions/createPress.d.ts.map +1 -1
  10. package/dist/interactions/index.d.ts +1 -1
  11. package/dist/interactions/index.d.ts.map +1 -1
  12. package/dist/select/createHiddenSelect.d.ts.map +1 -1
  13. package/dist/toolbar/createToolbar.d.ts.map +1 -1
  14. package/dist/tooltip/createTooltipTrigger.d.ts.map +1 -1
  15. package/package.json +9 -7
  16. package/src/autocomplete/createAutocomplete.ts +341 -0
  17. package/src/autocomplete/index.ts +9 -0
  18. package/src/breadcrumbs/createBreadcrumbs.ts +196 -0
  19. package/src/breadcrumbs/index.ts +8 -0
  20. package/src/button/createButton.ts +142 -0
  21. package/src/button/createToggleButton.ts +101 -0
  22. package/src/button/index.ts +4 -0
  23. package/src/button/types.ts +78 -0
  24. package/src/calendar/createCalendar.ts +138 -0
  25. package/src/calendar/createCalendarCell.ts +187 -0
  26. package/src/calendar/createCalendarGrid.ts +140 -0
  27. package/src/calendar/createRangeCalendar.ts +136 -0
  28. package/src/calendar/createRangeCalendarCell.ts +186 -0
  29. package/src/calendar/index.ts +34 -0
  30. package/src/checkbox/createCheckbox.ts +135 -0
  31. package/src/checkbox/createCheckboxGroup.ts +137 -0
  32. package/src/checkbox/createCheckboxGroupItem.ts +117 -0
  33. package/src/checkbox/createCheckboxGroupState.ts +193 -0
  34. package/src/checkbox/index.ts +13 -0
  35. package/src/color/createColorArea.ts +314 -0
  36. package/src/color/createColorField.ts +137 -0
  37. package/src/color/createColorSlider.ts +197 -0
  38. package/src/color/createColorSwatch.ts +40 -0
  39. package/src/color/createColorWheel.ts +208 -0
  40. package/src/color/index.ts +24 -0
  41. package/src/color/types.ts +116 -0
  42. package/src/combobox/createComboBox.ts +647 -0
  43. package/src/combobox/index.ts +6 -0
  44. package/src/combobox/intl/en-US.json +7 -0
  45. package/src/combobox/intl/es-ES.json +7 -0
  46. package/src/combobox/intl/index.ts +23 -0
  47. package/src/datepicker/createDateField.ts +154 -0
  48. package/src/datepicker/createDatePicker.ts +206 -0
  49. package/src/datepicker/createDateSegment.ts +229 -0
  50. package/src/datepicker/createTimeField.ts +154 -0
  51. package/src/datepicker/index.ts +28 -0
  52. package/src/dialog/createDialog.ts +120 -0
  53. package/src/dialog/index.ts +2 -0
  54. package/src/dialog/types.ts +19 -0
  55. package/src/disclosure/createDisclosure.ts +131 -0
  56. package/src/disclosure/createDisclosureGroup.ts +62 -0
  57. package/src/disclosure/index.ts +11 -0
  58. package/src/dnd/createDrag.ts +209 -0
  59. package/src/dnd/createDraggableCollection.ts +63 -0
  60. package/src/dnd/createDraggableItem.ts +243 -0
  61. package/src/dnd/createDrop.ts +321 -0
  62. package/src/dnd/createDroppableCollection.ts +293 -0
  63. package/src/dnd/createDroppableItem.ts +213 -0
  64. package/src/dnd/index.ts +47 -0
  65. package/src/dnd/types.ts +89 -0
  66. package/src/dnd/utils.ts +294 -0
  67. package/src/focus/FocusScope.tsx +408 -0
  68. package/src/focus/createAutoFocus.ts +321 -0
  69. package/src/focus/createFocusRestore.ts +313 -0
  70. package/src/focus/createVirtualFocus.ts +396 -0
  71. package/src/focus/index.ts +35 -0
  72. package/src/form/createFormReset.ts +51 -0
  73. package/src/form/createFormValidation.ts +224 -0
  74. package/src/form/index.ts +11 -0
  75. package/src/grid/GridKeyboardDelegate.ts +429 -0
  76. package/src/grid/createGrid.ts +261 -0
  77. package/src/grid/createGridCell.ts +182 -0
  78. package/src/grid/createGridRow.ts +153 -0
  79. package/src/grid/index.ts +18 -0
  80. package/src/grid/types.ts +133 -0
  81. package/src/gridlist/createGridList.ts +185 -0
  82. package/src/gridlist/createGridListItem.ts +180 -0
  83. package/src/gridlist/createGridListSelectionCheckbox.ts +59 -0
  84. package/src/gridlist/index.ts +16 -0
  85. package/src/gridlist/types.ts +81 -0
  86. package/src/i18n/NumberFormatter.ts +266 -0
  87. package/src/i18n/createCollator.ts +79 -0
  88. package/src/i18n/createDateFormatter.ts +83 -0
  89. package/src/i18n/createFilter.ts +131 -0
  90. package/src/i18n/createNumberFormatter.ts +52 -0
  91. package/src/i18n/createStringFormatter.ts +87 -0
  92. package/src/i18n/index.ts +40 -0
  93. package/src/i18n/locale.tsx +188 -0
  94. package/src/i18n/utils.ts +99 -0
  95. package/src/index.ts +670 -0
  96. package/src/interactions/FocusableProvider.tsx +44 -0
  97. package/src/interactions/PressEvent.ts +126 -0
  98. package/src/interactions/createFocus.ts +163 -0
  99. package/src/interactions/createFocusRing.ts +89 -0
  100. package/src/interactions/createFocusWithin.ts +206 -0
  101. package/src/interactions/createFocusable.ts +168 -0
  102. package/src/interactions/createHover.ts +254 -0
  103. package/src/interactions/createInteractionModality.ts +424 -0
  104. package/src/interactions/createKeyboard.ts +82 -0
  105. package/src/interactions/createLongPress.ts +174 -0
  106. package/src/interactions/createMove.ts +289 -0
  107. package/src/interactions/createPress.ts +834 -0
  108. package/src/interactions/index.ts +78 -0
  109. package/src/label/createField.ts +145 -0
  110. package/src/label/createLabel.ts +117 -0
  111. package/src/label/createLabels.ts +50 -0
  112. package/src/label/index.ts +19 -0
  113. package/src/landmark/createLandmark.ts +377 -0
  114. package/src/landmark/index.ts +8 -0
  115. package/src/link/createLink.ts +182 -0
  116. package/src/link/index.ts +1 -0
  117. package/src/listbox/createListBox.ts +269 -0
  118. package/src/listbox/createOption.ts +151 -0
  119. package/src/listbox/index.ts +12 -0
  120. package/src/live-announcer/announce.ts +322 -0
  121. package/src/live-announcer/index.ts +9 -0
  122. package/src/menu/createMenu.ts +396 -0
  123. package/src/menu/createMenuItem.ts +149 -0
  124. package/src/menu/createMenuTrigger.ts +88 -0
  125. package/src/menu/index.ts +18 -0
  126. package/src/meter/createMeter.ts +75 -0
  127. package/src/meter/index.ts +1 -0
  128. package/src/numberfield/createNumberField.ts +268 -0
  129. package/src/numberfield/index.ts +5 -0
  130. package/src/overlays/ariaHideOutside.ts +219 -0
  131. package/src/overlays/createInteractOutside.ts +149 -0
  132. package/src/overlays/createModal.tsx +202 -0
  133. package/src/overlays/createOverlay.ts +155 -0
  134. package/src/overlays/createOverlayTrigger.ts +85 -0
  135. package/src/overlays/createPreventScroll.ts +266 -0
  136. package/src/overlays/index.ts +44 -0
  137. package/src/popover/calculatePosition.ts +766 -0
  138. package/src/popover/createOverlayPosition.ts +356 -0
  139. package/src/popover/createPopover.ts +170 -0
  140. package/src/popover/index.ts +24 -0
  141. package/src/progress/createProgressBar.ts +128 -0
  142. package/src/progress/index.ts +5 -0
  143. package/src/radio/createRadio.ts +287 -0
  144. package/src/radio/createRadioGroup.ts +189 -0
  145. package/src/radio/createRadioGroupState.ts +201 -0
  146. package/src/radio/index.ts +23 -0
  147. package/src/searchfield/createSearchField.ts +186 -0
  148. package/src/searchfield/index.ts +2 -0
  149. package/src/select/createHiddenSelect.tsx +236 -0
  150. package/src/select/createSelect.ts +395 -0
  151. package/src/select/index.ts +14 -0
  152. package/src/selection/createTypeSelect.ts +201 -0
  153. package/src/selection/index.ts +6 -0
  154. package/src/separator/createSeparator.ts +82 -0
  155. package/src/separator/index.ts +6 -0
  156. package/src/slider/createSlider.ts +349 -0
  157. package/src/slider/index.ts +2 -0
  158. package/src/ssr/index.tsx +370 -0
  159. package/src/switch/createSwitch.ts +70 -0
  160. package/src/switch/index.ts +1 -0
  161. package/src/table/createTable.ts +526 -0
  162. package/src/table/createTableCell.ts +147 -0
  163. package/src/table/createTableColumnHeader.ts +115 -0
  164. package/src/table/createTableHeaderRow.ts +40 -0
  165. package/src/table/createTableRow.ts +155 -0
  166. package/src/table/createTableRowGroup.ts +32 -0
  167. package/src/table/createTableSelectAllCheckbox.ts +73 -0
  168. package/src/table/createTableSelectionCheckbox.ts +59 -0
  169. package/src/table/index.ts +30 -0
  170. package/src/table/types.ts +165 -0
  171. package/src/tabs/createTabs.ts +472 -0
  172. package/src/tabs/index.ts +14 -0
  173. package/src/tag/createTag.ts +194 -0
  174. package/src/tag/createTagGroup.ts +154 -0
  175. package/src/tag/index.ts +12 -0
  176. package/src/textfield/createTextField.ts +198 -0
  177. package/src/textfield/index.ts +5 -0
  178. package/src/toast/createToast.ts +118 -0
  179. package/src/toast/createToastRegion.ts +100 -0
  180. package/src/toast/index.ts +11 -0
  181. package/src/toggle/createToggle.ts +223 -0
  182. package/src/toggle/createToggleState.ts +94 -0
  183. package/src/toggle/index.ts +7 -0
  184. package/src/toolbar/createToolbar.ts +369 -0
  185. package/src/toolbar/index.ts +6 -0
  186. package/src/tooltip/createTooltip.ts +79 -0
  187. package/src/tooltip/createTooltipTrigger.ts +222 -0
  188. package/src/tooltip/index.ts +6 -0
  189. package/src/tree/createTree.ts +246 -0
  190. package/src/tree/createTreeItem.ts +233 -0
  191. package/src/tree/createTreeSelectionCheckbox.ts +68 -0
  192. package/src/tree/index.ts +16 -0
  193. package/src/tree/types.ts +87 -0
  194. package/src/utils/createDescription.ts +137 -0
  195. package/src/utils/dom.ts +327 -0
  196. package/src/utils/env.ts +54 -0
  197. package/src/utils/events.ts +106 -0
  198. package/src/utils/filterDOMProps.ts +116 -0
  199. package/src/utils/focus.ts +151 -0
  200. package/src/utils/geometry.ts +115 -0
  201. package/src/utils/globalListeners.ts +142 -0
  202. package/src/utils/index.ts +80 -0
  203. package/src/utils/mergeProps.ts +52 -0
  204. package/src/utils/platform.ts +52 -0
  205. package/src/utils/reactivity.ts +36 -0
  206. package/src/utils/textSelection.ts +114 -0
  207. package/src/visually-hidden/createVisuallyHidden.ts +124 -0
  208. package/src/visually-hidden/index.ts +6 -0
  209. package/dist/index.jsx +0 -15845
  210. package/dist/index.jsx.map +0 -7
@@ -0,0 +1,429 @@
1
+ /**
2
+ * GridKeyboardDelegate - Handles keyboard navigation in a grid.
3
+ * Based on @react-aria/grid/GridKeyboardDelegate.
4
+ */
5
+
6
+ import type { GridCollection, GridNode, Key } from '@proyecto-viviana/solid-stately';
7
+ import type { KeyboardDelegate } from './types';
8
+ import type { Accessor } from 'solid-js';
9
+
10
+ export interface GridKeyboardDelegateOptions<T> {
11
+ /** The grid collection. */
12
+ collection: GridCollection<T>;
13
+ /** Set of disabled keys. */
14
+ disabledKeys: Set<Key>;
15
+ /** Ref to the grid element. */
16
+ ref: Accessor<HTMLElement | null>;
17
+ /** Focus mode: row or cell. */
18
+ focusMode: 'row' | 'cell';
19
+ /** Text direction (ltr or rtl). */
20
+ direction: 'ltr' | 'rtl';
21
+ }
22
+
23
+ /**
24
+ * A keyboard delegate that handles navigation in a grid.
25
+ */
26
+ export class GridKeyboardDelegate<T> implements KeyboardDelegate {
27
+ private collection: GridCollection<T>;
28
+ private disabledKeys: Set<Key>;
29
+ private ref: Accessor<HTMLElement | null>;
30
+ private focusMode: 'row' | 'cell';
31
+ private direction: 'ltr' | 'rtl';
32
+
33
+ constructor(options: GridKeyboardDelegateOptions<T>) {
34
+ this.collection = options.collection;
35
+ this.disabledKeys = options.disabledKeys;
36
+ this.ref = options.ref;
37
+ this.focusMode = options.focusMode;
38
+ this.direction = options.direction;
39
+ }
40
+
41
+ /**
42
+ * Check if a key is disabled.
43
+ */
44
+ private isDisabled(key: Key): boolean {
45
+ return this.disabledKeys.has(key);
46
+ }
47
+
48
+ /**
49
+ * Get the parent row key for a cell.
50
+ */
51
+ private getRowKey(key: Key): Key | null {
52
+ const item = this.collection.getItem(key);
53
+ if (!item) return null;
54
+
55
+ if (item.type === 'item') {
56
+ return key;
57
+ }
58
+
59
+ if (item.type === 'cell' && item.parentKey != null) {
60
+ return item.parentKey;
61
+ }
62
+
63
+ return null;
64
+ }
65
+
66
+ /**
67
+ * Get all body rows (excluding header rows).
68
+ */
69
+ private getBodyRows(): GridNode<T>[] {
70
+ return this.collection.rows.filter((row) => row.type === 'item');
71
+ }
72
+
73
+ /**
74
+ * Get the first non-disabled key.
75
+ */
76
+ getFirstKey(fromKey?: Key, global?: boolean): Key | null {
77
+ const rows = this.getBodyRows();
78
+
79
+ if (this.focusMode === 'row' || global) {
80
+ // Find first non-disabled row
81
+ for (const row of rows) {
82
+ if (!this.isDisabled(row.key)) {
83
+ return row.key;
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+
89
+ // Cell focus mode - get first cell of current row or first row
90
+ if (fromKey != null) {
91
+ const rowKey = this.getRowKey(fromKey);
92
+ if (rowKey != null) {
93
+ const children = [...this.collection.getChildren(rowKey)];
94
+ if (children.length > 0) {
95
+ return children[0].key;
96
+ }
97
+ }
98
+ }
99
+
100
+ // Fall back to first cell of first row
101
+ if (rows.length > 0) {
102
+ const children = [...this.collection.getChildren(rows[0].key)];
103
+ if (children.length > 0) {
104
+ return children[0].key;
105
+ }
106
+ }
107
+
108
+ return null;
109
+ }
110
+
111
+ /**
112
+ * Get the last non-disabled key.
113
+ */
114
+ getLastKey(fromKey?: Key, global?: boolean): Key | null {
115
+ const rows = this.getBodyRows();
116
+
117
+ if (this.focusMode === 'row' || global) {
118
+ // Find last non-disabled row
119
+ for (let i = rows.length - 1; i >= 0; i--) {
120
+ if (!this.isDisabled(rows[i].key)) {
121
+ return rows[i].key;
122
+ }
123
+ }
124
+ return null;
125
+ }
126
+
127
+ // Cell focus mode - get last cell of current row or last row
128
+ if (fromKey != null) {
129
+ const rowKey = this.getRowKey(fromKey);
130
+ if (rowKey != null) {
131
+ const children = [...this.collection.getChildren(rowKey)];
132
+ if (children.length > 0) {
133
+ return children[children.length - 1].key;
134
+ }
135
+ }
136
+ }
137
+
138
+ // Fall back to last cell of last row
139
+ if (rows.length > 0) {
140
+ const children = [...this.collection.getChildren(rows[rows.length - 1].key)];
141
+ if (children.length > 0) {
142
+ return children[children.length - 1].key;
143
+ }
144
+ }
145
+
146
+ return null;
147
+ }
148
+
149
+ /**
150
+ * Get the key above the current key.
151
+ */
152
+ getKeyAbove(key: Key): Key | null {
153
+ const item = this.collection.getItem(key);
154
+ if (!item) return null;
155
+
156
+ const rows = this.getBodyRows();
157
+
158
+ if (this.focusMode === 'row' || item.type === 'item') {
159
+ // Find the row and get the previous one
160
+ const rowKey = item.type === 'item' ? key : item.parentKey;
161
+ const rowIndex = rows.findIndex((r) => r.key === rowKey);
162
+
163
+ if (rowIndex > 0) {
164
+ // Find previous non-disabled row
165
+ for (let i = rowIndex - 1; i >= 0; i--) {
166
+ if (!this.isDisabled(rows[i].key)) {
167
+ return rows[i].key;
168
+ }
169
+ }
170
+ }
171
+ return null;
172
+ }
173
+
174
+ // Cell focus mode - get cell in same column of previous row
175
+ if (item.type === 'cell' && item.parentKey != null) {
176
+ const rowIndex = rows.findIndex((r) => r.key === item.parentKey);
177
+ const colIndex = item.column ?? item.index;
178
+
179
+ if (rowIndex > 0) {
180
+ for (let i = rowIndex - 1; i >= 0; i--) {
181
+ if (!this.isDisabled(rows[i].key)) {
182
+ const children = [...this.collection.getChildren(rows[i].key)];
183
+ const targetCol = Math.min(colIndex, children.length - 1);
184
+ if (targetCol >= 0) {
185
+ return children[targetCol].key;
186
+ }
187
+ }
188
+ }
189
+ }
190
+ }
191
+
192
+ return null;
193
+ }
194
+
195
+ /**
196
+ * Get the key below the current key.
197
+ */
198
+ getKeyBelow(key: Key): Key | null {
199
+ const item = this.collection.getItem(key);
200
+ if (!item) return null;
201
+
202
+ const rows = this.getBodyRows();
203
+
204
+ if (this.focusMode === 'row' || item.type === 'item') {
205
+ // Find the row and get the next one
206
+ const rowKey = item.type === 'item' ? key : item.parentKey;
207
+ const rowIndex = rows.findIndex((r) => r.key === rowKey);
208
+
209
+ if (rowIndex >= 0 && rowIndex < rows.length - 1) {
210
+ // Find next non-disabled row
211
+ for (let i = rowIndex + 1; i < rows.length; i++) {
212
+ if (!this.isDisabled(rows[i].key)) {
213
+ return rows[i].key;
214
+ }
215
+ }
216
+ }
217
+ return null;
218
+ }
219
+
220
+ // Cell focus mode - get cell in same column of next row
221
+ if (item.type === 'cell' && item.parentKey != null) {
222
+ const rowIndex = rows.findIndex((r) => r.key === item.parentKey);
223
+ const colIndex = item.column ?? item.index;
224
+
225
+ if (rowIndex >= 0 && rowIndex < rows.length - 1) {
226
+ for (let i = rowIndex + 1; i < rows.length; i++) {
227
+ if (!this.isDisabled(rows[i].key)) {
228
+ const children = [...this.collection.getChildren(rows[i].key)];
229
+ const targetCol = Math.min(colIndex, children.length - 1);
230
+ if (targetCol >= 0) {
231
+ return children[targetCol].key;
232
+ }
233
+ }
234
+ }
235
+ }
236
+ }
237
+
238
+ return null;
239
+ }
240
+
241
+ /**
242
+ * Get the key to the left of the current key.
243
+ */
244
+ getKeyLeftOf(key: Key): Key | null {
245
+ const item = this.collection.getItem(key);
246
+ if (!item) return null;
247
+
248
+ // In row focus mode, left/right might not be meaningful
249
+ if (this.focusMode === 'row') {
250
+ return null;
251
+ }
252
+
253
+ if (item.type === 'cell' && item.parentKey != null) {
254
+ const children = [...this.collection.getChildren(item.parentKey)];
255
+ const colIndex = children.findIndex((c) => c.key === key);
256
+
257
+ if (this.direction === 'rtl') {
258
+ // RTL: left moves to higher index
259
+ if (colIndex < children.length - 1) {
260
+ return children[colIndex + 1].key;
261
+ }
262
+ } else {
263
+ // LTR: left moves to lower index
264
+ if (colIndex > 0) {
265
+ return children[colIndex - 1].key;
266
+ }
267
+ }
268
+ }
269
+
270
+ return null;
271
+ }
272
+
273
+ /**
274
+ * Get the key to the right of the current key.
275
+ */
276
+ getKeyRightOf(key: Key): Key | null {
277
+ const item = this.collection.getItem(key);
278
+ if (!item) return null;
279
+
280
+ // In row focus mode, left/right might not be meaningful
281
+ if (this.focusMode === 'row') {
282
+ return null;
283
+ }
284
+
285
+ if (item.type === 'cell' && item.parentKey != null) {
286
+ const children = [...this.collection.getChildren(item.parentKey)];
287
+ const colIndex = children.findIndex((c) => c.key === key);
288
+
289
+ if (this.direction === 'rtl') {
290
+ // RTL: right moves to lower index
291
+ if (colIndex > 0) {
292
+ return children[colIndex - 1].key;
293
+ }
294
+ } else {
295
+ // LTR: right moves to higher index
296
+ if (colIndex < children.length - 1) {
297
+ return children[colIndex + 1].key;
298
+ }
299
+ }
300
+ }
301
+
302
+ return null;
303
+ }
304
+
305
+ /**
306
+ * Get the key for page up.
307
+ */
308
+ getKeyPageAbove(key: Key): Key | null {
309
+ const el = this.ref();
310
+ if (!el) return null;
311
+
312
+ const item = this.collection.getItem(key);
313
+ if (!item) return null;
314
+
315
+ const rows = this.getBodyRows();
316
+ const rowKey = this.getRowKey(key);
317
+ const rowIndex = rows.findIndex((r) => r.key === rowKey);
318
+
319
+ if (rowIndex < 0) return null;
320
+
321
+ // Calculate how many rows fit in a page (rough estimate)
322
+ const rowHeight = el.scrollHeight / rows.length;
323
+ const pageSize = Math.max(1, Math.floor(el.clientHeight / rowHeight));
324
+
325
+ const targetIndex = Math.max(0, rowIndex - pageSize);
326
+
327
+ // Find first non-disabled row at or before target
328
+ for (let i = targetIndex; i >= 0; i--) {
329
+ if (!this.isDisabled(rows[i].key)) {
330
+ if (this.focusMode === 'row' || item.type === 'item') {
331
+ return rows[i].key;
332
+ }
333
+
334
+ // Cell focus mode - return cell at same column
335
+ const colIndex = item.type === 'cell' ? (item.column ?? item.index) : 0;
336
+ const children = [...this.collection.getChildren(rows[i].key)];
337
+ const targetCol = Math.min(colIndex, children.length - 1);
338
+ if (targetCol >= 0) {
339
+ return children[targetCol].key;
340
+ }
341
+ }
342
+ }
343
+
344
+ return null;
345
+ }
346
+
347
+ /**
348
+ * Get the key for page down.
349
+ */
350
+ getKeyPageBelow(key: Key): Key | null {
351
+ const el = this.ref();
352
+ if (!el) return null;
353
+
354
+ const item = this.collection.getItem(key);
355
+ if (!item) return null;
356
+
357
+ const rows = this.getBodyRows();
358
+ const rowKey = this.getRowKey(key);
359
+ const rowIndex = rows.findIndex((r) => r.key === rowKey);
360
+
361
+ if (rowIndex < 0) return null;
362
+
363
+ // Calculate how many rows fit in a page (rough estimate)
364
+ const rowHeight = el.scrollHeight / rows.length;
365
+ const pageSize = Math.max(1, Math.floor(el.clientHeight / rowHeight));
366
+
367
+ const targetIndex = Math.min(rows.length - 1, rowIndex + pageSize);
368
+
369
+ // Find first non-disabled row at or after target
370
+ for (let i = targetIndex; i < rows.length; i++) {
371
+ if (!this.isDisabled(rows[i].key)) {
372
+ if (this.focusMode === 'row' || item.type === 'item') {
373
+ return rows[i].key;
374
+ }
375
+
376
+ // Cell focus mode - return cell at same column
377
+ const colIndex = item.type === 'cell' ? (item.column ?? item.index) : 0;
378
+ const children = [...this.collection.getChildren(rows[i].key)];
379
+ const targetCol = Math.min(colIndex, children.length - 1);
380
+ if (targetCol >= 0) {
381
+ return children[targetCol].key;
382
+ }
383
+ }
384
+ }
385
+
386
+ return null;
387
+ }
388
+
389
+ /**
390
+ * Get the key that matches the search string.
391
+ */
392
+ getKeyForSearch(search: string, fromKey?: Key): Key | null {
393
+ const searchLower = search.toLowerCase();
394
+ const rows = this.getBodyRows();
395
+
396
+ let startIndex = 0;
397
+ if (fromKey != null) {
398
+ const rowKey = this.getRowKey(fromKey);
399
+ const idx = rows.findIndex((r) => r.key === rowKey);
400
+ if (idx >= 0) {
401
+ startIndex = idx + 1;
402
+ }
403
+ }
404
+
405
+ // Search from startIndex to end
406
+ for (let i = startIndex; i < rows.length; i++) {
407
+ const row = rows[i];
408
+ if (!this.isDisabled(row.key)) {
409
+ const textValue = row.textValue?.toLowerCase() ?? '';
410
+ if (textValue.startsWith(searchLower)) {
411
+ return row.key;
412
+ }
413
+ }
414
+ }
415
+
416
+ // Wrap around and search from beginning
417
+ for (let i = 0; i < startIndex; i++) {
418
+ const row = rows[i];
419
+ if (!this.isDisabled(row.key)) {
420
+ const textValue = row.textValue?.toLowerCase() ?? '';
421
+ if (textValue.startsWith(searchLower)) {
422
+ return row.key;
423
+ }
424
+ }
425
+ }
426
+
427
+ return null;
428
+ }
429
+ }
@@ -0,0 +1,261 @@
1
+ /**
2
+ * createGrid - Provides accessibility for a grid component.
3
+ * Based on @react-aria/grid/useGrid.
4
+ */
5
+
6
+ import { createMemo, createSignal, type Accessor } from 'solid-js';
7
+ import type { JSX } from 'solid-js';
8
+ import { createId } from '@proyecto-viviana/solid-stately';
9
+ import type { GridState, GridCollection, Key } from '@proyecto-viviana/solid-stately';
10
+ import type { GridProps, GridAria, KeyboardDelegate } from './types';
11
+ import { GridKeyboardDelegate } from './GridKeyboardDelegate';
12
+
13
+ // Global map to store grid metadata for child components
14
+ const gridMap = new WeakMap<
15
+ object,
16
+ {
17
+ keyboardDelegate: KeyboardDelegate;
18
+ actions: { onRowAction?: (key: Key) => void; onCellAction?: (key: Key) => void };
19
+ shouldSelectOnPressUp?: boolean;
20
+ }
21
+ >();
22
+
23
+ /**
24
+ * Get the grid metadata for child components.
25
+ */
26
+ export function getGridData<T>(state: GridState<T, GridCollection<T>>) {
27
+ return gridMap.get(state);
28
+ }
29
+
30
+ /**
31
+ * Creates accessibility props for a grid component.
32
+ * A grid displays data in rows and columns and enables navigation via arrow keys.
33
+ */
34
+ export function createGrid<T extends object>(
35
+ props: Accessor<GridProps>,
36
+ state: Accessor<GridState<T, GridCollection<T>>>,
37
+ ref: Accessor<HTMLElement | null>
38
+ ): GridAria {
39
+ const id = createId(props().id);
40
+
41
+ // Track focused state
42
+ const [_isFocused, setIsFocused] = createSignal(false);
43
+
44
+ // Create keyboard delegate
45
+ const keyboardDelegate = createMemo(() => {
46
+ const p = props();
47
+ const s = state();
48
+
49
+ if (p.keyboardDelegate) {
50
+ return p.keyboardDelegate;
51
+ }
52
+
53
+ return new GridKeyboardDelegate({
54
+ collection: s.collection,
55
+ disabledKeys: s.disabledKeys,
56
+ ref,
57
+ focusMode: p.focusMode ?? 'row',
58
+ direction: 'ltr', // TODO: get from locale
59
+ });
60
+ });
61
+
62
+ // Store metadata for child components
63
+ const storeGridData = () => {
64
+ const s = state();
65
+ const p = props();
66
+ gridMap.set(s, {
67
+ keyboardDelegate: keyboardDelegate(),
68
+ actions: {
69
+ onRowAction: p.onRowAction,
70
+ onCellAction: p.onCellAction,
71
+ },
72
+ shouldSelectOnPressUp: p.shouldSelectOnPressUp,
73
+ });
74
+ };
75
+
76
+ // Update grid data whenever state changes
77
+ createMemo(() => {
78
+ storeGridData();
79
+ });
80
+
81
+ // Keyboard navigation handler
82
+ const onKeyDown = (e: KeyboardEvent) => {
83
+ const s = state();
84
+ const p = props();
85
+ const delegate = keyboardDelegate();
86
+
87
+ if (s.isKeyboardNavigationDisabled) {
88
+ return;
89
+ }
90
+
91
+ const focusedKey = s.focusedKey;
92
+ if (focusedKey == null) {
93
+ // If nothing is focused, focus the first item
94
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Home' || e.key === 'End') {
95
+ const firstKey = delegate.getFirstKey?.();
96
+ if (firstKey != null) {
97
+ e.preventDefault();
98
+ s.setFocusedKey(firstKey);
99
+ }
100
+ }
101
+ return;
102
+ }
103
+
104
+ let nextKey: Key | null = null;
105
+
106
+ switch (e.key) {
107
+ case 'ArrowDown':
108
+ nextKey = delegate.getKeyBelow?.(focusedKey) ?? null;
109
+ break;
110
+ case 'ArrowUp':
111
+ nextKey = delegate.getKeyAbove?.(focusedKey) ?? null;
112
+ break;
113
+ case 'ArrowLeft':
114
+ nextKey = delegate.getKeyLeftOf?.(focusedKey) ?? null;
115
+ break;
116
+ case 'ArrowRight':
117
+ nextKey = delegate.getKeyRightOf?.(focusedKey) ?? null;
118
+ break;
119
+ case 'Home':
120
+ if (e.ctrlKey) {
121
+ nextKey = delegate.getFirstKey?.() ?? null;
122
+ } else {
123
+ // Go to first cell in row - for now just use first key
124
+ nextKey = delegate.getFirstKey?.(focusedKey) ?? null;
125
+ }
126
+ break;
127
+ case 'End':
128
+ if (e.ctrlKey) {
129
+ nextKey = delegate.getLastKey?.() ?? null;
130
+ } else {
131
+ // Go to last cell in row - for now just use last key
132
+ nextKey = delegate.getLastKey?.(focusedKey) ?? null;
133
+ }
134
+ break;
135
+ case 'PageDown':
136
+ nextKey = delegate.getKeyPageBelow?.(focusedKey) ?? null;
137
+ break;
138
+ case 'PageUp':
139
+ nextKey = delegate.getKeyPageAbove?.(focusedKey) ?? null;
140
+ break;
141
+ case 'Escape':
142
+ if (p.escapeKeyBehavior !== 'none') {
143
+ s.clearSelection();
144
+ }
145
+ return;
146
+ case 'a':
147
+ if (e.ctrlKey || e.metaKey) {
148
+ e.preventDefault();
149
+ if (s.selectionMode === 'multiple') {
150
+ s.selectAll();
151
+ }
152
+ }
153
+ return;
154
+ case ' ':
155
+ case 'Enter':
156
+ e.preventDefault();
157
+ // Toggle selection or trigger action
158
+ if (s.selectionMode !== 'none') {
159
+ if (e.shiftKey && s.selectionMode === 'multiple') {
160
+ s.extendSelection(focusedKey);
161
+ } else {
162
+ s.toggleSelection(focusedKey);
163
+ }
164
+ }
165
+ return;
166
+ default:
167
+ // Type to select
168
+ if (!p.disallowTypeAhead && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
169
+ const key = delegate.getKeyForSearch?.(e.key, focusedKey);
170
+ if (key != null) {
171
+ e.preventDefault();
172
+ s.setFocusedKey(key);
173
+ }
174
+ }
175
+ return;
176
+ }
177
+
178
+ if (nextKey != null) {
179
+ e.preventDefault();
180
+ s.setFocusedKey(nextKey);
181
+
182
+ // Handle shift+arrow for range selection
183
+ if (e.shiftKey && s.selectionMode === 'multiple') {
184
+ s.extendSelection(nextKey);
185
+ }
186
+ }
187
+ };
188
+
189
+ // Focus handling
190
+ const onFocus = (e: FocusEvent) => {
191
+ const s = state();
192
+ const el = ref();
193
+
194
+ if (!el?.contains(e.target as Element)) {
195
+ return;
196
+ }
197
+
198
+ if (!s.isFocused) {
199
+ s.setFocused(true);
200
+ setIsFocused(true);
201
+
202
+ // If no key is focused, focus the first one
203
+ if (s.focusedKey == null) {
204
+ const firstKey = keyboardDelegate().getFirstKey?.();
205
+ if (firstKey != null) {
206
+ s.setFocusedKey(firstKey);
207
+ }
208
+ }
209
+ }
210
+ };
211
+
212
+ const onBlur = (e: FocusEvent) => {
213
+ const s = state();
214
+ const el = ref();
215
+
216
+ // Only blur if focus is leaving the grid entirely
217
+ if (el && !el.contains(e.relatedTarget as Element)) {
218
+ s.setFocused(false);
219
+ setIsFocused(false);
220
+ }
221
+ };
222
+
223
+ // Warn if no label is provided
224
+ createMemo(() => {
225
+ const p = props();
226
+ if (!p['aria-label'] && !p['aria-labelledby']) {
227
+ console.warn('Grid: An aria-label or aria-labelledby prop is required for accessibility.');
228
+ }
229
+ });
230
+
231
+ const gridProps = createMemo(() => {
232
+ const p = props();
233
+ const s = state();
234
+
235
+ const baseProps: Record<string, unknown> = {
236
+ role: 'grid',
237
+ id,
238
+ 'aria-label': p['aria-label'],
239
+ 'aria-labelledby': p['aria-labelledby'],
240
+ 'aria-describedby': p['aria-describedby'],
241
+ 'aria-multiselectable': s.selectionMode === 'multiple' ? 'true' : undefined,
242
+ tabIndex: s.collection.size === 0 ? 0 : -1,
243
+ onKeyDown,
244
+ onFocus,
245
+ onBlur,
246
+ };
247
+
248
+ if (p.isVirtualized) {
249
+ baseProps['aria-rowcount'] = s.collection.rowCount;
250
+ baseProps['aria-colcount'] = s.collection.columnCount;
251
+ }
252
+
253
+ return baseProps as JSX.HTMLAttributes<HTMLElement>;
254
+ });
255
+
256
+ return {
257
+ get gridProps() {
258
+ return gridProps();
259
+ },
260
+ };
261
+ }