@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.
- package/dist/autocomplete/createAutocomplete.d.ts +2 -2
- package/dist/autocomplete/createAutocomplete.d.ts.map +1 -1
- package/dist/index.js +233 -234
- package/dist/index.js.map +2 -2
- package/dist/index.ssr.js +233 -234
- package/dist/index.ssr.js.map +2 -2
- package/dist/interactions/PressEvent.d.ts +13 -10
- package/dist/interactions/PressEvent.d.ts.map +1 -1
- package/dist/interactions/createPress.d.ts.map +1 -1
- package/dist/interactions/index.d.ts +1 -1
- package/dist/interactions/index.d.ts.map +1 -1
- package/dist/select/createHiddenSelect.d.ts.map +1 -1
- package/dist/toolbar/createToolbar.d.ts.map +1 -1
- package/dist/tooltip/createTooltipTrigger.d.ts.map +1 -1
- package/package.json +9 -7
- package/src/autocomplete/createAutocomplete.ts +341 -0
- package/src/autocomplete/index.ts +9 -0
- package/src/breadcrumbs/createBreadcrumbs.ts +196 -0
- package/src/breadcrumbs/index.ts +8 -0
- package/src/button/createButton.ts +142 -0
- package/src/button/createToggleButton.ts +101 -0
- package/src/button/index.ts +4 -0
- package/src/button/types.ts +78 -0
- package/src/calendar/createCalendar.ts +138 -0
- package/src/calendar/createCalendarCell.ts +187 -0
- package/src/calendar/createCalendarGrid.ts +140 -0
- package/src/calendar/createRangeCalendar.ts +136 -0
- package/src/calendar/createRangeCalendarCell.ts +186 -0
- package/src/calendar/index.ts +34 -0
- package/src/checkbox/createCheckbox.ts +135 -0
- package/src/checkbox/createCheckboxGroup.ts +137 -0
- package/src/checkbox/createCheckboxGroupItem.ts +117 -0
- package/src/checkbox/createCheckboxGroupState.ts +193 -0
- package/src/checkbox/index.ts +13 -0
- package/src/color/createColorArea.ts +314 -0
- package/src/color/createColorField.ts +137 -0
- package/src/color/createColorSlider.ts +197 -0
- package/src/color/createColorSwatch.ts +40 -0
- package/src/color/createColorWheel.ts +208 -0
- package/src/color/index.ts +24 -0
- package/src/color/types.ts +116 -0
- package/src/combobox/createComboBox.ts +647 -0
- package/src/combobox/index.ts +6 -0
- package/src/combobox/intl/en-US.json +7 -0
- package/src/combobox/intl/es-ES.json +7 -0
- package/src/combobox/intl/index.ts +23 -0
- package/src/datepicker/createDateField.ts +154 -0
- package/src/datepicker/createDatePicker.ts +206 -0
- package/src/datepicker/createDateSegment.ts +229 -0
- package/src/datepicker/createTimeField.ts +154 -0
- package/src/datepicker/index.ts +28 -0
- package/src/dialog/createDialog.ts +120 -0
- package/src/dialog/index.ts +2 -0
- package/src/dialog/types.ts +19 -0
- package/src/disclosure/createDisclosure.ts +131 -0
- package/src/disclosure/createDisclosureGroup.ts +62 -0
- package/src/disclosure/index.ts +11 -0
- package/src/dnd/createDrag.ts +209 -0
- package/src/dnd/createDraggableCollection.ts +63 -0
- package/src/dnd/createDraggableItem.ts +243 -0
- package/src/dnd/createDrop.ts +321 -0
- package/src/dnd/createDroppableCollection.ts +293 -0
- package/src/dnd/createDroppableItem.ts +213 -0
- package/src/dnd/index.ts +47 -0
- package/src/dnd/types.ts +89 -0
- package/src/dnd/utils.ts +294 -0
- package/src/focus/FocusScope.tsx +408 -0
- package/src/focus/createAutoFocus.ts +321 -0
- package/src/focus/createFocusRestore.ts +313 -0
- package/src/focus/createVirtualFocus.ts +396 -0
- package/src/focus/index.ts +35 -0
- package/src/form/createFormReset.ts +51 -0
- package/src/form/createFormValidation.ts +224 -0
- package/src/form/index.ts +11 -0
- package/src/grid/GridKeyboardDelegate.ts +429 -0
- package/src/grid/createGrid.ts +261 -0
- package/src/grid/createGridCell.ts +182 -0
- package/src/grid/createGridRow.ts +153 -0
- package/src/grid/index.ts +18 -0
- package/src/grid/types.ts +133 -0
- package/src/gridlist/createGridList.ts +185 -0
- package/src/gridlist/createGridListItem.ts +180 -0
- package/src/gridlist/createGridListSelectionCheckbox.ts +59 -0
- package/src/gridlist/index.ts +16 -0
- package/src/gridlist/types.ts +81 -0
- package/src/i18n/NumberFormatter.ts +266 -0
- package/src/i18n/createCollator.ts +79 -0
- package/src/i18n/createDateFormatter.ts +83 -0
- package/src/i18n/createFilter.ts +131 -0
- package/src/i18n/createNumberFormatter.ts +52 -0
- package/src/i18n/createStringFormatter.ts +87 -0
- package/src/i18n/index.ts +40 -0
- package/src/i18n/locale.tsx +188 -0
- package/src/i18n/utils.ts +99 -0
- package/src/index.ts +670 -0
- package/src/interactions/FocusableProvider.tsx +44 -0
- package/src/interactions/PressEvent.ts +126 -0
- package/src/interactions/createFocus.ts +163 -0
- package/src/interactions/createFocusRing.ts +89 -0
- package/src/interactions/createFocusWithin.ts +206 -0
- package/src/interactions/createFocusable.ts +168 -0
- package/src/interactions/createHover.ts +254 -0
- package/src/interactions/createInteractionModality.ts +424 -0
- package/src/interactions/createKeyboard.ts +82 -0
- package/src/interactions/createLongPress.ts +174 -0
- package/src/interactions/createMove.ts +289 -0
- package/src/interactions/createPress.ts +834 -0
- package/src/interactions/index.ts +78 -0
- package/src/label/createField.ts +145 -0
- package/src/label/createLabel.ts +117 -0
- package/src/label/createLabels.ts +50 -0
- package/src/label/index.ts +19 -0
- package/src/landmark/createLandmark.ts +377 -0
- package/src/landmark/index.ts +8 -0
- package/src/link/createLink.ts +182 -0
- package/src/link/index.ts +1 -0
- package/src/listbox/createListBox.ts +269 -0
- package/src/listbox/createOption.ts +151 -0
- package/src/listbox/index.ts +12 -0
- package/src/live-announcer/announce.ts +322 -0
- package/src/live-announcer/index.ts +9 -0
- package/src/menu/createMenu.ts +396 -0
- package/src/menu/createMenuItem.ts +149 -0
- package/src/menu/createMenuTrigger.ts +88 -0
- package/src/menu/index.ts +18 -0
- package/src/meter/createMeter.ts +75 -0
- package/src/meter/index.ts +1 -0
- package/src/numberfield/createNumberField.ts +268 -0
- package/src/numberfield/index.ts +5 -0
- package/src/overlays/ariaHideOutside.ts +219 -0
- package/src/overlays/createInteractOutside.ts +149 -0
- package/src/overlays/createModal.tsx +202 -0
- package/src/overlays/createOverlay.ts +155 -0
- package/src/overlays/createOverlayTrigger.ts +85 -0
- package/src/overlays/createPreventScroll.ts +266 -0
- package/src/overlays/index.ts +44 -0
- package/src/popover/calculatePosition.ts +766 -0
- package/src/popover/createOverlayPosition.ts +356 -0
- package/src/popover/createPopover.ts +170 -0
- package/src/popover/index.ts +24 -0
- package/src/progress/createProgressBar.ts +128 -0
- package/src/progress/index.ts +5 -0
- package/src/radio/createRadio.ts +287 -0
- package/src/radio/createRadioGroup.ts +189 -0
- package/src/radio/createRadioGroupState.ts +201 -0
- package/src/radio/index.ts +23 -0
- package/src/searchfield/createSearchField.ts +186 -0
- package/src/searchfield/index.ts +2 -0
- package/src/select/createHiddenSelect.tsx +236 -0
- package/src/select/createSelect.ts +395 -0
- package/src/select/index.ts +14 -0
- package/src/selection/createTypeSelect.ts +201 -0
- package/src/selection/index.ts +6 -0
- package/src/separator/createSeparator.ts +82 -0
- package/src/separator/index.ts +6 -0
- package/src/slider/createSlider.ts +349 -0
- package/src/slider/index.ts +2 -0
- package/src/ssr/index.tsx +370 -0
- package/src/switch/createSwitch.ts +70 -0
- package/src/switch/index.ts +1 -0
- package/src/table/createTable.ts +526 -0
- package/src/table/createTableCell.ts +147 -0
- package/src/table/createTableColumnHeader.ts +115 -0
- package/src/table/createTableHeaderRow.ts +40 -0
- package/src/table/createTableRow.ts +155 -0
- package/src/table/createTableRowGroup.ts +32 -0
- package/src/table/createTableSelectAllCheckbox.ts +73 -0
- package/src/table/createTableSelectionCheckbox.ts +59 -0
- package/src/table/index.ts +30 -0
- package/src/table/types.ts +165 -0
- package/src/tabs/createTabs.ts +472 -0
- package/src/tabs/index.ts +14 -0
- package/src/tag/createTag.ts +194 -0
- package/src/tag/createTagGroup.ts +154 -0
- package/src/tag/index.ts +12 -0
- package/src/textfield/createTextField.ts +198 -0
- package/src/textfield/index.ts +5 -0
- package/src/toast/createToast.ts +118 -0
- package/src/toast/createToastRegion.ts +100 -0
- package/src/toast/index.ts +11 -0
- package/src/toggle/createToggle.ts +223 -0
- package/src/toggle/createToggleState.ts +94 -0
- package/src/toggle/index.ts +7 -0
- package/src/toolbar/createToolbar.ts +369 -0
- package/src/toolbar/index.ts +6 -0
- package/src/tooltip/createTooltip.ts +79 -0
- package/src/tooltip/createTooltipTrigger.ts +222 -0
- package/src/tooltip/index.ts +6 -0
- package/src/tree/createTree.ts +246 -0
- package/src/tree/createTreeItem.ts +233 -0
- package/src/tree/createTreeSelectionCheckbox.ts +68 -0
- package/src/tree/index.ts +16 -0
- package/src/tree/types.ts +87 -0
- package/src/utils/createDescription.ts +137 -0
- package/src/utils/dom.ts +327 -0
- package/src/utils/env.ts +54 -0
- package/src/utils/events.ts +106 -0
- package/src/utils/filterDOMProps.ts +116 -0
- package/src/utils/focus.ts +151 -0
- package/src/utils/geometry.ts +115 -0
- package/src/utils/globalListeners.ts +142 -0
- package/src/utils/index.ts +80 -0
- package/src/utils/mergeProps.ts +52 -0
- package/src/utils/platform.ts +52 -0
- package/src/utils/reactivity.ts +36 -0
- package/src/utils/textSelection.ts +114 -0
- package/src/visually-hidden/createVisuallyHidden.ts +124 -0
- package/src/visually-hidden/index.ts +6 -0
- package/dist/index.jsx +0 -15845
- 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
|
+
}
|