@proyecto-viviana/solid-stately 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/calendar/IncompleteDate.d.ts +61 -0
- package/dist/calendar/createCalendarState.d.ts +20 -10
- package/dist/calendar/createDateFieldState.d.ts +10 -3
- package/dist/calendar/createRangeCalendarState.d.ts +10 -2
- package/dist/calendar/index.d.ts +1 -1
- package/dist/collections/createListState.d.ts +4 -1
- package/dist/collections/createSelectionState.d.ts +17 -5
- package/dist/collections/getChildNodes.d.ts +11 -0
- package/dist/collections/index.d.ts +2 -1
- package/dist/combobox/createComboBoxState.d.ts +7 -0
- package/dist/dnd/index.d.ts +1 -1
- package/dist/dnd/types.d.ts +3 -1
- package/dist/grid/types.d.ts +17 -3
- package/dist/index.d.ts +6 -6
- package/dist/index.js +1570 -425
- package/dist/index.js.map +1 -1
- package/dist/select/createSelectState.d.ts +6 -0
- package/dist/selection/Selection.d.ts +16 -0
- package/dist/selection/SelectionManager.d.ts +110 -0
- package/dist/selection/createMultipleSelectionState.d.ts +59 -0
- package/dist/selection/index.d.ts +1 -0
- package/dist/table/TableCollection.d.ts +30 -0
- package/dist/table/createTreeGridState.d.ts +15 -0
- package/dist/table/index.d.ts +2 -1
- package/dist/table/types.d.ts +58 -0
- package/dist/toast/createToastState.d.ts +16 -0
- package/dist/toast/index.d.ts +1 -1
- package/dist/tree/types.d.ts +14 -6
- package/package.json +1 -1
- package/src/calendar/IncompleteDate.ts +430 -0
- package/src/calendar/createCalendarState.ts +104 -37
- package/src/calendar/createDateFieldState.ts +262 -261
- package/src/calendar/createRangeCalendarState.ts +131 -19
- package/src/calendar/index.ts +2 -0
- package/src/collections/createListState.ts +50 -19
- package/src/collections/createMenuState.ts +3 -0
- package/src/collections/createSelectionState.ts +38 -28
- package/src/collections/getChildNodes.ts +98 -0
- package/src/collections/index.ts +9 -0
- package/src/combobox/createComboBoxState.ts +8 -0
- package/src/dnd/index.d.ts +1 -0
- package/src/dnd/index.ts +1 -0
- package/src/dnd/types.d.ts +3 -1
- package/src/dnd/types.ts +4 -1
- package/src/grid/createGridState.ts +36 -2
- package/src/grid/types.ts +17 -3
- package/src/index.d.ts +1 -0
- package/src/index.ts +11 -0
- package/src/select/createSelectState.ts +15 -2
- package/src/selection/Selection.ts +28 -0
- package/src/selection/SelectionManager.ts +526 -0
- package/src/selection/createMultipleSelectionState.ts +247 -0
- package/src/selection/index.ts +6 -0
- package/src/table/TableCollection.ts +213 -43
- package/src/table/createTableState.ts +20 -0
- package/src/table/createTreeGridState.ts +146 -0
- package/src/table/index.ts +3 -0
- package/src/table/types.ts +66 -0
- package/src/tabs/createTabListState.ts +11 -9
- package/src/toast/createToastState.ts +43 -10
- package/src/toast/index.ts +1 -0
- package/src/tree/createTreeState.ts +37 -6
- package/src/tree/types.ts +14 -6
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw multiple-selection + focus state for a collection.
|
|
3
|
+
*
|
|
4
|
+
* The lower of the two selection layers ported from upstream
|
|
5
|
+
* (`@react-stately/selection`'s `useMultipleSelectionState`): it owns the
|
|
6
|
+
* selected keys, focus, selection behavior, and disabled keys, but is NOT
|
|
7
|
+
* collection-aware. The collection-aware operations (toggle, extend, select
|
|
8
|
+
* all, range, etc.) live in {@link SelectionManager}, which is built on top of
|
|
9
|
+
* this state.
|
|
10
|
+
*
|
|
11
|
+
* Based on @react-stately/selection.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createSignal, createMemo, createComputed, untrack, type Accessor } from "solid-js";
|
|
15
|
+
import { access, type MaybeAccessor } from "../utils";
|
|
16
|
+
import type { SelectionStateProps } from "../collections/createSelectionState";
|
|
17
|
+
import type {
|
|
18
|
+
DisabledBehavior,
|
|
19
|
+
FocusStrategy,
|
|
20
|
+
Key,
|
|
21
|
+
Selection,
|
|
22
|
+
SelectionBehavior,
|
|
23
|
+
SelectionMode,
|
|
24
|
+
} from "../collections/types";
|
|
25
|
+
import { Selection as SelectionClass } from "./Selection";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The raw state consumed by {@link SelectionManager}. Mirrors upstream's
|
|
29
|
+
* `MultipleSelectionState`: property reads (not Solid accessors) so the manager
|
|
30
|
+
* can read `state.selectionMode` etc. exactly as upstream does — the getters
|
|
31
|
+
* read the underlying signals, so reads inside reactive scopes still track.
|
|
32
|
+
*/
|
|
33
|
+
export interface MultipleSelectionState {
|
|
34
|
+
/** The type of selection that is allowed in the collection. */
|
|
35
|
+
readonly selectionMode: SelectionMode;
|
|
36
|
+
/** The selection behavior for the collection. */
|
|
37
|
+
readonly selectionBehavior: SelectionBehavior;
|
|
38
|
+
/** Sets the selection behavior for the collection. */
|
|
39
|
+
setSelectionBehavior(selectionBehavior: SelectionBehavior): void;
|
|
40
|
+
/** Whether the collection allows empty selection. */
|
|
41
|
+
readonly disallowEmptySelection: boolean;
|
|
42
|
+
/** Whether the collection is currently focused. */
|
|
43
|
+
readonly isFocused: boolean;
|
|
44
|
+
/** Sets whether the collection is focused. */
|
|
45
|
+
setFocused(isFocused: boolean): void;
|
|
46
|
+
/** The current focused key in the collection. */
|
|
47
|
+
readonly focusedKey: Key | null;
|
|
48
|
+
/** Whether the first or last child of the focused key should receive focus. */
|
|
49
|
+
readonly childFocusStrategy: FocusStrategy | null;
|
|
50
|
+
/** Sets the focused key, and optionally, whether the first or last child of that key should receive focus. */
|
|
51
|
+
setFocusedKey(key: Key | null, childFocusStrategy?: FocusStrategy): void;
|
|
52
|
+
/** The currently selected keys in the collection. */
|
|
53
|
+
readonly selectedKeys: Selection;
|
|
54
|
+
/** The last selection payload emitted through onSelectionChange. @internal */
|
|
55
|
+
readonly lastSelectionEvent: Selection | null;
|
|
56
|
+
/** Sets the selected keys in the collection. */
|
|
57
|
+
setSelectedKeys(keys: Selection): void;
|
|
58
|
+
/** Re-emits a selection payload without recomputing or mutating selection. @internal */
|
|
59
|
+
emitDuplicateSelectionEvent(keys?: Selection): void;
|
|
60
|
+
/** The currently disabled keys in the collection. */
|
|
61
|
+
readonly disabledKeys: Set<Key>;
|
|
62
|
+
/** Whether `disabledKeys` applies to all interactions, or only selection. */
|
|
63
|
+
readonly disabledBehavior: DisabledBehavior;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function equalSets(setA: Set<Key>, setB: Set<Key>): boolean {
|
|
67
|
+
if (setA.size !== setB.size) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const item of setA) {
|
|
72
|
+
if (!setB.has(item)) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function convertSelection(
|
|
81
|
+
selection: "all" | Iterable<Key> | null | undefined,
|
|
82
|
+
defaultValue: Selection,
|
|
83
|
+
): Selection;
|
|
84
|
+
function convertSelection(
|
|
85
|
+
selection: "all" | Iterable<Key> | null | undefined,
|
|
86
|
+
): Selection | undefined;
|
|
87
|
+
function convertSelection(
|
|
88
|
+
selection: "all" | Iterable<Key> | null | undefined,
|
|
89
|
+
defaultValue?: Selection,
|
|
90
|
+
): Selection | undefined {
|
|
91
|
+
if (!selection) {
|
|
92
|
+
return defaultValue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return selection === "all" ? "all" : new SelectionClass(selection);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Manages raw state for multiple selection and focus in a collection.
|
|
100
|
+
*/
|
|
101
|
+
export function createMultipleSelectionState(
|
|
102
|
+
props: MaybeAccessor<SelectionStateProps> = {},
|
|
103
|
+
): MultipleSelectionState {
|
|
104
|
+
const getProps = () => access(props);
|
|
105
|
+
|
|
106
|
+
// Focus is a plain signal in Solid; upstream needs a ref + state pair only
|
|
107
|
+
// because React requires both a synchronous read and a re-render trigger.
|
|
108
|
+
const [isFocused, setIsFocused] = createSignal(false);
|
|
109
|
+
const [focusedKey, setFocusedKeySignal] = createSignal<Key | null>(null);
|
|
110
|
+
const [childFocusStrategy, setChildFocusStrategy] = createSignal<FocusStrategy | null>(null);
|
|
111
|
+
|
|
112
|
+
const defaultSelectedKeys = convertSelection(
|
|
113
|
+
getProps().defaultSelectedKeys,
|
|
114
|
+
new SelectionClass(),
|
|
115
|
+
);
|
|
116
|
+
const [uncontrolledSelectedKeys, setUncontrolledSelectedKeys] =
|
|
117
|
+
createSignal<Selection>(defaultSelectedKeys);
|
|
118
|
+
|
|
119
|
+
const selectedKeys = createMemo<Selection>(() => {
|
|
120
|
+
const p = getProps();
|
|
121
|
+
if (p.selectedKeys !== undefined) {
|
|
122
|
+
return convertSelection(p.selectedKeys, new SelectionClass());
|
|
123
|
+
}
|
|
124
|
+
return uncontrolledSelectedKeys();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Selection behavior is seeded from the prop but kept in internal state so
|
|
128
|
+
// that `setSelectionBehavior` (e.g. the long-press-to-select touch flow) can
|
|
129
|
+
// override it. Two reactive rules mirror upstream's render-time logic.
|
|
130
|
+
const selectionBehaviorProp = (): SelectionBehavior => getProps().selectionBehavior ?? "toggle";
|
|
131
|
+
|
|
132
|
+
const [selectionBehavior, setSelectionBehaviorState] =
|
|
133
|
+
createSignal<SelectionBehavior>(selectionBehaviorProp());
|
|
134
|
+
|
|
135
|
+
// If the selectionBehavior prop is 'replace' but the current state is 'toggle'
|
|
136
|
+
// (e.g. due to a long press to enter selection mode on touch) and the
|
|
137
|
+
// selection becomes empty, reset the behavior back to 'replace'.
|
|
138
|
+
createComputed(() => {
|
|
139
|
+
const keys = selectedKeys();
|
|
140
|
+
if (
|
|
141
|
+
selectionBehaviorProp() === "replace" &&
|
|
142
|
+
untrack(selectionBehavior) === "toggle" &&
|
|
143
|
+
typeof keys === "object" &&
|
|
144
|
+
keys.size === 0
|
|
145
|
+
) {
|
|
146
|
+
setSelectionBehaviorState("replace");
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// If the selectionBehavior prop changes, sync it into state.
|
|
151
|
+
let lastSelectionBehavior = selectionBehaviorProp();
|
|
152
|
+
createComputed(() => {
|
|
153
|
+
const prop = selectionBehaviorProp();
|
|
154
|
+
if (prop !== lastSelectionBehavior) {
|
|
155
|
+
setSelectionBehaviorState(prop);
|
|
156
|
+
lastSelectionBehavior = prop;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const disabledKeys = createMemo<Set<Key>>(() => {
|
|
161
|
+
const keys = getProps().disabledKeys;
|
|
162
|
+
return keys ? new Set<Key>(keys) : new Set<Key>();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
let lastSelectionEvent: Selection | null = null;
|
|
166
|
+
|
|
167
|
+
const emitSelectionEvent = (keys: Selection) => {
|
|
168
|
+
lastSelectionEvent = keys;
|
|
169
|
+
getProps().onSelectionChange?.(keys);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const setSelectedKeys = (keys: Selection) => {
|
|
173
|
+
const p = getProps();
|
|
174
|
+
const current = selectedKeys();
|
|
175
|
+
const shouldUpdate =
|
|
176
|
+
p.allowDuplicateSelectionEvents ||
|
|
177
|
+
keys === "all" ||
|
|
178
|
+
current === "all" ||
|
|
179
|
+
!equalSets(keys, current);
|
|
180
|
+
|
|
181
|
+
if (!shouldUpdate) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Uncontrolled: store internally. Controlled: only notify.
|
|
186
|
+
if (p.selectedKeys === undefined) {
|
|
187
|
+
setUncontrolledSelectedKeys(keys);
|
|
188
|
+
}
|
|
189
|
+
emitSelectionEvent(keys);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const emitDuplicateSelectionEvent = (keys?: Selection) => {
|
|
193
|
+
const eventKeys = keys ?? lastSelectionEvent;
|
|
194
|
+
if (eventKeys == null) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
emitSelectionEvent(eventKeys);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
get selectionMode() {
|
|
202
|
+
return getProps().selectionMode ?? "none";
|
|
203
|
+
},
|
|
204
|
+
get selectionBehavior() {
|
|
205
|
+
return selectionBehavior();
|
|
206
|
+
},
|
|
207
|
+
setSelectionBehavior(behavior: SelectionBehavior) {
|
|
208
|
+
setSelectionBehaviorState(behavior);
|
|
209
|
+
},
|
|
210
|
+
get disallowEmptySelection() {
|
|
211
|
+
return getProps().disallowEmptySelection ?? false;
|
|
212
|
+
},
|
|
213
|
+
get isFocused() {
|
|
214
|
+
return isFocused();
|
|
215
|
+
},
|
|
216
|
+
setFocused(f: boolean) {
|
|
217
|
+
setIsFocused(f);
|
|
218
|
+
},
|
|
219
|
+
get focusedKey() {
|
|
220
|
+
return focusedKey();
|
|
221
|
+
},
|
|
222
|
+
get childFocusStrategy() {
|
|
223
|
+
return childFocusStrategy();
|
|
224
|
+
},
|
|
225
|
+
setFocusedKey(key: Key | null, strategy: FocusStrategy = "first") {
|
|
226
|
+
setFocusedKeySignal(key);
|
|
227
|
+
setChildFocusStrategy(strategy);
|
|
228
|
+
},
|
|
229
|
+
get selectedKeys() {
|
|
230
|
+
return selectedKeys();
|
|
231
|
+
},
|
|
232
|
+
get lastSelectionEvent() {
|
|
233
|
+
return lastSelectionEvent;
|
|
234
|
+
},
|
|
235
|
+
setSelectedKeys,
|
|
236
|
+
emitDuplicateSelectionEvent,
|
|
237
|
+
get disabledKeys() {
|
|
238
|
+
return disabledKeys();
|
|
239
|
+
},
|
|
240
|
+
get disabledBehavior() {
|
|
241
|
+
return getProps().disabledBehavior ?? "all";
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Re-export the accessor type for convenience where a reactive read is wanted.
|
|
247
|
+
export type { Accessor };
|
package/src/selection/index.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TableCollection - A collection class for table data.
|
|
3
3
|
* Based on @react-stately/table/TableCollection.
|
|
4
|
+
*
|
|
5
|
+
* Supports a plain (flat) table and, when an `expandedKeys` option is supplied, a
|
|
6
|
+
* tree grid: `childRows` are walked recursively, each parent row keeps its full
|
|
7
|
+
* structure (cells followed by every child row), and only rows whose ancestors are
|
|
8
|
+
* all expanded are flattened into the navigable body. Mirrors the structure produced
|
|
9
|
+
* by `@react-stately/table`'s `generateTreeGridCollection`.
|
|
4
10
|
*/
|
|
5
11
|
|
|
6
12
|
import type { Key } from "../collections/types";
|
|
@@ -24,6 +30,10 @@ export class TableCollection<T = unknown> implements ITableCollection<T> {
|
|
|
24
30
|
private _keyMap: Map<Key, GridNode<T>> = new Map();
|
|
25
31
|
private _rowHeaderColumnKeys: Set<Key>;
|
|
26
32
|
private _size: number = 0;
|
|
33
|
+
/** Visible rows in DFS order. Drives size, navigation, iteration and the `rows` list. */
|
|
34
|
+
private _flattenedRows: GridNode<T>[] = [];
|
|
35
|
+
private _treeColumn: Key | null = null;
|
|
36
|
+
private _userColumnCount: number = 0;
|
|
27
37
|
|
|
28
38
|
constructor(options: TableCollectionOptions<T>) {
|
|
29
39
|
const {
|
|
@@ -33,8 +43,12 @@ export class TableCollection<T = unknown> implements ITableCollection<T> {
|
|
|
33
43
|
getTextValue,
|
|
34
44
|
showSelectionCheckboxes = false,
|
|
35
45
|
rowHeaderColumnKeys,
|
|
46
|
+
expandedKeys,
|
|
36
47
|
} = options;
|
|
37
48
|
|
|
49
|
+
// Passing `expandedKeys` (even an empty set) selects tree-grid mode.
|
|
50
|
+
const isTreeGrid = expandedKeys !== undefined;
|
|
51
|
+
|
|
38
52
|
this._columns = this.buildColumns(columns, showSelectionCheckboxes);
|
|
39
53
|
|
|
40
54
|
this._rowHeaderColumnKeys = rowHeaderColumnKeys ?? this.getDefaultRowHeaderColumnKeys();
|
|
@@ -55,8 +69,25 @@ export class TableCollection<T = unknown> implements ITableCollection<T> {
|
|
|
55
69
|
this._keyMap.set("header", this._head);
|
|
56
70
|
}
|
|
57
71
|
|
|
58
|
-
|
|
59
|
-
|
|
72
|
+
let bodyChildren: GridNode<T>[];
|
|
73
|
+
if (isTreeGrid) {
|
|
74
|
+
const built = this.buildTreeRows(rows, expandedKeys, getKey, getTextValue);
|
|
75
|
+
bodyChildren = built.topLevelRows;
|
|
76
|
+
this._flattenedRows = built.flattenedRows;
|
|
77
|
+
this._userColumnCount = this.countLeafColumns();
|
|
78
|
+
this._treeColumn =
|
|
79
|
+
options.treeColumn !== undefined
|
|
80
|
+
? options.treeColumn
|
|
81
|
+
: (this._rowHeaderColumnKeys.values().next().value ?? null);
|
|
82
|
+
} else {
|
|
83
|
+
const bodyRows = this.buildRows(rows, getKey, getTextValue);
|
|
84
|
+
bodyChildren = bodyRows;
|
|
85
|
+
this._flattenedRows = bodyRows;
|
|
86
|
+
this._userColumnCount = this.countLeafColumns();
|
|
87
|
+
this._treeColumn = null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this._size = this._flattenedRows.length;
|
|
60
91
|
|
|
61
92
|
this._body = {
|
|
62
93
|
type: "item" as GridNodeType,
|
|
@@ -64,13 +95,17 @@ export class TableCollection<T = unknown> implements ITableCollection<T> {
|
|
|
64
95
|
index: this._headerRows.length,
|
|
65
96
|
level: 0,
|
|
66
97
|
hasChildNodes: true,
|
|
67
|
-
|
|
98
|
+
// The body's children retain the full tree structure (top-level rows with their
|
|
99
|
+
// child rows nested inside) so sibling-based aria-posinset/aria-setsize work; the
|
|
100
|
+
// flattened visible list lives in `_flattenedRows`. For a flat table these are the
|
|
101
|
+
// same array.
|
|
102
|
+
childNodes: bodyChildren,
|
|
68
103
|
value: null,
|
|
69
104
|
textValue: "",
|
|
70
105
|
};
|
|
71
106
|
this._keyMap.set("body", this._body);
|
|
72
107
|
|
|
73
|
-
this._rows = [...this._headerRows, ...
|
|
108
|
+
this._rows = [...this._headerRows, ...this._flattenedRows];
|
|
74
109
|
}
|
|
75
110
|
|
|
76
111
|
private buildColumns(
|
|
@@ -158,6 +193,23 @@ export class TableCollection<T = unknown> implements ITableCollection<T> {
|
|
|
158
193
|
return new Set();
|
|
159
194
|
}
|
|
160
195
|
|
|
196
|
+
/** Counts the leaf columns supplied by the user (excludes the selection column). */
|
|
197
|
+
private countLeafColumns(): number {
|
|
198
|
+
let count = 0;
|
|
199
|
+
const visit = (col: GridNode<T>) => {
|
|
200
|
+
if (col.key === "__selection__") {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (col.childNodes.length > 0) {
|
|
204
|
+
col.childNodes.forEach(visit);
|
|
205
|
+
} else {
|
|
206
|
+
count++;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
this._columns.forEach(visit);
|
|
210
|
+
return count;
|
|
211
|
+
}
|
|
212
|
+
|
|
161
213
|
private buildHeaderRows(): GridNode<T>[] {
|
|
162
214
|
if (this._columns.length === 0) {
|
|
163
215
|
return [];
|
|
@@ -194,6 +246,47 @@ export class TableCollection<T = unknown> implements ITableCollection<T> {
|
|
|
194
246
|
return [headerRow];
|
|
195
247
|
}
|
|
196
248
|
|
|
249
|
+
/**
|
|
250
|
+
* Builds the cell nodes for one row. Does not register the cells in the key map;
|
|
251
|
+
* the caller registers cells once the owning row is known to be present.
|
|
252
|
+
*/
|
|
253
|
+
private buildCells(
|
|
254
|
+
rowKey: Key,
|
|
255
|
+
value: T,
|
|
256
|
+
rowLevel: number,
|
|
257
|
+
getTextValue?: (item: T, column: ColumnDefinition<T>) => string,
|
|
258
|
+
): GridNode<T>[] {
|
|
259
|
+
return this._columns.map((col, colIndex) => {
|
|
260
|
+
const cellKey = `${rowKey}-${col.key}`;
|
|
261
|
+
const columnDefinition =
|
|
262
|
+
(col.props?.columnDefinition as ColumnDefinition<T> | undefined) ??
|
|
263
|
+
({ key: col.key } as ColumnDefinition<T>);
|
|
264
|
+
const cellTextValue =
|
|
265
|
+
col.key === "__selection__"
|
|
266
|
+
? "Selection"
|
|
267
|
+
: getTextValue
|
|
268
|
+
? getTextValue(value, columnDefinition)
|
|
269
|
+
: String((value as Record<string, unknown>)?.[String(col.key)] ?? "");
|
|
270
|
+
|
|
271
|
+
const cell: GridNode<T> = {
|
|
272
|
+
type: this._rowHeaderColumnKeys.has(col.key)
|
|
273
|
+
? ("rowheader" as GridNodeType)
|
|
274
|
+
: ("cell" as GridNodeType),
|
|
275
|
+
key: cellKey,
|
|
276
|
+
index: colIndex,
|
|
277
|
+
column: colIndex,
|
|
278
|
+
level: rowLevel + 1,
|
|
279
|
+
hasChildNodes: false,
|
|
280
|
+
childNodes: [],
|
|
281
|
+
value,
|
|
282
|
+
textValue: cellTextValue,
|
|
283
|
+
parentKey: rowKey,
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
return cell;
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
197
290
|
private buildRows(
|
|
198
291
|
rows: RowDefinition<T>[] | T[],
|
|
199
292
|
getKey?: (item: T) => Key,
|
|
@@ -214,36 +307,8 @@ export class TableCollection<T = unknown> implements ITableCollection<T> {
|
|
|
214
307
|
? ((row as Record<string, unknown>).textValue as string)
|
|
215
308
|
: undefined;
|
|
216
309
|
|
|
217
|
-
const cells
|
|
218
|
-
|
|
219
|
-
const columnDefinition =
|
|
220
|
-
(col.props?.columnDefinition as ColumnDefinition<T> | undefined) ??
|
|
221
|
-
({ key: col.key } as ColumnDefinition<T>);
|
|
222
|
-
const cellTextValue =
|
|
223
|
-
col.key === "__selection__"
|
|
224
|
-
? "Selection"
|
|
225
|
-
: getTextValue
|
|
226
|
-
? getTextValue(value, columnDefinition)
|
|
227
|
-
: String((value as Record<string, unknown>)?.[String(col.key)] ?? "");
|
|
228
|
-
|
|
229
|
-
const cell: GridNode<T> = {
|
|
230
|
-
type: this._rowHeaderColumnKeys.has(col.key)
|
|
231
|
-
? ("rowheader" as GridNodeType)
|
|
232
|
-
: ("cell" as GridNodeType),
|
|
233
|
-
key: cellKey,
|
|
234
|
-
index: colIndex,
|
|
235
|
-
column: colIndex,
|
|
236
|
-
level: 1,
|
|
237
|
-
hasChildNodes: false,
|
|
238
|
-
childNodes: [],
|
|
239
|
-
value,
|
|
240
|
-
textValue: cellTextValue,
|
|
241
|
-
parentKey: key,
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
this._keyMap.set(cellKey, cell);
|
|
245
|
-
return cell;
|
|
246
|
-
});
|
|
310
|
+
const cells = this.buildCells(key, value as T, 0, getTextValue);
|
|
311
|
+
cells.forEach((cell) => this._keyMap.set(cell.key, cell));
|
|
247
312
|
|
|
248
313
|
const rowNode: GridNode<T> = {
|
|
249
314
|
type: "item" as GridNodeType,
|
|
@@ -264,6 +329,97 @@ export class TableCollection<T = unknown> implements ITableCollection<T> {
|
|
|
264
329
|
return result;
|
|
265
330
|
}
|
|
266
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Builds the tree-grid rows: every row keeps its full structure (cells then all child
|
|
334
|
+
* rows, expanded or not) while only the rows whose ancestors are all expanded are
|
|
335
|
+
* registered in the key map and pushed into the flattened body. Mirrors
|
|
336
|
+
* `generateTreeGridCollection` upstream.
|
|
337
|
+
*/
|
|
338
|
+
private buildTreeRows(
|
|
339
|
+
rows: RowDefinition<T>[] | T[],
|
|
340
|
+
expandedKeys: "all" | Set<Key>,
|
|
341
|
+
getKey?: (item: T) => Key,
|
|
342
|
+
getTextValue?: (item: T, column: ColumnDefinition<T>) => string,
|
|
343
|
+
): { topLevelRows: GridNode<T>[]; flattenedRows: GridNode<T>[] } {
|
|
344
|
+
const isExpanded = (key: Key): boolean => expandedKeys === "all" || expandedKeys.has(key);
|
|
345
|
+
|
|
346
|
+
// Build the full tree structure without touching the key map. `index` is the row's
|
|
347
|
+
// position among its same-level siblings, so aria-posinset/aria-setsize read directly.
|
|
348
|
+
const buildRowNode = (
|
|
349
|
+
row: RowDefinition<T> | T,
|
|
350
|
+
level: number,
|
|
351
|
+
parentKey: Key | null,
|
|
352
|
+
indexAmongSiblings: number,
|
|
353
|
+
): GridNode<T> => {
|
|
354
|
+
const isRowDef = this.isRowDefinition(row);
|
|
355
|
+
const key = isRowDef ? row.key : (getKey?.(row as T) ?? indexAmongSiblings);
|
|
356
|
+
const value = (isRowDef ? row.value : (row as T)) as T;
|
|
357
|
+
const textValue = isRowDef
|
|
358
|
+
? row.textValue
|
|
359
|
+
: typeof (row as Record<string, unknown>)?.textValue === "string"
|
|
360
|
+
? ((row as Record<string, unknown>).textValue as string)
|
|
361
|
+
: undefined;
|
|
362
|
+
|
|
363
|
+
const cells = this.buildCells(key, value, level, getTextValue);
|
|
364
|
+
|
|
365
|
+
const childRowDefs = isRowDef ? (row.childRows ?? []) : [];
|
|
366
|
+
const hasChildRows = childRowDefs.length > 0 || (isRowDef && !!row.hasChildRows);
|
|
367
|
+
|
|
368
|
+
const childRowNodes = childRowDefs.map((child, childIdx) =>
|
|
369
|
+
buildRowNode(child, level + 1, key, childIdx),
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
const childNodes = [...cells, ...childRowNodes];
|
|
373
|
+
const rowNode: GridNode<T> = {
|
|
374
|
+
type: "item" as GridNodeType,
|
|
375
|
+
key,
|
|
376
|
+
index: indexAmongSiblings,
|
|
377
|
+
level,
|
|
378
|
+
parentKey,
|
|
379
|
+
hasChildNodes: true,
|
|
380
|
+
childNodes,
|
|
381
|
+
value,
|
|
382
|
+
textValue: textValue ?? cells.map((c) => c.textValue).join(" "),
|
|
383
|
+
isExpandable: hasChildRows,
|
|
384
|
+
isExpanded: hasChildRows && isExpanded(key),
|
|
385
|
+
firstChildKey: childNodes[0]?.key ?? null,
|
|
386
|
+
lastChildKey: childNodes[childNodes.length - 1]?.key ?? null,
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
return rowNode;
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const topLevelRows = rows.map((row, i) => buildRowNode(row, 0, "body", i));
|
|
393
|
+
|
|
394
|
+
// Flatten the visible rows (DFS). A row is visited iff every ancestor is expanded;
|
|
395
|
+
// visiting a row registers it and its cells, then descends into its child rows only
|
|
396
|
+
// when the row itself is expanded.
|
|
397
|
+
const flattenedRows: GridNode<T>[] = [];
|
|
398
|
+
const rowIndexOffset = this._headerRows.length;
|
|
399
|
+
const visit = (node: GridNode<T>) => {
|
|
400
|
+
node.rowIndex = rowIndexOffset + flattenedRows.length;
|
|
401
|
+
flattenedRows.push(node);
|
|
402
|
+
this._keyMap.set(node.key, node);
|
|
403
|
+
for (const child of node.childNodes) {
|
|
404
|
+
if (child.type !== "item") {
|
|
405
|
+
this._keyMap.set(child.key, child);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (isExpanded(node.key)) {
|
|
409
|
+
for (const child of node.childNodes) {
|
|
410
|
+
if (child.type === "item") {
|
|
411
|
+
visit(child);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
for (const node of topLevelRows) {
|
|
417
|
+
visit(node);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return { topLevelRows, flattenedRows };
|
|
421
|
+
}
|
|
422
|
+
|
|
267
423
|
private isRowDefinition(row: unknown): row is RowDefinition<T> {
|
|
268
424
|
return typeof row === "object" && row !== null && "key" in row && "value" in row;
|
|
269
425
|
}
|
|
@@ -304,6 +460,21 @@ export class TableCollection<T = unknown> implements ITableCollection<T> {
|
|
|
304
460
|
return this._rowHeaderColumnKeys;
|
|
305
461
|
}
|
|
306
462
|
|
|
463
|
+
/** The key of the column that renders the tree-grid affordance, or `null` for a flat table. */
|
|
464
|
+
get treeColumn(): Key | null {
|
|
465
|
+
return this._treeColumn;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** The number of leaf columns provided by the user (excludes the selection column). */
|
|
469
|
+
get userColumnCount(): number {
|
|
470
|
+
return this._userColumnCount;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** The key map of all present nodes (visible rows, their cells, columns and the body). */
|
|
474
|
+
get keyMap(): Map<Key, GridNode<T>> {
|
|
475
|
+
return this._keyMap;
|
|
476
|
+
}
|
|
477
|
+
|
|
307
478
|
getKeys(): Iterable<Key> {
|
|
308
479
|
return this._keyMap.keys();
|
|
309
480
|
}
|
|
@@ -313,11 +484,10 @@ export class TableCollection<T = unknown> implements ITableCollection<T> {
|
|
|
313
484
|
}
|
|
314
485
|
|
|
315
486
|
at(index: number): GridNode<T> | null {
|
|
316
|
-
|
|
317
|
-
if (index < 0 || index >= bodyRows.length) {
|
|
487
|
+
if (index < 0 || index >= this._flattenedRows.length) {
|
|
318
488
|
return null;
|
|
319
489
|
}
|
|
320
|
-
return
|
|
490
|
+
return this._flattenedRows[index];
|
|
321
491
|
}
|
|
322
492
|
|
|
323
493
|
getChildren(key: Key): Iterable<GridNode<T>> {
|
|
@@ -336,12 +506,12 @@ export class TableCollection<T = unknown> implements ITableCollection<T> {
|
|
|
336
506
|
}
|
|
337
507
|
|
|
338
508
|
getFirstKey(): Key | null {
|
|
339
|
-
const bodyRows = this.
|
|
509
|
+
const bodyRows = this._flattenedRows;
|
|
340
510
|
return bodyRows.length > 0 ? bodyRows[0].key : null;
|
|
341
511
|
}
|
|
342
512
|
|
|
343
513
|
getLastKey(): Key | null {
|
|
344
|
-
const bodyRows = this.
|
|
514
|
+
const bodyRows = this._flattenedRows;
|
|
345
515
|
return bodyRows.length > 0 ? bodyRows[bodyRows.length - 1].key : null;
|
|
346
516
|
}
|
|
347
517
|
|
|
@@ -349,7 +519,7 @@ export class TableCollection<T = unknown> implements ITableCollection<T> {
|
|
|
349
519
|
const node = this._keyMap.get(key);
|
|
350
520
|
if (!node) return null;
|
|
351
521
|
|
|
352
|
-
const bodyRows = this.
|
|
522
|
+
const bodyRows = this._flattenedRows;
|
|
353
523
|
const index = bodyRows.findIndex((r) => r.key === key);
|
|
354
524
|
if (index > 0) {
|
|
355
525
|
return bodyRows[index - 1].key;
|
|
@@ -361,7 +531,7 @@ export class TableCollection<T = unknown> implements ITableCollection<T> {
|
|
|
361
531
|
const node = this._keyMap.get(key);
|
|
362
532
|
if (!node) return null;
|
|
363
533
|
|
|
364
|
-
const bodyRows = this.
|
|
534
|
+
const bodyRows = this._flattenedRows;
|
|
365
535
|
const index = bodyRows.findIndex((r) => r.key === key);
|
|
366
536
|
if (index >= 0 && index < bodyRows.length - 1) {
|
|
367
537
|
return bodyRows[index + 1].key;
|
|
@@ -370,8 +540,8 @@ export class TableCollection<T = unknown> implements ITableCollection<T> {
|
|
|
370
540
|
}
|
|
371
541
|
|
|
372
542
|
*[Symbol.iterator](): Iterator<GridNode<T>> {
|
|
373
|
-
// Only iterate body rows, not header rows
|
|
374
|
-
for (const row of this.
|
|
543
|
+
// Only iterate visible body rows, not header rows.
|
|
544
|
+
for (const row of this._flattenedRows) {
|
|
375
545
|
yield row;
|
|
376
546
|
}
|
|
377
547
|
}
|
|
@@ -33,6 +33,7 @@ export function createTableState<
|
|
|
33
33
|
const gridState = createGridState<T, C>(() => ({
|
|
34
34
|
collection: getOptions().collection,
|
|
35
35
|
disabledKeys: getOptions().disabledKeys,
|
|
36
|
+
disabledBehavior: getOptions().disabledBehavior,
|
|
36
37
|
focusMode: getOptions().focusMode,
|
|
37
38
|
selectionMode: getOptions().selectionMode,
|
|
38
39
|
selectionBehavior: getOptions().selectionBehavior,
|
|
@@ -83,6 +84,9 @@ export function createTableState<
|
|
|
83
84
|
get disabledKeys() {
|
|
84
85
|
return gridState.disabledKeys;
|
|
85
86
|
},
|
|
87
|
+
get disabledBehavior() {
|
|
88
|
+
return gridState.disabledBehavior;
|
|
89
|
+
},
|
|
86
90
|
get isKeyboardNavigationDisabled() {
|
|
87
91
|
return gridState.isKeyboardNavigationDisabled;
|
|
88
92
|
},
|
|
@@ -101,6 +105,9 @@ export function createTableState<
|
|
|
101
105
|
get selectionBehavior() {
|
|
102
106
|
return gridState.selectionBehavior;
|
|
103
107
|
},
|
|
108
|
+
get disallowEmptySelection() {
|
|
109
|
+
return gridState.disallowEmptySelection;
|
|
110
|
+
},
|
|
104
111
|
get selectedKeys() {
|
|
105
112
|
return gridState.selectedKeys;
|
|
106
113
|
},
|
|
@@ -112,6 +119,8 @@ export function createTableState<
|
|
|
112
119
|
setFocused: gridState.setFocused,
|
|
113
120
|
toggleSelection: gridState.toggleSelection,
|
|
114
121
|
replaceSelection: gridState.replaceSelection,
|
|
122
|
+
setSelectionBehavior: gridState.setSelectionBehavior,
|
|
123
|
+
setSelectedKeys: gridState.setSelectedKeys,
|
|
115
124
|
extendSelection: gridState.extendSelection,
|
|
116
125
|
selectAll: gridState.selectAll,
|
|
117
126
|
clearSelection: gridState.clearSelection,
|
|
@@ -126,5 +135,16 @@ export function createTableState<
|
|
|
126
135
|
return sortDescriptor();
|
|
127
136
|
},
|
|
128
137
|
sort,
|
|
138
|
+
|
|
139
|
+
// A plain table has no expandable rows.
|
|
140
|
+
get expandedKeys(): "all" | Set<Key> {
|
|
141
|
+
return EMPTY_EXPANDED_KEYS;
|
|
142
|
+
},
|
|
143
|
+
get treeColumn(): Key | null {
|
|
144
|
+
return getOptions().collection.treeColumn ?? null;
|
|
145
|
+
},
|
|
146
|
+
toggleKey: () => {},
|
|
129
147
|
};
|
|
130
148
|
}
|
|
149
|
+
|
|
150
|
+
const EMPTY_EXPANDED_KEYS: Set<Key> = new Set();
|