@proyecto-viviana/solidaria 0.2.1 → 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
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Switch hook for Solidaria
|
|
3
|
+
*
|
|
4
|
+
* Provides the behavior and accessibility implementation for a switch component.
|
|
5
|
+
* A switch is similar to a checkbox, but represents on/off values as opposed to selection.
|
|
6
|
+
*
|
|
7
|
+
* This is a 1:1 port of @react-aria/switch's useSwitch hook.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { JSX, Accessor } from 'solid-js';
|
|
11
|
+
import { createToggle, type AriaToggleProps } from '../toggle/createToggle';
|
|
12
|
+
import { type ToggleState } from '@proyecto-viviana/solid-stately';
|
|
13
|
+
import { type MaybeAccessor } from '../utils/reactivity';
|
|
14
|
+
|
|
15
|
+
// ============================================
|
|
16
|
+
// TYPES
|
|
17
|
+
// ============================================
|
|
18
|
+
|
|
19
|
+
export interface AriaSwitchProps extends AriaToggleProps {
|
|
20
|
+
// Switch uses the same props as toggle
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SwitchAria {
|
|
24
|
+
/** Props for the label wrapper element. */
|
|
25
|
+
labelProps: JSX.LabelHTMLAttributes<HTMLLabelElement>;
|
|
26
|
+
/** Props for the input element. */
|
|
27
|
+
inputProps: JSX.InputHTMLAttributes<HTMLInputElement>;
|
|
28
|
+
/** Whether the switch is selected. */
|
|
29
|
+
isSelected: Accessor<boolean>;
|
|
30
|
+
/** Whether the switch is in a pressed state. */
|
|
31
|
+
isPressed: Accessor<boolean>;
|
|
32
|
+
/** Whether the switch is disabled. */
|
|
33
|
+
isDisabled: boolean;
|
|
34
|
+
/** Whether the switch is read only. */
|
|
35
|
+
isReadOnly: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ============================================
|
|
39
|
+
// IMPLEMENTATION
|
|
40
|
+
// ============================================
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Provides the behavior and accessibility implementation for a switch component.
|
|
44
|
+
* A switch is similar to a checkbox, but represents on/off values as opposed to selection.
|
|
45
|
+
*/
|
|
46
|
+
export function createSwitch(
|
|
47
|
+
props: MaybeAccessor<AriaSwitchProps>,
|
|
48
|
+
state: ToggleState,
|
|
49
|
+
ref: () => HTMLInputElement | null
|
|
50
|
+
): SwitchAria {
|
|
51
|
+
// Don't destructure inputProps - it's a getter that needs to be evaluated each time
|
|
52
|
+
const toggle = createToggle(props, state, ref);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
labelProps: toggle.labelProps,
|
|
56
|
+
get inputProps() {
|
|
57
|
+
// Access toggle.inputProps (the getter) each time to get fresh values
|
|
58
|
+
const baseProps = toggle.inputProps;
|
|
59
|
+
return {
|
|
60
|
+
...baseProps,
|
|
61
|
+
role: 'switch' as const,
|
|
62
|
+
checked: toggle.isSelected(),
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
isSelected: toggle.isSelected,
|
|
66
|
+
isPressed: toggle.isPressed,
|
|
67
|
+
isDisabled: toggle.isDisabled,
|
|
68
|
+
isReadOnly: toggle.isReadOnly,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createSwitch, type AriaSwitchProps, type SwitchAria } from './createSwitch';
|
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createTable - Provides accessibility for a table component.
|
|
3
|
+
* Based on @react-aria/table/useTable.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createMemo, createEffect, on, type Accessor } from 'solid-js';
|
|
7
|
+
import type { JSX } from 'solid-js';
|
|
8
|
+
import { createId } from '@proyecto-viviana/solid-stately';
|
|
9
|
+
import type { TableState, TableCollection, Key, GridNode } from '@proyecto-viviana/solid-stately';
|
|
10
|
+
import type { AriaTableProps, TableAria } from './types';
|
|
11
|
+
import { useLocale } from '../i18n';
|
|
12
|
+
import { announce } from '../live-announcer';
|
|
13
|
+
|
|
14
|
+
// Global map to store table metadata for child components
|
|
15
|
+
const tableMap = new WeakMap<
|
|
16
|
+
object,
|
|
17
|
+
{
|
|
18
|
+
tableId: string;
|
|
19
|
+
actions: { onRowAction?: (key: Key) => void; onCellAction?: (key: Key) => void };
|
|
20
|
+
shouldSelectOnPressUp?: boolean;
|
|
21
|
+
focusMode?: 'row' | 'cell';
|
|
22
|
+
}
|
|
23
|
+
>();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get the table metadata for child components.
|
|
27
|
+
*/
|
|
28
|
+
export function getTableData<T>(state: TableState<T, TableCollection<T>>) {
|
|
29
|
+
return tableMap.get(state);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Helper to get cells from a row by iterating children
|
|
34
|
+
*/
|
|
35
|
+
function getChildCells<T>(collection: TableCollection<T>, rowKey: Key): GridNode<T>[] {
|
|
36
|
+
const children = collection.getChildren(rowKey);
|
|
37
|
+
return [...children].filter(node => node.type === 'cell' || node.type === 'rowheader');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Helper to get cell at specific index in a row
|
|
42
|
+
*/
|
|
43
|
+
function getCellAtIndex<T>(collection: TableCollection<T>, rowKey: Key, index: number): GridNode<T> | null {
|
|
44
|
+
const cells = getChildCells(collection, rowKey);
|
|
45
|
+
return cells[index] ?? null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Helper to check if a node is a cell
|
|
50
|
+
*/
|
|
51
|
+
function isCell<T>(node: GridNode<T> | null): boolean {
|
|
52
|
+
return node?.type === 'cell' || node?.type === 'rowheader';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Helper to check if a node is a row
|
|
57
|
+
*/
|
|
58
|
+
function isRow<T>(node: GridNode<T> | null): boolean {
|
|
59
|
+
return node?.type === 'item';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Creates accessibility props for a table component.
|
|
64
|
+
*/
|
|
65
|
+
export function createTable<T extends object>(
|
|
66
|
+
props: Accessor<AriaTableProps>,
|
|
67
|
+
state: Accessor<TableState<T, TableCollection<T>>>,
|
|
68
|
+
ref: Accessor<HTMLTableElement | null>
|
|
69
|
+
): TableAria {
|
|
70
|
+
const id = createId(props().id);
|
|
71
|
+
const locale = useLocale();
|
|
72
|
+
|
|
73
|
+
// Track previous sort descriptor for announcements
|
|
74
|
+
let prevSortDescriptor: { column: Key; direction: 'ascending' | 'descending' } | null = null;
|
|
75
|
+
let isFirstRender = true;
|
|
76
|
+
|
|
77
|
+
// Store metadata for child components
|
|
78
|
+
const storeTableData = () => {
|
|
79
|
+
const s = state();
|
|
80
|
+
const p = props();
|
|
81
|
+
tableMap.set(s, {
|
|
82
|
+
tableId: id,
|
|
83
|
+
actions: {
|
|
84
|
+
onRowAction: p.onRowAction,
|
|
85
|
+
onCellAction: p.onCellAction,
|
|
86
|
+
},
|
|
87
|
+
shouldSelectOnPressUp: p.shouldSelectOnPressUp,
|
|
88
|
+
focusMode: p.focusMode,
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Update table data whenever props/state changes
|
|
93
|
+
createMemo(() => {
|
|
94
|
+
storeTableData();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Announce sort changes (only after initial render)
|
|
98
|
+
createEffect(on(
|
|
99
|
+
() => state().sortDescriptor,
|
|
100
|
+
(sortDescriptor) => {
|
|
101
|
+
if (isFirstRender) {
|
|
102
|
+
isFirstRender = false;
|
|
103
|
+
prevSortDescriptor = sortDescriptor;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (sortDescriptor && (
|
|
108
|
+
sortDescriptor.column !== prevSortDescriptor?.column ||
|
|
109
|
+
sortDescriptor.direction !== prevSortDescriptor?.direction
|
|
110
|
+
)) {
|
|
111
|
+
const collection = state().collection;
|
|
112
|
+
const column = collection.columns.find(c => c.key === sortDescriptor.column);
|
|
113
|
+
const columnName = column?.textValue ?? String(sortDescriptor.column);
|
|
114
|
+
const directionText = sortDescriptor.direction === 'ascending' ? 'ascending' : 'descending';
|
|
115
|
+
|
|
116
|
+
announce(`Sorted by ${columnName}, ${directionText}`, 'assertive', 500);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
prevSortDescriptor = sortDescriptor;
|
|
120
|
+
}
|
|
121
|
+
));
|
|
122
|
+
|
|
123
|
+
// Keyboard navigation handler with full 2D navigation
|
|
124
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
125
|
+
const s = state();
|
|
126
|
+
const collection = s.collection;
|
|
127
|
+
const p = props();
|
|
128
|
+
const focusMode = p.focusMode ?? 'row';
|
|
129
|
+
const isRTL = locale().direction === 'rtl';
|
|
130
|
+
|
|
131
|
+
if (s.isKeyboardNavigationDisabled) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const focusedKey = s.focusedKey;
|
|
136
|
+
if (focusedKey == null) {
|
|
137
|
+
// If nothing is focused, focus the first item
|
|
138
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Home' || e.key === 'End') {
|
|
139
|
+
const firstKey = collection.getFirstKey();
|
|
140
|
+
if (firstKey != null) {
|
|
141
|
+
e.preventDefault();
|
|
142
|
+
s.setFocusedKey(firstKey);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const focusedItem = collection.getItem(focusedKey);
|
|
149
|
+
if (!focusedItem) return;
|
|
150
|
+
|
|
151
|
+
let nextKey: Key | null = null;
|
|
152
|
+
|
|
153
|
+
switch (e.key) {
|
|
154
|
+
case 'ArrowDown': {
|
|
155
|
+
e.preventDefault();
|
|
156
|
+
// If focused on a cell, move to the same column in the next row
|
|
157
|
+
if (isCell(focusedItem) && focusedItem.parentKey != null) {
|
|
158
|
+
const nextRowKey = collection.getKeyAfter(focusedItem.parentKey);
|
|
159
|
+
if (nextRowKey != null) {
|
|
160
|
+
const cellIndex = focusedItem.index;
|
|
161
|
+
const nextCell = getCellAtIndex(collection, nextRowKey, cellIndex);
|
|
162
|
+
nextKey = nextCell?.key ?? nextRowKey;
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
// Move to next row
|
|
166
|
+
nextKey = collection.getKeyAfter(focusedKey);
|
|
167
|
+
}
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
case 'ArrowUp': {
|
|
172
|
+
e.preventDefault();
|
|
173
|
+
// If focused on a cell, move to the same column in the previous row
|
|
174
|
+
if (isCell(focusedItem) && focusedItem.parentKey != null) {
|
|
175
|
+
const prevRowKey = collection.getKeyBefore(focusedItem.parentKey);
|
|
176
|
+
if (prevRowKey != null) {
|
|
177
|
+
const cellIndex = focusedItem.index;
|
|
178
|
+
const prevCell = getCellAtIndex(collection, prevRowKey, cellIndex);
|
|
179
|
+
nextKey = prevCell?.key ?? prevRowKey;
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
// Move to previous row
|
|
183
|
+
nextKey = collection.getKeyBefore(focusedKey);
|
|
184
|
+
}
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
case 'ArrowRight': {
|
|
189
|
+
e.preventDefault();
|
|
190
|
+
const goNext = !isRTL;
|
|
191
|
+
|
|
192
|
+
if (isRow(focusedItem)) {
|
|
193
|
+
// If on a row, go to the first/last cell
|
|
194
|
+
const cells = getChildCells(collection, focusedKey);
|
|
195
|
+
if (cells.length > 0) {
|
|
196
|
+
nextKey = goNext ? cells[0].key : cells[cells.length - 1].key;
|
|
197
|
+
}
|
|
198
|
+
} else if (isCell(focusedItem) && focusedItem.parentKey != null) {
|
|
199
|
+
// If on a cell, go to the next/prev cell
|
|
200
|
+
const cells = getChildCells(collection, focusedItem.parentKey);
|
|
201
|
+
const currentIndex = focusedItem.index;
|
|
202
|
+
const targetIndex = goNext ? currentIndex + 1 : currentIndex - 1;
|
|
203
|
+
|
|
204
|
+
if (targetIndex >= 0 && targetIndex < cells.length) {
|
|
205
|
+
nextKey = cells[targetIndex].key;
|
|
206
|
+
} else if (focusMode === 'row') {
|
|
207
|
+
// Wrap to row
|
|
208
|
+
nextKey = focusedItem.parentKey;
|
|
209
|
+
} else {
|
|
210
|
+
// Wrap to first/last cell
|
|
211
|
+
nextKey = goNext ? cells[0].key : cells[cells.length - 1].key;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
case 'ArrowLeft': {
|
|
218
|
+
e.preventDefault();
|
|
219
|
+
const goNext = isRTL;
|
|
220
|
+
|
|
221
|
+
if (isRow(focusedItem)) {
|
|
222
|
+
// If on a row, go to the last/first cell
|
|
223
|
+
const cells = getChildCells(collection, focusedKey);
|
|
224
|
+
if (cells.length > 0) {
|
|
225
|
+
nextKey = goNext ? cells[0].key : cells[cells.length - 1].key;
|
|
226
|
+
}
|
|
227
|
+
} else if (isCell(focusedItem) && focusedItem.parentKey != null) {
|
|
228
|
+
// If on a cell, go to the prev/next cell
|
|
229
|
+
const cells = getChildCells(collection, focusedItem.parentKey);
|
|
230
|
+
const currentIndex = focusedItem.index;
|
|
231
|
+
const targetIndex = goNext ? currentIndex + 1 : currentIndex - 1;
|
|
232
|
+
|
|
233
|
+
if (targetIndex >= 0 && targetIndex < cells.length) {
|
|
234
|
+
nextKey = cells[targetIndex].key;
|
|
235
|
+
} else if (focusMode === 'row') {
|
|
236
|
+
// Wrap to row
|
|
237
|
+
nextKey = focusedItem.parentKey;
|
|
238
|
+
} else {
|
|
239
|
+
// Wrap to first/last cell
|
|
240
|
+
nextKey = goNext ? cells[0].key : cells[cells.length - 1].key;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
case 'Home': {
|
|
247
|
+
e.preventDefault();
|
|
248
|
+
if (e.ctrlKey) {
|
|
249
|
+
// Ctrl+Home: Go to first row/cell
|
|
250
|
+
const firstRowKey = collection.getFirstKey();
|
|
251
|
+
if (firstRowKey != null) {
|
|
252
|
+
if (isCell(focusedItem) || focusMode === 'cell') {
|
|
253
|
+
const cells = getChildCells(collection, firstRowKey);
|
|
254
|
+
nextKey = cells[0]?.key ?? firstRowKey;
|
|
255
|
+
} else {
|
|
256
|
+
nextKey = firstRowKey;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
} else if (isCell(focusedItem) && focusedItem.parentKey != null) {
|
|
260
|
+
// Home: Go to first cell in current row
|
|
261
|
+
const cells = getChildCells(collection, focusedItem.parentKey);
|
|
262
|
+
nextKey = cells[0]?.key ?? null;
|
|
263
|
+
} else {
|
|
264
|
+
// On row: go to first row
|
|
265
|
+
nextKey = collection.getFirstKey();
|
|
266
|
+
}
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
case 'End': {
|
|
271
|
+
e.preventDefault();
|
|
272
|
+
if (e.ctrlKey) {
|
|
273
|
+
// Ctrl+End: Go to last row/cell
|
|
274
|
+
const lastRowKey = collection.getLastKey();
|
|
275
|
+
if (lastRowKey != null) {
|
|
276
|
+
if (isCell(focusedItem) || focusMode === 'cell') {
|
|
277
|
+
const cells = getChildCells(collection, lastRowKey);
|
|
278
|
+
nextKey = cells[cells.length - 1]?.key ?? lastRowKey;
|
|
279
|
+
} else {
|
|
280
|
+
nextKey = lastRowKey;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
} else if (isCell(focusedItem) && focusedItem.parentKey != null) {
|
|
284
|
+
// End: Go to last cell in current row
|
|
285
|
+
const cells = getChildCells(collection, focusedItem.parentKey);
|
|
286
|
+
nextKey = cells[cells.length - 1]?.key ?? null;
|
|
287
|
+
} else {
|
|
288
|
+
// On row: go to last row
|
|
289
|
+
nextKey = collection.getLastKey();
|
|
290
|
+
}
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
case 'PageDown': {
|
|
295
|
+
e.preventDefault();
|
|
296
|
+
// Move down by roughly a page (using DOM measurements if available)
|
|
297
|
+
const el = ref();
|
|
298
|
+
if (el) {
|
|
299
|
+
const visibleHeight = el.clientHeight;
|
|
300
|
+
let currentKey: Key | null = focusedKey;
|
|
301
|
+
let traveled = 0;
|
|
302
|
+
|
|
303
|
+
// If on a cell, start from the parent row
|
|
304
|
+
if (isCell(focusedItem) && focusedItem.parentKey != null) {
|
|
305
|
+
currentKey = focusedItem.parentKey;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Move down until we've traveled approximately one page
|
|
309
|
+
while (currentKey != null && traveled < visibleHeight) {
|
|
310
|
+
const next = collection.getKeyAfter(currentKey);
|
|
311
|
+
if (next == null) break;
|
|
312
|
+
|
|
313
|
+
// Estimate row height (default to 40px if we can't measure)
|
|
314
|
+
const rowElement = el.querySelector(`[data-key="${currentKey}"]`);
|
|
315
|
+
traveled += rowElement?.clientHeight ?? 40;
|
|
316
|
+
currentKey = next;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (currentKey != null) {
|
|
320
|
+
// If we started on a cell, focus the same column in the new row
|
|
321
|
+
if (isCell(focusedItem)) {
|
|
322
|
+
const cellIndex = focusedItem.index;
|
|
323
|
+
const targetCell = getCellAtIndex(collection, currentKey, cellIndex);
|
|
324
|
+
nextKey = targetCell?.key ?? currentKey;
|
|
325
|
+
} else {
|
|
326
|
+
nextKey = currentKey;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
} else {
|
|
330
|
+
// Fallback: move 10 rows
|
|
331
|
+
let count = 10;
|
|
332
|
+
let current: Key | null = isCell(focusedItem) && focusedItem.parentKey != null
|
|
333
|
+
? focusedItem.parentKey
|
|
334
|
+
: focusedKey;
|
|
335
|
+
while (count > 0 && current != null) {
|
|
336
|
+
const next = collection.getKeyAfter(current);
|
|
337
|
+
if (next == null) break;
|
|
338
|
+
current = next;
|
|
339
|
+
count--;
|
|
340
|
+
}
|
|
341
|
+
if (current != null && isCell(focusedItem)) {
|
|
342
|
+
const targetCell = getCellAtIndex(collection, current, focusedItem.index);
|
|
343
|
+
nextKey = targetCell?.key ?? current;
|
|
344
|
+
} else {
|
|
345
|
+
nextKey = current;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
case 'PageUp': {
|
|
352
|
+
e.preventDefault();
|
|
353
|
+
// Move up by roughly a page
|
|
354
|
+
const el = ref();
|
|
355
|
+
if (el) {
|
|
356
|
+
const visibleHeight = el.clientHeight;
|
|
357
|
+
let currentKey: Key | null = focusedKey;
|
|
358
|
+
let traveled = 0;
|
|
359
|
+
|
|
360
|
+
// If on a cell, start from the parent row
|
|
361
|
+
if (isCell(focusedItem) && focusedItem.parentKey != null) {
|
|
362
|
+
currentKey = focusedItem.parentKey;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Move up until we've traveled approximately one page
|
|
366
|
+
while (currentKey != null && traveled < visibleHeight) {
|
|
367
|
+
const prev = collection.getKeyBefore(currentKey);
|
|
368
|
+
if (prev == null) break;
|
|
369
|
+
|
|
370
|
+
const rowElement = el.querySelector(`[data-key="${currentKey}"]`);
|
|
371
|
+
traveled += rowElement?.clientHeight ?? 40;
|
|
372
|
+
currentKey = prev;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (currentKey != null) {
|
|
376
|
+
if (isCell(focusedItem)) {
|
|
377
|
+
const cellIndex = focusedItem.index;
|
|
378
|
+
const targetCell = getCellAtIndex(collection, currentKey, cellIndex);
|
|
379
|
+
nextKey = targetCell?.key ?? currentKey;
|
|
380
|
+
} else {
|
|
381
|
+
nextKey = currentKey;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
} else {
|
|
385
|
+
// Fallback: move 10 rows
|
|
386
|
+
let count = 10;
|
|
387
|
+
let current: Key | null = isCell(focusedItem) && focusedItem.parentKey != null
|
|
388
|
+
? focusedItem.parentKey
|
|
389
|
+
: focusedKey;
|
|
390
|
+
while (count > 0 && current != null) {
|
|
391
|
+
const prev = collection.getKeyBefore(current);
|
|
392
|
+
if (prev == null) break;
|
|
393
|
+
current = prev;
|
|
394
|
+
count--;
|
|
395
|
+
}
|
|
396
|
+
if (current != null && isCell(focusedItem)) {
|
|
397
|
+
const targetCell = getCellAtIndex(collection, current, focusedItem.index);
|
|
398
|
+
nextKey = targetCell?.key ?? current;
|
|
399
|
+
} else {
|
|
400
|
+
nextKey = current;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
case 'Escape':
|
|
407
|
+
s.clearSelection();
|
|
408
|
+
return;
|
|
409
|
+
|
|
410
|
+
case 'a':
|
|
411
|
+
if (e.ctrlKey || e.metaKey) {
|
|
412
|
+
e.preventDefault();
|
|
413
|
+
if (s.selectionMode === 'multiple') {
|
|
414
|
+
s.selectAll();
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return;
|
|
418
|
+
|
|
419
|
+
case ' ':
|
|
420
|
+
case 'Enter':
|
|
421
|
+
e.preventDefault();
|
|
422
|
+
// Toggle selection or trigger action
|
|
423
|
+
if (s.selectionMode !== 'none') {
|
|
424
|
+
// For cells, select the parent row
|
|
425
|
+
const keyToSelect = isCell(focusedItem) && focusedItem.parentKey != null
|
|
426
|
+
? focusedItem.parentKey
|
|
427
|
+
: focusedKey;
|
|
428
|
+
|
|
429
|
+
if (e.shiftKey && s.selectionMode === 'multiple') {
|
|
430
|
+
s.extendSelection(keyToSelect);
|
|
431
|
+
} else {
|
|
432
|
+
s.toggleSelection(keyToSelect);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return;
|
|
436
|
+
|
|
437
|
+
default:
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (nextKey != null) {
|
|
442
|
+
s.setFocusedKey(nextKey);
|
|
443
|
+
|
|
444
|
+
// Handle shift+arrow for range selection
|
|
445
|
+
if (e.shiftKey && s.selectionMode === 'multiple') {
|
|
446
|
+
// For cells, select the parent row
|
|
447
|
+
const focusedNode = collection.getItem(nextKey);
|
|
448
|
+
const keyToSelect = focusedNode && isCell(focusedNode) && focusedNode.parentKey != null
|
|
449
|
+
? focusedNode.parentKey
|
|
450
|
+
: nextKey;
|
|
451
|
+
s.extendSelection(keyToSelect);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
// Focus handling
|
|
457
|
+
const onFocus = (e: FocusEvent) => {
|
|
458
|
+
const s = state();
|
|
459
|
+
const el = ref();
|
|
460
|
+
|
|
461
|
+
if (!el?.contains(e.target as Element)) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (!s.isFocused) {
|
|
466
|
+
s.setFocused(true);
|
|
467
|
+
|
|
468
|
+
// If no key is focused, focus the first one
|
|
469
|
+
if (s.focusedKey == null) {
|
|
470
|
+
const firstKey = s.collection.getFirstKey();
|
|
471
|
+
if (firstKey != null) {
|
|
472
|
+
s.setFocusedKey(firstKey);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const onBlur = (e: FocusEvent) => {
|
|
479
|
+
const s = state();
|
|
480
|
+
const el = ref();
|
|
481
|
+
|
|
482
|
+
// Only blur if focus is leaving the table entirely
|
|
483
|
+
if (el && !el.contains(e.relatedTarget as Element)) {
|
|
484
|
+
s.setFocused(false);
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
// Warn if no label is provided
|
|
489
|
+
createMemo(() => {
|
|
490
|
+
const p = props();
|
|
491
|
+
if (!p['aria-label'] && !p['aria-labelledby']) {
|
|
492
|
+
console.warn('Table: An aria-label or aria-labelledby prop is required for accessibility.');
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
const gridProps = createMemo(() => {
|
|
497
|
+
const p = props();
|
|
498
|
+
const s = state();
|
|
499
|
+
|
|
500
|
+
const baseProps: Record<string, unknown> = {
|
|
501
|
+
role: 'grid',
|
|
502
|
+
id,
|
|
503
|
+
'aria-label': p['aria-label'],
|
|
504
|
+
'aria-labelledby': p['aria-labelledby'],
|
|
505
|
+
'aria-describedby': p['aria-describedby'],
|
|
506
|
+
'aria-multiselectable': s.selectionMode === 'multiple' ? 'true' : undefined,
|
|
507
|
+
tabIndex: s.collection.size === 0 ? 0 : -1,
|
|
508
|
+
onKeyDown,
|
|
509
|
+
onFocus,
|
|
510
|
+
onBlur,
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
if (p.isVirtualized) {
|
|
514
|
+
baseProps['aria-rowcount'] = s.collection.rowCount;
|
|
515
|
+
baseProps['aria-colcount'] = s.collection.columnCount;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return baseProps as JSX.HTMLAttributes<HTMLTableElement>;
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
get gridProps() {
|
|
523
|
+
return gridProps();
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
}
|