@octane-xplat/table 0.0.1 → 0.10.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/LICENSE +21 -0
- package/README.md +427 -3
- package/dist/native/ColumnDrag.tsrx.js +237 -0
- package/dist/native/DataGrid.tsrx.js +957 -0
- package/dist/native/GridControls.tsrx.js +424 -0
- package/dist/native/grid-accessibility.js +12 -0
- package/dist/native/grid-layout.js +43 -0
- package/dist/native/index.js +4 -0
- package/dist/native/octane-client-build.json +8 -0
- package/dist/native/table.js +56 -0
- package/dist/native/useTable.tsrx.js +24 -0
- package/dist/web/ColumnDrag.tsrx.js +85 -0
- package/dist/web/DataGrid.tsrx.js +242 -0
- package/dist/web/GridControls.tsrx.js +114 -0
- package/dist/web/grid-accessibility.js +12 -0
- package/dist/web/grid-layout.js +43 -0
- package/dist/web/index.js +4 -0
- package/dist/web/octane-client-build.json +8 -0
- package/dist/web/table.js +56 -0
- package/dist/web/useTable.tsrx.js +26 -0
- package/package.json +75 -3
- package/src/ColumnDrag.tsrx +123 -0
- package/src/DataGrid.mobile.test.tsrx +55 -0
- package/src/DataGrid.tsrx +350 -0
- package/src/DataGrid.web.test.tsrx +277 -0
- package/src/GridControls.tsrx +157 -0
- package/src/grid-accessibility.ts +14 -0
- package/src/grid-layout.test.ts +32 -0
- package/src/grid-layout.ts +52 -0
- package/src/index.macos.ts +8 -0
- package/src/index.ts +7 -0
- package/src/index.web.ts +8 -0
- package/src/props.ts +122 -0
- package/src/table.test.ts +114 -0
- package/src/table.ts +91 -0
- package/src/useTable.tsrx +34 -0
- package/types/index.d.ts +20 -0
- package/types/index.macos.d.ts +17 -0
- package/types/props.d.ts +96 -0
- package/types/table.d.ts +83 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alec Larson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,4 +1,428 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@octane-xplat/table`
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
```sh
|
|
4
|
+
pnpm add @octane-xplat/table
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Headless data grid for Octane xplat apps. The row model — sorting, filtering,
|
|
8
|
+
grouping, expanding, pagination, row selection, column ordering/visibility/
|
|
9
|
+
sizing — is `@tanstack/table-core` v9 (framework-agnostic, DOM-free), bound to
|
|
10
|
+
component rendering through TanStack Store atoms and `useSyncExternalStore`.
|
|
11
|
+
Rendering goes through shared ui primitives (`VirtualList` windowing on every
|
|
12
|
+
target), so one column/state contract covers web, iOS/Android, and macOS.
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { DataGrid } from '@octane-xplat/table'
|
|
16
|
+
|
|
17
|
+
const rows = [{ name: 'Passport', count: 1 }]
|
|
18
|
+
export function PackingGrid() {
|
|
19
|
+
return (
|
|
20
|
+
<DataGrid
|
|
21
|
+
className="packing-grid"
|
|
22
|
+
data={rows}
|
|
23
|
+
columns={[
|
|
24
|
+
{ accessorKey: 'name', header: 'Name' },
|
|
25
|
+
{ accessorKey: 'count', header: 'Count', size: 120 },
|
|
26
|
+
]}
|
|
27
|
+
onRowPress={(row) => row.toggleSelected()}
|
|
28
|
+
/>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`DataGrid` is the default presentation: `vx-table-*` chrome (shared with the
|
|
34
|
+
bounded `Table` primitive), header rows from `table.getHeaderGroups()`,
|
|
35
|
+
virtualized body rows. Sortable headers toggle on press. The header and enabled
|
|
36
|
+
controls sit outside the body viewport, so they remain in place while rows scroll.
|
|
37
|
+
Give the grid a bounded height or a bounded flex parent.
|
|
38
|
+
|
|
39
|
+
```css
|
|
40
|
+
/* App stylesheet; keep the body viewport bounded. */
|
|
41
|
+
.packing-grid {
|
|
42
|
+
height: 400px;
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
For custom rendering keep the headless half and draw your own rows:
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import { useTable } from '@octane-xplat/table'
|
|
50
|
+
import { Text } from '@octane-xplat/ui'
|
|
51
|
+
|
|
52
|
+
export function CustomRows() {
|
|
53
|
+
const { table, state } = useTable({
|
|
54
|
+
data: [{ name: 'Passport' }],
|
|
55
|
+
columns: [{ accessorKey: 'name', header: 'Name' }],
|
|
56
|
+
})
|
|
57
|
+
return (
|
|
58
|
+
<Text>
|
|
59
|
+
{table.getRowModel().rows.length} rows; {state.sorting.length} sorts
|
|
60
|
+
</Text>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`createTable` (no hooks) constructs the same instance for headless use.
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { createTable } from '@octane-xplat/table'
|
|
69
|
+
|
|
70
|
+
const table = createTable({
|
|
71
|
+
data: [{ name: 'Passport' }],
|
|
72
|
+
columns: [{ accessorKey: 'name', header: 'Name' }],
|
|
73
|
+
})
|
|
74
|
+
console.log(table.getRowModel().rows.length)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Enable interaction controls
|
|
78
|
+
|
|
79
|
+
All controls are opt-in. No UI dependency is added; the leaf uses shared Xplat
|
|
80
|
+
primitives and the existing table-core state.
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
import { DataGrid } from '@octane-xplat/table'
|
|
84
|
+
import { Text } from '@octane-xplat/ui'
|
|
85
|
+
|
|
86
|
+
const orders = [{ id: 'order-1', name: 'Passport case', total: 12 }]
|
|
87
|
+
|
|
88
|
+
export function Orders() {
|
|
89
|
+
return (
|
|
90
|
+
<DataGrid
|
|
91
|
+
id="orders"
|
|
92
|
+
className="orders-grid"
|
|
93
|
+
data={orders}
|
|
94
|
+
getRowId={(order) => order.id}
|
|
95
|
+
columns={[
|
|
96
|
+
{
|
|
97
|
+
accessorKey: 'name',
|
|
98
|
+
header: 'Name',
|
|
99
|
+
size: 180,
|
|
100
|
+
minSize: 100,
|
|
101
|
+
maxSize: 300,
|
|
102
|
+
enableHiding: false,
|
|
103
|
+
},
|
|
104
|
+
{ accessorKey: 'total', header: 'Total', size: 120 },
|
|
105
|
+
]}
|
|
106
|
+
showGlobalFilter
|
|
107
|
+
showColumnFilters
|
|
108
|
+
showPagination
|
|
109
|
+
showRowSelection
|
|
110
|
+
showColumnSettings
|
|
111
|
+
resizableColumns
|
|
112
|
+
reorderableColumns
|
|
113
|
+
initialState={{ pagination: { pageIndex: 0, pageSize: 10 } }}
|
|
114
|
+
renderEmpty={() => <Text>No matching orders</Text>}
|
|
115
|
+
/>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```css
|
|
121
|
+
.orders-grid {
|
|
122
|
+
height: 600px;
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Import `Text` from `@octane-xplat/ui`. Start with your application's `orders`
|
|
127
|
+
array and immutable updates; IDs must remain stable through filtering, sorting,
|
|
128
|
+
pages, and data replacement. The [maintained example](examples/interactive.tsrx)
|
|
129
|
+
includes concrete rows and a nonvisual probe.
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
// Replace the data array when an order changes; preserve each ID.
|
|
133
|
+
const updatedOrders = orders.map((order) => ({ ...order, total: order.total + 1 }))
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
| Prop | Behavior |
|
|
137
|
+
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
138
|
+
| `showGlobalFilter` | Search across globally filterable columns. Changing the query resets the page index to zero. |
|
|
139
|
+
| `showColumnFilters` | Text fields for filterable leaf headings. Default filter is `includesString`, including numeric cells; custom column `filterFn` or `options.defaultColumn.filterFn` takes precedence. Custom range/date filters need a custom presentation via `useTable`. |
|
|
140
|
+
| `showPagination` | Previous/next, page count, and page-size buttons. `pageSizeOptions` defaults to `[10,25,50]`; invalid/nonpositive/noninteger entries are ignored. Empty data shows page zero of zero. Changing page size resets to page zero. |
|
|
141
|
+
| `showRowSelection` | Checkboxes for selectable rows, current-page select-all, total selected-ID count, and clear-all. IDs selected on other pages or outside the current filter remain selected. |
|
|
142
|
+
| `getRowLabel` | Accessible row-checkbox label; falls back to the row ID. |
|
|
143
|
+
| `renderSelectionActions(table)` | Caller-composed bulk actions next to the count/clear action; render only when `showRowSelection` is enabled. |
|
|
144
|
+
| `showColumnSettings` | A Columns button opens an inline panel with visibility checkboxes, earlier/later move buttons, and reset. `enableHiding:false` protects a column. Reset restores initial visibility/order/sizing. |
|
|
145
|
+
| `resizableColumns` | Fixed-width columns, live pan resize, and narrow/widen buttons in 10-dip increments, clamped to `minSize`/`maxSize`. Cancel restores the column's previous sizing override. |
|
|
146
|
+
| `reorderableColumns` | Separate pan handles commit a leaf-column move on release after crossing an adjacent column midpoint. Cancel preserves order. Use settings move buttons for keyboard/touch access without dragging. |
|
|
147
|
+
| `direction` | `ltr` by default; `rtl` reverses horizontal gesture deltas. Match the direction to your surrounding layout; this prop does not itself mirror the layout. |
|
|
148
|
+
| `options` | Core options such as state callbacks/atoms, selection predicates, server/manual row-model options, and defaults. Explicit `data`, `columns`, `state`, `initialState`, `features`, and `getRowId` props take precedence. |
|
|
149
|
+
|
|
150
|
+
Selection never activates `onRowPress`; the activation surface is a separate
|
|
151
|
+
sibling of the checkbox. `options.enableRowSelection` can be a predicate to
|
|
152
|
+
block specific rows. Select-all means the **current row-model page**, not every
|
|
153
|
+
record in a server dataset. Clear removes off-page IDs too; the caller decides
|
|
154
|
+
whether to prune IDs when records disappear. A missing ID remains in the count
|
|
155
|
+
but has no loaded row in `table.getSelectedRowModel()`; use the selection state
|
|
156
|
+
for server-side bulk actions that include unloaded records.
|
|
157
|
+
|
|
158
|
+
Page selection ignores disabled rows. When some eligible rows are selected, the
|
|
159
|
+
page control shows a dash; activating it selects the remaining eligible rows.
|
|
160
|
+
An empty or entirely disabled page disables that control. Give `getRowLabel` a
|
|
161
|
+
human-readable name, such as `row => row.original.name`, so a checkbox says
|
|
162
|
+
“Select Alice” rather than “Select a”. Web checkboxes support Space and Enter.
|
|
163
|
+
|
|
164
|
+
### Row accessibility
|
|
165
|
+
|
|
166
|
+
The web presentation exposes a table with header/row/cell roles. Its row count
|
|
167
|
+
includes header rows plus the **filtered** data total. Body indices follow the
|
|
168
|
+
filtered, sorted order with the page offset, before VirtualList removes
|
|
169
|
+
unmounted rows. For example, page index 2 with page size 10 starts at data row 21
|
|
170
|
+
(`aria-rowindex=22` with one header row). Sorting changes positions, not row IDs.
|
|
171
|
+
Controls stay outside the table's semantic container.
|
|
172
|
+
|
|
173
|
+
Selection and activation controls carry a “Row 21 of 60” hint on native and web.
|
|
174
|
+
NativeScript has no mixed checkbox-state enum, so mixed selection uses the
|
|
175
|
+
spoken value “Partially selected” without claiming checked or unchecked.
|
|
176
|
+
AppKit exposes `AXCheckBox` with numeric off/on/mixed values (0/1/2) derived from
|
|
177
|
+
semantic state, independently of spoken text. Disabled controls reject actions.
|
|
178
|
+
These are host properties; assistive-technology navigation and speech still need
|
|
179
|
+
OS verification. This presentation does not implement spreadsheet-style arrow
|
|
180
|
+
navigation between cells.
|
|
181
|
+
|
|
182
|
+
```tsx
|
|
183
|
+
export function SelectableOrders() {
|
|
184
|
+
return (
|
|
185
|
+
<DataGrid
|
|
186
|
+
data={orders}
|
|
187
|
+
getRowId={(order) => order.id}
|
|
188
|
+
columns={[{ accessorKey: 'name', header: 'Name' }]}
|
|
189
|
+
showRowSelection
|
|
190
|
+
options={{ enableRowSelection: (row) => row.original.total > 0 }}
|
|
191
|
+
/>
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Widths are read from live `columnSizing` via `column.getSize()`. Enabling resize
|
|
197
|
+
or reorder makes all columns fixed-width so gesture distances and row/header
|
|
198
|
+
layout agree. Without these flags, unspecified widths retain the original
|
|
199
|
+
flexible distribution. Keep the visible columns within the grid viewport; this
|
|
200
|
+
version does not add a horizontally synchronized scroll owner.
|
|
201
|
+
|
|
202
|
+
```tsx
|
|
203
|
+
export function SizedOrders() {
|
|
204
|
+
return (
|
|
205
|
+
<DataGrid
|
|
206
|
+
data={orders}
|
|
207
|
+
columns={[
|
|
208
|
+
{ accessorKey: 'name', header: 'Name', size: 180, minSize: 100, maxSize: 300 },
|
|
209
|
+
{ accessorKey: 'total', header: 'Total', size: 100 },
|
|
210
|
+
]}
|
|
211
|
+
resizableColumns
|
|
212
|
+
reorderableColumns
|
|
213
|
+
/>
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Feature surface
|
|
219
|
+
|
|
220
|
+
`dataGridFeatures` (the default set) enables every stock feature whose
|
|
221
|
+
machinery is DOM-free — sorting, column/global filtering, grouping +
|
|
222
|
+
aggregation, expanding, pagination, pinning, selection, column
|
|
223
|
+
ordering/visibility/sizing — plus the row-model factories and builtin
|
|
224
|
+
`sortFns`/`filterFns`/`aggregationFns` registries, which v9 keeps as separate
|
|
225
|
+
slots.
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
import { createTable, dataGridFeatures } from '@octane-xplat/table'
|
|
229
|
+
|
|
230
|
+
const table = createTable({
|
|
231
|
+
features: dataGridFeatures,
|
|
232
|
+
data: orders,
|
|
233
|
+
columns: [{ accessorKey: 'name', header: 'Name' }],
|
|
234
|
+
})
|
|
235
|
+
table.setSorting([{ id: 'name', desc: false }])
|
|
236
|
+
console.log(table.getRowModel().rows)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Not in the default set:
|
|
240
|
+
|
|
241
|
+
- `columnResizingFeature` — its drag handler installs `window` listeners;
|
|
242
|
+
resize-gesture UI is a platform-leaf concern.
|
|
243
|
+
- `cellSelectionFeature` / `cellSpanningFeature` — range selection is
|
|
244
|
+
spreadsheet-shaped; revisit per platform.
|
|
245
|
+
|
|
246
|
+
Pass `features` to add slots (e.g.
|
|
247
|
+
`features: { columnResizingFeature }` on a web-only surface).
|
|
248
|
+
|
|
249
|
+
```tsx
|
|
250
|
+
// Grid.web.tsx — upstream's resizing feature is browser-only.
|
|
251
|
+
import { columnResizingFeature } from '@tanstack/table-core'
|
|
252
|
+
import { DataGrid } from '@octane-xplat/table'
|
|
253
|
+
|
|
254
|
+
export function WebGrid() {
|
|
255
|
+
return (
|
|
256
|
+
<DataGrid
|
|
257
|
+
data={[{ name: 'Passport' }]}
|
|
258
|
+
columns={[{ accessorKey: 'name', header: 'Name' }]}
|
|
259
|
+
features={{ columnResizingFeature }}
|
|
260
|
+
/>
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Controlled state
|
|
266
|
+
|
|
267
|
+
State slices live in `table.baseAtoms` (internal) with `table.atoms` as the
|
|
268
|
+
derived readonly view; `table.store` is the flat `TableState` store the
|
|
269
|
+
adapter subscribes to. For a custom `useTable` presentation, pass `state` plus `atoms` (external writable
|
|
270
|
+
atoms) or the corresponding change callbacks. DataGrid now exposes those through
|
|
271
|
+
`options`. A `state` snapshot alone freezes that slice; subscribing alone does
|
|
272
|
+
not make a read-only snapshot writable.
|
|
273
|
+
|
|
274
|
+
```tsx
|
|
275
|
+
import { useState } from 'octane'
|
|
276
|
+
import type { DataGridFeatures, TableState } from '@octane-xplat/table'
|
|
277
|
+
|
|
278
|
+
// useTable and Text are imported in CustomRows above. Call within a component.
|
|
279
|
+
export function ControlledRows() {
|
|
280
|
+
const [sorting, setSorting] = useState<TableState<DataGridFeatures>['sorting']>([])
|
|
281
|
+
const { table } = useTable({
|
|
282
|
+
data: orders,
|
|
283
|
+
columns: [{ accessorKey: 'name', header: 'Name' }],
|
|
284
|
+
state: { sorting },
|
|
285
|
+
onSortingChange: setSorting,
|
|
286
|
+
})
|
|
287
|
+
return <Text>{table.getRowModel().rows.length} rows</Text>
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
For example, a caller-owned sorting slice uses core updater callbacks:
|
|
292
|
+
|
|
293
|
+
```tsx
|
|
294
|
+
import { useState } from 'octane'
|
|
295
|
+
import { DataGrid, type DataGridFeatures, type TableState } from '@octane-xplat/table'
|
|
296
|
+
|
|
297
|
+
export function SortedOrders() {
|
|
298
|
+
const [sorting, setSorting] = useState<TableState<DataGridFeatures>['sorting']>([])
|
|
299
|
+
return (
|
|
300
|
+
<DataGrid
|
|
301
|
+
data={orders}
|
|
302
|
+
columns={[{ accessorKey: 'name', header: 'Name' }]}
|
|
303
|
+
state={{ sorting }}
|
|
304
|
+
options={{ onSortingChange: setSorting }}
|
|
305
|
+
/>
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Import `useState` from `octane`; the explicit state type keeps an empty initial
|
|
311
|
+
array from becoming `never[]`.
|
|
312
|
+
|
|
313
|
+
```tsx
|
|
314
|
+
import { useState } from 'octane'
|
|
315
|
+
import type { DataGridFeatures, TableState } from '@octane-xplat/table'
|
|
316
|
+
|
|
317
|
+
export function SortCount() {
|
|
318
|
+
const [sorting] = useState<TableState<DataGridFeatures>['sorting']>([])
|
|
319
|
+
return <Text>{sorting.length}</Text>
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Selection uses the same ownership pattern. The callback receives either a new
|
|
324
|
+
value or a function that updates the previous value; `useState` accepts both:
|
|
325
|
+
|
|
326
|
+
```tsx
|
|
327
|
+
const [rowSelection, setRowSelection] =
|
|
328
|
+
useState<TableState<DataGridFeatures>['rowSelection']>({})
|
|
329
|
+
<DataGrid data={rows} columns={columns} getRowId={row => row.id}
|
|
330
|
+
showRowSelection getRowLabel={row => row.original.name}
|
|
331
|
+
state={{ rowSelection }} options={{ onRowSelectionChange: setRowSelection }} />
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Click a checkbox and the selected count should update. Filtering, changing pages,
|
|
335
|
+
and replacing data keep selection by ID. Clear selection also removes IDs for
|
|
336
|
+
records that are currently unloaded. The [interactive example](examples/interactive.tsrx)
|
|
337
|
+
uses this caller-owned selection. A snapshot without an update callback remains
|
|
338
|
+
read-only; DataGrid does not silently take ownership.
|
|
339
|
+
|
|
340
|
+
For server pages, supply already paged `data`, `state.pagination`, and
|
|
341
|
+
`options={{ manualPagination: true, rowCount, onPaginationChange }}`. The callback
|
|
342
|
+
receives the core updater and must publish the new controlled state plus fetch
|
|
343
|
+
that page. Unknown totals use `pageCount: -1`; next stays enabled, so the caller
|
|
344
|
+
must own terminal/cursor behavior. Supply `rowCount` for accessible server totals;
|
|
345
|
+
without it, the table reports an unknown row count (`aria-rowcount=-1`) and hints
|
|
346
|
+
say “Row 21” without a total. `pageCount` alone does not determine the exact final
|
|
347
|
+
row count. Filtering and sorting of manual server data are also caller-owned;
|
|
348
|
+
set the corresponding core manual options to avoid transforming it twice.
|
|
349
|
+
This is not an automatic server fetcher.
|
|
350
|
+
|
|
351
|
+
```tsx
|
|
352
|
+
import { useState } from 'octane'
|
|
353
|
+
import { DataGrid, type DataGridFeatures, type TableState } from '@octane-xplat/table'
|
|
354
|
+
|
|
355
|
+
// The parent fetches pageRows whenever pagination changes.
|
|
356
|
+
export function ServerPage({
|
|
357
|
+
pageRows,
|
|
358
|
+
rowCount,
|
|
359
|
+
requestPage,
|
|
360
|
+
}: {
|
|
361
|
+
pageRows: { id: string; name: string }[]
|
|
362
|
+
rowCount: number
|
|
363
|
+
requestPage: (pageIndex: number, pageSize: number) => void
|
|
364
|
+
}) {
|
|
365
|
+
const [pagination, setPagination] = useState<TableState<DataGridFeatures>['pagination']>({
|
|
366
|
+
pageIndex: 0,
|
|
367
|
+
pageSize: 10,
|
|
368
|
+
})
|
|
369
|
+
return (
|
|
370
|
+
<DataGrid
|
|
371
|
+
data={pageRows}
|
|
372
|
+
columns={[{ accessorKey: 'name', header: 'Name' }]}
|
|
373
|
+
showPagination
|
|
374
|
+
state={{ pagination }}
|
|
375
|
+
options={{
|
|
376
|
+
manualPagination: true,
|
|
377
|
+
rowCount,
|
|
378
|
+
onPaginationChange: (updater) => {
|
|
379
|
+
const next = typeof updater === 'function' ? updater(pagination) : updater
|
|
380
|
+
setPagination(next)
|
|
381
|
+
requestPage(next.pageIndex, next.pageSize)
|
|
382
|
+
},
|
|
383
|
+
}}
|
|
384
|
+
/>
|
|
385
|
+
)
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
## Boundaries
|
|
390
|
+
|
|
391
|
+
- Vertical virtualization only (VirtualList); no horizontal column
|
|
392
|
+
virtualization or sticky column-pinning layout. Headers stay outside vertical
|
|
393
|
+
body scrolling; that does not pin them against an outer page scroll.
|
|
394
|
+
- `Table` in `@octane-xplat/ui` stays the bounded, unvirtualized
|
|
395
|
+
presentational tier — DataGrid is the data-grid tier above it.
|
|
396
|
+
|
|
397
|
+
## Verification and remaining parity gaps
|
|
398
|
+
|
|
399
|
+
Run `pnpm --filter @octane-xplat/table typecheck`, `test`, `test:native`, `build`, and
|
|
400
|
+
`test:packed`. The source typecheck covers web and the iOS native resolution lane;
|
|
401
|
+
the native build uses the same shared controls for iOS/Android. Run the fixture:
|
|
402
|
+
|
|
403
|
+
```sh
|
|
404
|
+
pnpm probe run packages/table/examples/interactive.tsrx --target web --deps @octane-xplat/table
|
|
405
|
+
pnpm probe run packages/table/examples/interactive.tsrx --target ios --device YOUR_BOOTED_SIMULATOR_UDID --deps @octane-xplat/table
|
|
406
|
+
pnpm probe run packages/table/examples/geometry.tsrx --target macos --deps @octane-xplat/table
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
The geometry fixture isolates bounded row sorting and column resize/reorder from
|
|
410
|
+
filter/pager input. Its AppKit run measures native frames and retained row order.
|
|
411
|
+
The iOS and Android selection fixtures currently hit an unrelated Unicode
|
|
412
|
+
property-regex parse failure before assertions; the direct VirtualList iOS geometry case passes. The full
|
|
413
|
+
AppKit fixture has an unresolved filter-input timeout.
|
|
414
|
+
|
|
415
|
+
Use a real booted simulator ID from `pnpm probe doctor`. Results are handler
|
|
416
|
+
or pointer dispatch, not proof of OS input, hit-testing, or screen-reader
|
|
417
|
+
navigation. See the [audit implementation record](../../docs/notes/astryx-parity-table.md#implementation-follow-through)
|
|
418
|
+
for the actual targets/results and known probe blockers.
|
|
419
|
+
|
|
420
|
+
Still deferred: grouped section headers/detail/tree controls, frozen-column
|
|
421
|
+
layout, PowerSearch operator/date/multi-select filter UI, richer multi-sort UX,
|
|
422
|
+
column virtualization, cell editing, and OS screen-reader/input verification.
|
|
423
|
+
The maintained [selection fixture](examples/selection.tsrx) checks the real
|
|
424
|
+
VirtualList, caller state, eligible page scope, mixed controls, and a windowed
|
|
425
|
+
row ordinal. Chromium and AppKit pass its handler-dispatch and host-property
|
|
426
|
+
checks; iOS/Android object-driver checks cover shared controls but are not
|
|
427
|
+
on-device accessibility evidence. These limits do not remove the
|
|
428
|
+
underlying headless features.
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { dragTarget, moveColumn, resizedWidth } from "./grid-layout.js";
|
|
2
|
+
import { defineUniversalComponent, hookSlots, universalChildren, universalComponent, universalPlan, universalProps, universalValue, useRef } from "@nativescript-community/octane";
|
|
3
|
+
import { HStack, Pressable, Text, View } from "@octane-xplat/ui";
|
|
4
|
+
//#region src/ColumnDrag.tsrx
|
|
5
|
+
var _h$0 = /* @__PURE__ */ Symbol(/* @__PURE__ */ hookSlots(1));
|
|
6
|
+
var __octaneUniversalPlan0 = universalPlan("nativescript", {
|
|
7
|
+
"kind": "text",
|
|
8
|
+
"value": "↔"
|
|
9
|
+
});
|
|
10
|
+
var __octaneUniversalPlan1 = universalPlan("nativescript", {
|
|
11
|
+
"kind": "slot",
|
|
12
|
+
"slot": 0
|
|
13
|
+
});
|
|
14
|
+
var __octaneUniversalPlan2 = universalPlan("nativescript", {
|
|
15
|
+
"kind": "slot",
|
|
16
|
+
"slot": 0
|
|
17
|
+
});
|
|
18
|
+
var __octaneUniversalPlan3 = universalPlan("nativescript", {
|
|
19
|
+
"kind": "text",
|
|
20
|
+
"value": "−"
|
|
21
|
+
});
|
|
22
|
+
var __octaneUniversalPlan4 = universalPlan("nativescript", {
|
|
23
|
+
"kind": "slot",
|
|
24
|
+
"slot": 0
|
|
25
|
+
});
|
|
26
|
+
var __octaneUniversalPlan5 = universalPlan("nativescript", {
|
|
27
|
+
"kind": "text",
|
|
28
|
+
"value": "│"
|
|
29
|
+
});
|
|
30
|
+
var __octaneUniversalPlan6 = universalPlan("nativescript", {
|
|
31
|
+
"kind": "slot",
|
|
32
|
+
"slot": 0
|
|
33
|
+
});
|
|
34
|
+
var __octaneUniversalPlan7 = universalPlan("nativescript", {
|
|
35
|
+
"kind": "text",
|
|
36
|
+
"value": "+"
|
|
37
|
+
});
|
|
38
|
+
var __octaneUniversalPlan8 = universalPlan("nativescript", {
|
|
39
|
+
"kind": "slot",
|
|
40
|
+
"slot": 0
|
|
41
|
+
});
|
|
42
|
+
var __octaneUniversalPlan9 = universalPlan("nativescript", {
|
|
43
|
+
"kind": "range",
|
|
44
|
+
"children": [
|
|
45
|
+
{
|
|
46
|
+
"kind": "slot",
|
|
47
|
+
"slot": 0
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"kind": "slot",
|
|
51
|
+
"slot": 1
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"kind": "slot",
|
|
55
|
+
"slot": 2
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
});
|
|
59
|
+
var __octaneUniversalPlan10 = universalPlan("nativescript", {
|
|
60
|
+
"kind": "range",
|
|
61
|
+
"children": [{
|
|
62
|
+
"kind": "slot",
|
|
63
|
+
"slot": 0
|
|
64
|
+
}, {
|
|
65
|
+
"kind": "slot",
|
|
66
|
+
"slot": 1
|
|
67
|
+
}]
|
|
68
|
+
});
|
|
69
|
+
var __octaneUniversalPlan11 = universalPlan("nativescript", {
|
|
70
|
+
"kind": "slot",
|
|
71
|
+
"slot": 0
|
|
72
|
+
});
|
|
73
|
+
var ColumnDrag = defineUniversalComponent("nativescript", function ColumnDrag(props) {
|
|
74
|
+
props.state;
|
|
75
|
+
const drag = useRef(null, _h$0);
|
|
76
|
+
const begin = () => {
|
|
77
|
+
drag.current = {
|
|
78
|
+
size: props.column.getSize(),
|
|
79
|
+
sizing: { ...props.table.store.get().columnSizing },
|
|
80
|
+
order: props.table.getAllLeafColumns().map((column) => column.id),
|
|
81
|
+
columns: props.table.getVisibleLeafColumns().map((column) => {
|
|
82
|
+
const size = column.getSize();
|
|
83
|
+
return {
|
|
84
|
+
id: column.id,
|
|
85
|
+
getSize: () => size
|
|
86
|
+
};
|
|
87
|
+
})
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
const onPan = (event, resize) => {
|
|
91
|
+
if (event.state === "began") {
|
|
92
|
+
begin();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const start = drag.current;
|
|
96
|
+
if (!start) return;
|
|
97
|
+
if (event.state === "cancelled") {
|
|
98
|
+
if (resize) props.table.setColumnSizing((old) => {
|
|
99
|
+
const next = { ...old };
|
|
100
|
+
if (start.sizing[props.column.id] == null) delete next[props.column.id];
|
|
101
|
+
else next[props.column.id] = start.sizing[props.column.id];
|
|
102
|
+
return next;
|
|
103
|
+
});
|
|
104
|
+
drag.current = null;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const dx = event.dx * (props.direction === "rtl" ? -1 : 1);
|
|
108
|
+
if (resize && Number.isFinite(dx)) props.table.setColumnSizing((old) => ({
|
|
109
|
+
...old,
|
|
110
|
+
[props.column.id]: resizedWidth(props.column, start.size + dx)
|
|
111
|
+
}));
|
|
112
|
+
if (event.state === "ended") {
|
|
113
|
+
if (!resize) {
|
|
114
|
+
const target = dragTarget(start.columns, props.column.id, dx);
|
|
115
|
+
props.table.setColumnOrder(moveColumn(start.order, props.column.id, target));
|
|
116
|
+
}
|
|
117
|
+
drag.current = null;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const adjust = (delta) => props.table.setColumnSizing((old) => ({
|
|
121
|
+
...old,
|
|
122
|
+
[props.column.id]: resizedWidth(props.column, props.column.getSize() + delta)
|
|
123
|
+
}));
|
|
124
|
+
return universalValue(__octaneUniversalPlan11, [universalComponent("nativescript", HStack, universalProps([[
|
|
125
|
+
"set",
|
|
126
|
+
"className",
|
|
127
|
+
"vx-datagrid-handles"
|
|
128
|
+
]], universalChildren("nativescript", () => {
|
|
129
|
+
return universalValue(__octaneUniversalPlan10, [props.reorder ? universalValue(__octaneUniversalPlan2, [universalComponent("nativescript", View, universalProps([
|
|
130
|
+
[
|
|
131
|
+
"set",
|
|
132
|
+
"id",
|
|
133
|
+
props.id ? `${props.id}-drag` : void 0
|
|
134
|
+
],
|
|
135
|
+
[
|
|
136
|
+
"set",
|
|
137
|
+
"className",
|
|
138
|
+
"vx-datagrid-handle"
|
|
139
|
+
],
|
|
140
|
+
[
|
|
141
|
+
"set",
|
|
142
|
+
"onPan",
|
|
143
|
+
(event) => onPan(event, false)
|
|
144
|
+
],
|
|
145
|
+
[
|
|
146
|
+
"set",
|
|
147
|
+
"accessibilityLabel",
|
|
148
|
+
`Drag ${props.column.id} to reorder`
|
|
149
|
+
]
|
|
150
|
+
], universalChildren("nativescript", () => {
|
|
151
|
+
return universalValue(__octaneUniversalPlan1, [universalComponent("nativescript", Text, universalProps([], universalChildren("nativescript", () => {
|
|
152
|
+
return universalValue(__octaneUniversalPlan0, []);
|
|
153
|
+
})))]);
|
|
154
|
+
})))]) : null, props.resize ? universalValue(__octaneUniversalPlan9, [
|
|
155
|
+
universalComponent("nativescript", Pressable, universalProps([
|
|
156
|
+
[
|
|
157
|
+
"set",
|
|
158
|
+
"id",
|
|
159
|
+
props.id ? `${props.id}-narrow` : void 0
|
|
160
|
+
],
|
|
161
|
+
[
|
|
162
|
+
"set",
|
|
163
|
+
"className",
|
|
164
|
+
"vx-datagrid-handle"
|
|
165
|
+
],
|
|
166
|
+
[
|
|
167
|
+
"set",
|
|
168
|
+
"onPress",
|
|
169
|
+
() => adjust(-10)
|
|
170
|
+
],
|
|
171
|
+
[
|
|
172
|
+
"set",
|
|
173
|
+
"accessibilityLabel",
|
|
174
|
+
`Narrow ${props.column.id}`
|
|
175
|
+
]
|
|
176
|
+
], universalChildren("nativescript", () => {
|
|
177
|
+
return universalValue(__octaneUniversalPlan4, [universalComponent("nativescript", Text, universalProps([], universalChildren("nativescript", () => {
|
|
178
|
+
return universalValue(__octaneUniversalPlan3, []);
|
|
179
|
+
})))]);
|
|
180
|
+
}))),
|
|
181
|
+
universalComponent("nativescript", View, universalProps([
|
|
182
|
+
[
|
|
183
|
+
"set",
|
|
184
|
+
"id",
|
|
185
|
+
props.id ? `${props.id}-resize` : void 0
|
|
186
|
+
],
|
|
187
|
+
[
|
|
188
|
+
"set",
|
|
189
|
+
"className",
|
|
190
|
+
"vx-datagrid-handle"
|
|
191
|
+
],
|
|
192
|
+
[
|
|
193
|
+
"set",
|
|
194
|
+
"onPan",
|
|
195
|
+
(event) => onPan(event, true)
|
|
196
|
+
],
|
|
197
|
+
[
|
|
198
|
+
"set",
|
|
199
|
+
"accessibilityLabel",
|
|
200
|
+
`Drag to resize ${props.column.id}`
|
|
201
|
+
]
|
|
202
|
+
], universalChildren("nativescript", () => {
|
|
203
|
+
return universalValue(__octaneUniversalPlan6, [universalComponent("nativescript", Text, universalProps([], universalChildren("nativescript", () => {
|
|
204
|
+
return universalValue(__octaneUniversalPlan5, []);
|
|
205
|
+
})))]);
|
|
206
|
+
}))),
|
|
207
|
+
universalComponent("nativescript", Pressable, universalProps([
|
|
208
|
+
[
|
|
209
|
+
"set",
|
|
210
|
+
"id",
|
|
211
|
+
props.id ? `${props.id}-widen` : void 0
|
|
212
|
+
],
|
|
213
|
+
[
|
|
214
|
+
"set",
|
|
215
|
+
"className",
|
|
216
|
+
"vx-datagrid-handle"
|
|
217
|
+
],
|
|
218
|
+
[
|
|
219
|
+
"set",
|
|
220
|
+
"onPress",
|
|
221
|
+
() => adjust(10)
|
|
222
|
+
],
|
|
223
|
+
[
|
|
224
|
+
"set",
|
|
225
|
+
"accessibilityLabel",
|
|
226
|
+
`Widen ${props.column.id}`
|
|
227
|
+
]
|
|
228
|
+
], universalChildren("nativescript", () => {
|
|
229
|
+
return universalValue(__octaneUniversalPlan8, [universalComponent("nativescript", Text, universalProps([], universalChildren("nativescript", () => {
|
|
230
|
+
return universalValue(__octaneUniversalPlan7, []);
|
|
231
|
+
})))]);
|
|
232
|
+
})))
|
|
233
|
+
]) : null]);
|
|
234
|
+
})))]);
|
|
235
|
+
}, { "module": "@nativescript-community/octane" });
|
|
236
|
+
//#endregion
|
|
237
|
+
export { ColumnDrag };
|