@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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +427 -3
  3. package/dist/native/ColumnDrag.tsrx.js +237 -0
  4. package/dist/native/DataGrid.tsrx.js +957 -0
  5. package/dist/native/GridControls.tsrx.js +424 -0
  6. package/dist/native/grid-accessibility.js +12 -0
  7. package/dist/native/grid-layout.js +43 -0
  8. package/dist/native/index.js +4 -0
  9. package/dist/native/octane-client-build.json +8 -0
  10. package/dist/native/table.js +56 -0
  11. package/dist/native/useTable.tsrx.js +24 -0
  12. package/dist/web/ColumnDrag.tsrx.js +85 -0
  13. package/dist/web/DataGrid.tsrx.js +242 -0
  14. package/dist/web/GridControls.tsrx.js +114 -0
  15. package/dist/web/grid-accessibility.js +12 -0
  16. package/dist/web/grid-layout.js +43 -0
  17. package/dist/web/index.js +4 -0
  18. package/dist/web/octane-client-build.json +8 -0
  19. package/dist/web/table.js +56 -0
  20. package/dist/web/useTable.tsrx.js +26 -0
  21. package/package.json +75 -3
  22. package/src/ColumnDrag.tsrx +123 -0
  23. package/src/DataGrid.mobile.test.tsrx +55 -0
  24. package/src/DataGrid.tsrx +350 -0
  25. package/src/DataGrid.web.test.tsrx +277 -0
  26. package/src/GridControls.tsrx +157 -0
  27. package/src/grid-accessibility.ts +14 -0
  28. package/src/grid-layout.test.ts +32 -0
  29. package/src/grid-layout.ts +52 -0
  30. package/src/index.macos.ts +8 -0
  31. package/src/index.ts +7 -0
  32. package/src/index.web.ts +8 -0
  33. package/src/props.ts +122 -0
  34. package/src/table.test.ts +114 -0
  35. package/src/table.ts +91 -0
  36. package/src/useTable.tsrx +34 -0
  37. package/types/index.d.ts +20 -0
  38. package/types/index.macos.d.ts +17 -0
  39. package/types/props.d.ts +96 -0
  40. package/types/table.d.ts +83 -0
@@ -0,0 +1,277 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, describe, expect, it, vi } from 'vitest'
3
+ import { act, createRoot, useState } from 'octane'
4
+ import { DataGrid } from './DataGrid.tsrx'
5
+
6
+ // Windowing has its own integration suite; keep these control regressions
7
+ // independent of jsdom's absent layout and ResizeObserver implementation.
8
+ vi.mock('@octane-xplat/ui', async (original) => {
9
+ const ui = await original<any>()
10
+ return {
11
+ ...ui,
12
+ VirtualList: (props: any) => (
13
+ <ui.View>
14
+ {props.items.length
15
+ ? props.items.map((item: any, index: number) => (
16
+ <ui.View key={props.keyExtractor(item, index)}>
17
+ {props.renderItem(item, index)}
18
+ </ui.View>
19
+ ))
20
+ : props.renderEmpty?.()}
21
+ </ui.View>
22
+ ),
23
+ }
24
+ })
25
+
26
+ vi.stubGlobal('matchMedia', () => ({
27
+ matches: false,
28
+ addEventListener() {},
29
+ removeEventListener() {},
30
+ }))
31
+
32
+ const roots: any[] = []
33
+ const data = [
34
+ { id: 'a', name: 'Alice', count: 1 },
35
+ { id: 'b', name: 'Bob', count: 2 },
36
+ { id: 'c', name: 'Charlie', count: 3 },
37
+ ]
38
+
39
+ const columns = [
40
+ {
41
+ accessorKey: 'name',
42
+ header: 'Name',
43
+ size: 140,
44
+ minSize: 100,
45
+ maxSize: 180,
46
+ enableHiding: false,
47
+ },
48
+ { accessorKey: 'count', header: 'Count', size: 100 },
49
+ ]
50
+
51
+ function mount(extra: any = {}) {
52
+ const el = document.createElement('div')
53
+ document.body.append(el)
54
+ const root = createRoot(el)
55
+ roots.push(root)
56
+ act(() =>
57
+ root.render(
58
+ <DataGrid id="grid" data={data} columns={columns} getRowId={(row) => row.id} {...extra} />,
59
+ ),
60
+ )
61
+
62
+ return el
63
+ }
64
+
65
+ const click = (id: string) =>
66
+ act(() => document.getElementById(id)!.dispatchEvent(new MouseEvent('click', { bubbles: true })))
67
+
68
+ const input = (id: string, text: string) =>
69
+ act(() => {
70
+ const el = document.getElementById(id) as HTMLInputElement
71
+ el.value = text
72
+ el.dispatchEvent(new Event('input', { bubbles: true }))
73
+ })
74
+
75
+ const pan = (id: string, type: string, x: number) =>
76
+ act(() => {
77
+ const event = new Event(type, { bubbles: true })
78
+ Object.assign(event, { clientX: x, clientY: 0, pointerId: 1 })
79
+ document.getElementById(id)!.dispatchEvent(event)
80
+ })
81
+
82
+ const names = () => [...document.querySelectorAll('[id^="grid-row-"]')].map((el) => el.textContent)
83
+
84
+ afterEach(() => {
85
+ for (const root of roots.splice(0)) act(() => root.unmount())
86
+ document.body.innerHTML = ''
87
+ })
88
+
89
+ describe('DataGrid controls', () => {
90
+ it('filters and resets to the first page, pages results and changes page size', () => {
91
+ mount({
92
+ showGlobalFilter: true,
93
+ showColumnFilters: true,
94
+ showPagination: true,
95
+ initialState: { pagination: { pageIndex: 1, pageSize: 1 } },
96
+ })
97
+
98
+ expect(names()).toEqual(['Bob2'])
99
+ input('grid-search', 'ali')
100
+ expect(names()).toEqual(['Alice1'])
101
+ expect(document.getElementById('grid-page')!.textContent).toBe('Page 1 of 1')
102
+ input('grid-search', '')
103
+ click('grid-next')
104
+ expect(names()).toEqual(['Bob2'])
105
+ input('grid-th-name-filter', 'char')
106
+ expect(names()).toEqual(['Charlie3'])
107
+ input('grid-th-name-filter', '')
108
+ click('grid-size-10')
109
+ expect(names()).toHaveLength(3)
110
+ input('grid-th-count-filter', '2')
111
+ expect(names()).toEqual(['Bob2'])
112
+ })
113
+
114
+ it('selects only selectable page rows, preserves off-page IDs, and does not activate a row', () => {
115
+ const press = vi.fn()
116
+ mount({
117
+ showRowSelection: true,
118
+ showPagination: true,
119
+ onRowPress: press,
120
+ options: { enableRowSelection: (row: any) => row.id !== 'b' },
121
+ initialState: { pagination: { pageIndex: 0, pageSize: 2 } },
122
+ })
123
+
124
+ click('grid-select-a')
125
+ expect(document.getElementById('grid-select-page')!.getAttribute('aria-checked')).toBe('true')
126
+ expect(press).not.toHaveBeenCalled()
127
+ expect(document.getElementById('grid-select-b')!.getAttribute('aria-disabled')).toBe('true')
128
+ click('grid-next')
129
+ click('grid-select-page')
130
+ expect(document.getElementById('grid-selected-count')!.textContent).toBe('2 selected')
131
+ click('grid-clear-selection')
132
+ expect(document.getElementById('grid-selected-count')!.textContent).toBe('0 selected')
133
+ })
134
+
135
+ it('protects non-hideable columns, moves columns and resets settings', () => {
136
+ mount({ showColumnSettings: true })
137
+ click('grid-settings')
138
+ expect(document.getElementById('grid-visible-name')!.getAttribute('aria-disabled')).toBe('true')
139
+ click('grid-visible-count')
140
+ expect(document.getElementById('grid-th-count')).toBeNull()
141
+ click('grid-visible-count')
142
+ click('grid-earlier-count')
143
+ expect(names()[0]).toBe('1Alice')
144
+ click('grid-reset-columns')
145
+ expect(names()[0]).toBe('Alice1')
146
+ })
147
+
148
+ it('resizes within bounds with matching row widths and rolls back cancelled pans', () => {
149
+ mount({ resizableColumns: true })
150
+ const width = () =>
151
+ (document.getElementById('grid-th-name')!.parentElement as HTMLElement).style.width
152
+
153
+ click('grid-th-name-widen')
154
+ expect(width()).toBe('150px')
155
+ expect((document.querySelector('#grid-row-a .vx-table-cell') as HTMLElement).style.width).toBe(
156
+ '150px',
157
+ )
158
+
159
+ pan('grid-th-name-resize', 'pointerdown', 0)
160
+ pan('grid-th-name-resize', 'pointermove', 200)
161
+ expect(width()).toBe('180px')
162
+ pan('grid-th-name-resize', 'pointercancel', 200)
163
+ expect(width()).toBe('150px')
164
+ })
165
+
166
+ it('reorders on release and preserves order on cancellation', () => {
167
+ mount({ reorderableColumns: true })
168
+ pan('grid-th-name-drag', 'pointerdown', 0)
169
+ pan('grid-th-name-drag', 'pointermove', 200)
170
+ pan('grid-th-name-drag', 'pointercancel', 200)
171
+ expect(names()[0]).toBe('Alice1')
172
+ pan('grid-th-name-drag', 'pointerdown', 0)
173
+ pan('grid-th-name-drag', 'pointerup', 200)
174
+ expect(names()[0]).toBe('1Alice')
175
+ })
176
+
177
+ it('forwards controlled callbacks without changing the owner snapshot', () => {
178
+ const changed = vi.fn()
179
+ mount({ state: { sorting: [] }, options: { onSortingChange: changed } })
180
+ click('grid-th-name')
181
+ expect(changed).toHaveBeenCalledOnce()
182
+ expect(changed.mock.calls[0][0]([])).toEqual([{ id: 'name', desc: false }])
183
+ expect(names()[0]).toBe('Alice1')
184
+ })
185
+ })
186
+
187
+
188
+ describe('DataGrid selection and row accessibility', () => {
189
+ it('announces mixed page selection, supports Space, and disables an empty filtered page', () => {
190
+ mount({ showRowSelection: true, showGlobalFilter: true, getRowLabel: (row: any) => row.original.name })
191
+ const page = () => document.getElementById('grid-select-page')!
192
+ const alice = document.getElementById('grid-select-a')!
193
+ expect(alice.getAttribute('aria-label')).toBe('Select Alice')
194
+ expect(alice.getAttribute('aria-description')).toBe('Row 1 of 3')
195
+ act(() => alice.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true })))
196
+ expect(page().getAttribute('aria-checked')).toBe('mixed')
197
+ click('grid-select-page')
198
+ expect(page().getAttribute('aria-checked')).toBe('true')
199
+ click('grid-select-page')
200
+ expect(page().getAttribute('aria-checked')).toBe('false')
201
+ input('grid-search', 'missing')
202
+ expect(page().getAttribute('aria-disabled')).toBe('true')
203
+ click('grid-select-page')
204
+ expect(document.getElementById('grid-selected-count')!.textContent).toBe('0 selected')
205
+ })
206
+
207
+ it('uses sorted filtered ordinals with a page offset and excludes controls from the table', () => {
208
+ mount({ showRowSelection: true, showPagination: true, showGlobalFilter: true,
209
+ initialState: { pagination: { pageIndex: 1, pageSize: 1 }, sorting: [{ id: 'name', desc: true }] } })
210
+
211
+ const table = document.getElementById('grid-table')!
212
+ expect(table.getAttribute('role')).toBe('table')
213
+ expect(table.getAttribute('aria-rowcount')).toBe('4')
214
+ expect(table.getAttribute('aria-colcount')).toBe('3')
215
+ expect(document.getElementById('grid-row-b')!.getAttribute('aria-rowindex')).toBe('3')
216
+ expect(table.querySelectorAll('[role="columnheader"]')).toHaveLength(3)
217
+ expect(table.querySelectorAll('[role="cell"]')).toHaveLength(3)
218
+ expect(table.contains(document.getElementById('grid-clear-selection'))).toBe(false)
219
+ input('grid-search', 'ali')
220
+ expect(document.getElementById('grid-row-a')!.getAttribute('aria-rowindex')).toBe('2')
221
+ expect(table.getAttribute('aria-rowcount')).toBe('2')
222
+ })
223
+
224
+ it('preserves caller-owned IDs through filtering, paging and data replacement, then clears them', () => {
225
+ const changes = vi.fn()
226
+ const el = document.createElement('div')
227
+ document.body.append(el)
228
+ const root = createRoot(el)
229
+ roots.push(root)
230
+ function Owner() {
231
+ const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({ absent: true })
232
+ const [rows, setRows] = useState(data)
233
+ return <>
234
+ <button id="replace" onClick={() => setRows([...data].reverse())}>Replace</button>
235
+ <DataGrid id="grid" data={rows} columns={columns} getRowId={(row) => row.id}
236
+ showRowSelection showGlobalFilter showPagination
237
+ initialState={{ pagination: { pageIndex: 0, pageSize: 2 } }}
238
+ state={{ rowSelection }} options={{ enableRowSelection: (row) => row.id !== 'b',
239
+ onRowSelectionChange: (updater) => { changes(updater); setRowSelection(updater) } }} />
240
+ </>
241
+ }
242
+
243
+ act(() => root.render(<Owner />))
244
+ click('grid-select-page')
245
+ expect(document.getElementById('grid-selected-count')!.textContent).toBe('2 selected')
246
+ input('grid-search', 'char')
247
+ click('grid-select-page')
248
+ expect(document.getElementById('grid-selected-count')!.textContent).toBe('3 selected')
249
+ click('grid-select-page')
250
+ expect(document.getElementById('grid-selected-count')!.textContent).toBe('2 selected')
251
+ input('grid-search', '')
252
+ click('grid-next')
253
+ click('replace')
254
+ click('grid-previous')
255
+ expect(document.getElementById('grid-select-b')!.getAttribute('aria-checked')).toBe('false')
256
+ click('grid-clear-selection')
257
+ expect(document.getElementById('grid-selected-count')!.textContent).toBe('0 selected')
258
+ expect(changes).toHaveBeenCalledTimes(4)
259
+ })
260
+
261
+ it('keeps a controlled snapshot read-only until its owner publishes the updater', () => {
262
+ const changed = vi.fn()
263
+ mount({ showRowSelection: true, state: { rowSelection: {} }, options: { onRowSelectionChange: changed } })
264
+ click('grid-select-a')
265
+ expect(changed.mock.calls[0][0]({})).toEqual({ a: true })
266
+ expect(document.getElementById('grid-select-a')!.getAttribute('aria-checked')).toBe('false')
267
+ })
268
+
269
+ it.each([[-1, undefined], [101, 100]])('describes manual server pages with total %s', (expected, rowCount) => {
270
+ mount({ showRowSelection: true, state: { pagination: { pageIndex: 3, pageSize: 10 } },
271
+ options: { manualPagination: true, rowCount, pageCount: -1 } })
272
+
273
+ expect(document.getElementById('grid-table')!.getAttribute('aria-rowcount')).toBe(String(expected))
274
+ expect(document.getElementById('grid-row-a')!.getAttribute('aria-rowindex')).toBe('32')
275
+ expect(document.getElementById('grid-select-a')!.getAttribute('aria-description')).toBe(rowCount == null ? 'Row 31' : 'Row 31 of 100')
276
+ })
277
+ })
@@ -0,0 +1,157 @@
1
+ import { useState } from 'octane'
2
+ import {
3
+ CheckboxInput as Checkbox,
4
+ Pressable,
5
+ HStack as Row,
6
+ Text,
7
+ TextInput,
8
+ View,
9
+ } from '@octane-xplat/ui'
10
+
11
+ import { moveColumn } from './grid-layout'
12
+
13
+ function Action(props: { id?: string; label: string; disabled?: boolean; onPress: () => void }) {
14
+ return (
15
+ <Pressable
16
+ id={props.id}
17
+ className="vx-datagrid-action"
18
+ disabled={props.disabled}
19
+ onPress={props.onPress}
20
+ accessibilityLabel={props.label}
21
+ >
22
+ <Text>{props.label}</Text>
23
+ </Pressable>
24
+ )
25
+ }
26
+
27
+ /** Controls subscribe through their parent's state snapshot on native too. */
28
+ export function GridControls(props: {
29
+ table: any
30
+ state: any
31
+ id?: string
32
+ settings?: boolean
33
+ search?: boolean
34
+ }) {
35
+ const [settingsOpen, setSettingsOpen] = useState(false)
36
+ void props.state
37
+ const table = props.table
38
+ const columns = table.getAllLeafColumns()
39
+ const order = columns.map((column: any) => column.id)
40
+ const move = (id: string, index: number) =>
41
+ table.setColumnOrder(moveColumn(order, id, order[index]))
42
+
43
+ return (
44
+ <View>
45
+ <Row className="vx-datagrid-controls">
46
+ {props.search ? (
47
+ <TextInput
48
+ id={props.id ? `${props.id}-search` : undefined}
49
+ className="flex-1"
50
+ value={String(props.state.globalFilter ?? '')}
51
+ placeholder="Filter rows"
52
+ accessibilityLabel="Filter rows"
53
+ onChange={(value) => {
54
+ table.setGlobalFilter(value)
55
+ table.setPageIndex(0)
56
+ }}
57
+ />
58
+ ) : null}
59
+ {props.settings ? (
60
+ <Action
61
+ id={props.id ? `${props.id}-settings` : undefined}
62
+ label="Columns"
63
+ onPress={() => setSettingsOpen(!settingsOpen)}
64
+ />
65
+ ) : null}
66
+ </Row>
67
+ {props.settings && settingsOpen ? (
68
+ <View className="vx-datagrid-settings">
69
+ {columns.map((column: any, index: number) => (
70
+ <Row key={column.id} className="vx-datagrid-controls">
71
+ <Checkbox
72
+ id={props.id ? `${props.id}-visible-${column.id}` : undefined}
73
+ checked={column.getIsVisible()}
74
+ isDisabled={!column.getCanHide()}
75
+ label={
76
+ typeof column.columnDef.header === 'string' ? column.columnDef.header : column.id
77
+ }
78
+ accessibilityLabel={`Show ${column.id}`}
79
+ onCheckedChange={(checked) => column.toggleVisibility(checked)}
80
+ />
81
+ <Action
82
+ id={props.id ? `${props.id}-earlier-${column.id}` : undefined}
83
+ label={`Move ${column.id} earlier`}
84
+ disabled={index === 0}
85
+ onPress={() => move(column.id, index - 1)}
86
+ />
87
+ <Action
88
+ id={props.id ? `${props.id}-later-${column.id}` : undefined}
89
+ label={`Move ${column.id} later`}
90
+ disabled={index === columns.length - 1}
91
+ onPress={() => move(column.id, index + 1)}
92
+ />
93
+ </Row>
94
+ ))}
95
+ <Action
96
+ id={props.id ? `${props.id}-reset-columns` : undefined}
97
+ label="Reset columns"
98
+ onPress={() => {
99
+ table.resetColumnVisibility()
100
+ table.resetColumnOrder()
101
+ table.resetColumnSizing()
102
+ }}
103
+ />
104
+ </View>
105
+ ) : null}
106
+ </View>
107
+ )
108
+ }
109
+
110
+ export function GridPagination(props: {
111
+ table: any
112
+ state: any
113
+ id?: string
114
+ sizes?: readonly number[]
115
+ }) {
116
+ void props.state
117
+ const table = props.table
118
+ const { pageIndex, pageSize } = props.state.pagination
119
+ const count = table.getPageCount()
120
+ const sizes = [
121
+ ...new Set((props.sizes ?? [10, 25, 50]).filter((size) => Number.isInteger(size) && size > 0)),
122
+ ]
123
+
124
+ return (
125
+ <Row className="vx-datagrid-controls" accessibilityLabel="Pagination">
126
+ <Action
127
+ id={props.id ? `${props.id}-previous` : undefined}
128
+ label="Previous page"
129
+ disabled={!table.getCanPreviousPage()}
130
+ onPress={() => table.previousPage()}
131
+ />
132
+ <Text id={props.id ? `${props.id}-page` : undefined}>
133
+ {count < 0
134
+ ? `Page ${pageIndex + 1}`
135
+ : `Page ${count === 0 ? 0 : pageIndex + 1} of ${count}`}
136
+ </Text>
137
+ <Action
138
+ id={props.id ? `${props.id}-next` : undefined}
139
+ label="Next page"
140
+ disabled={!table.getCanNextPage()}
141
+ onPress={() => table.nextPage()}
142
+ />
143
+ {sizes.map((size) => (
144
+ <Action
145
+ key={size}
146
+ id={props.id ? `${props.id}-size-${size}` : undefined}
147
+ label={`${size} per page`}
148
+ disabled={pageSize === size}
149
+ onPress={() => {
150
+ table.setPageSize(size)
151
+ table.setPageIndex(0)
152
+ }}
153
+ />
154
+ ))}
155
+ </Row>
156
+ )
157
+ }
@@ -0,0 +1,14 @@
1
+ /** Positions describe the final flat row model, before VirtualList windowing.
2
+ * Header rows count toward ARIA indices, but not the spoken data-row ordinal. */
3
+ export function gridRowPosition(
4
+ index: number,
5
+ pagination: { pageIndex: number; pageSize: number },
6
+ headerRows: number,
7
+ rowCount: number,
8
+ ) {
9
+ const ordinal = pagination.pageIndex * pagination.pageSize + index + 1
10
+ return {
11
+ ariaIndex: headerRows + ordinal,
12
+ label: rowCount < 0 ? `Row ${ordinal}` : `Row ${ordinal} of ${rowCount}`,
13
+ }
14
+ }
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { dragTarget, moveColumn, resizedWidth } from './grid-layout'
3
+
4
+ describe('column gesture geometry', () => {
5
+ it('moves across variable-width midpoints in either direction', () => {
6
+ const columns = [
7
+ { id: 'a', getSize: () => 100 },
8
+ { id: 'b', getSize: () => 200 },
9
+ { id: 'c', getSize: () => 80 },
10
+ ]
11
+
12
+ expect(dragTarget(columns, 'a', 149)).toBe('a')
13
+ expect(dragTarget(columns, 'a', 150)).toBe('b')
14
+ expect(dragTarget(columns, 'a', 290)).toBe('c')
15
+ expect(dragTarget(columns, 'c', -140)).toBe('b')
16
+ expect(dragTarget(columns, 'c', -290)).toBe('a')
17
+ expect(dragTarget(columns, 'a', Number.NaN)).toBe('a')
18
+ })
19
+
20
+ it('keeps hidden IDs in the complete order and rejects missing targets', () => {
21
+ const order = ['a', 'hidden', 'b']
22
+ expect(moveColumn(order, 'a', 'b')).toEqual(['hidden', 'b', 'a'])
23
+ expect(order).toEqual(['a', 'hidden', 'b'])
24
+ expect(moveColumn(order, 'missing', 'b')).toBe(order)
25
+ })
26
+
27
+ it('clamps to both configured size bounds', () => {
28
+ const column = { columnDef: { minSize: 80, maxSize: 180 } }
29
+ expect(resizedWidth(column, 10)).toBe(80)
30
+ expect(resizedWidth(column, 500)).toBe(180)
31
+ })
32
+ })
@@ -0,0 +1,52 @@
1
+ /** Resolve the displayed width without changing flexible legacy columns. */
2
+ export function columnStyle(column: any, fixed: boolean, sizing: Record<string, number>) {
3
+ const explicit = fixed || column.columnDef.size != null || sizing[column.id] != null
4
+ return explicit
5
+ ? { width: column.getSize(), flexGrow: 0, flexShrink: 0 }
6
+ : { flexGrow: 1, flexShrink: 1 }
7
+ }
8
+
9
+ /** Move a column within the complete order, preserving hidden columns. */
10
+ export function moveColumn(order: string[], source: string, target: string) {
11
+ const from = order.indexOf(source)
12
+ const to = order.indexOf(target)
13
+ if (from < 0 || to < 0 || from === to) {
14
+ return order
15
+ }
16
+
17
+ const next = [...order]
18
+ next.splice(from, 1)
19
+ next.splice(to, 0, source)
20
+ return next
21
+ }
22
+
23
+ /** Find the visible column whose midpoint a horizontal drag has crossed. */
24
+ export function dragTarget(columns: { id: string; getSize(): number }[], id: string, dx: number) {
25
+ const from = columns.findIndex((column) => column.id === id)
26
+ if (from < 0 || !Number.isFinite(dx)) {
27
+ return id
28
+ }
29
+
30
+ let distance = columns[from].getSize() / 2
31
+ let target = id
32
+ const direction = dx < 0 ? -1 : 1
33
+ for (let i = from + direction; i >= 0 && i < columns.length; i += direction) {
34
+ distance += columns[i].getSize() / 2
35
+ if (Math.abs(dx) < distance) {
36
+ break
37
+ }
38
+
39
+ target = columns[i].id
40
+ distance += columns[i].getSize() / 2
41
+ }
42
+
43
+ return target
44
+ }
45
+
46
+ /** Clamp interactive widths to the column's configured bounds. */
47
+ export function resizedWidth(column: any, width: number) {
48
+ return Math.max(
49
+ column.columnDef.minSize ?? 20,
50
+ Math.min(column.columnDef.maxSize ?? Number.MAX_SAFE_INTEGER, width),
51
+ )
52
+ }
@@ -0,0 +1,8 @@
1
+ export { DataGrid } from './DataGrid.tsrx'
2
+ export { useTable } from './useTable.tsrx'
3
+ export { createTable, dataGridFeatures } from './table'
4
+ export type { DataGridOptions, DataGridProps, UseTable } from './props'
5
+
6
+ export type { Cell, ColumnDef, Header, Row, Table, TableState } from '@tanstack/table-core'
7
+
8
+ export type { DataGridFeatures } from './table'
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export { DataGrid } from './DataGrid.tsrx'
2
+ export { useTable } from './useTable.tsrx'
3
+ export { createTable, dataGridFeatures } from './table'
4
+ export type { DataGridFeatures } from './table'
5
+ export type { DataGridOptions, DataGridProps, UseTable } from './props'
6
+
7
+ export type { Cell, ColumnDef, Header, Row, Table, TableState } from '@tanstack/table-core'
@@ -0,0 +1,8 @@
1
+ export { DataGrid } from './DataGrid.tsrx'
2
+ export { useTable } from './useTable.tsrx'
3
+ export { createTable, dataGridFeatures } from './table'
4
+ export type { DataGridOptions, DataGridProps, UseTable } from './props'
5
+
6
+ export type { Cell, ColumnDef, Header, Row, Table, TableState } from '@tanstack/table-core'
7
+
8
+ export type { DataGridFeatures } from './table'
package/src/props.ts ADDED
@@ -0,0 +1,122 @@
1
+ import type {
2
+ Cell,
3
+ ColumnDef,
4
+ Header,
5
+ Row,
6
+ RowData,
7
+ Table,
8
+ TableFeatures,
9
+ TableOptions,
10
+ TableState,
11
+ } from '@tanstack/table-core'
12
+
13
+ import type { DataGridFeatures } from './table'
14
+
15
+ export interface LayoutChildProps {
16
+ row?: number
17
+ col?: number
18
+ rowSpan?: number
19
+ colSpan?: number
20
+ dock?: 'left' | 'top' | 'right' | 'bottom'
21
+ left?: number
22
+ top?: number
23
+ flexGrow?: number
24
+ flexShrink?: number
25
+ alignSelf?: string
26
+ order?: number
27
+ }
28
+
29
+ export interface AccessibilityProps {
30
+ accessible?: boolean
31
+ accessibilityLabel?: string
32
+ accessibilityHint?: string
33
+ accessibilityRole?: string
34
+ accessibilityState?: Record<string, any>
35
+ testID?: string
36
+ }
37
+
38
+ /** Options shared by `createTable`/`useTable`/`DataGrid`. Extends the
39
+ * table-core options for the enabled feature set minus its reactivity slot
40
+ * (owned by the adapter). */
41
+ export type DataGridOptions<TFeatures extends TableFeatures, TData extends RowData> = Omit<
42
+ TableOptions<TFeatures, TData>,
43
+ 'features' | 'coreReactivityFeature'
44
+ > & {
45
+ /** Feature slots beyond the defaults — pass `tableFeatures({...})`
46
+ * output or merge onto `dataGridFeatures`. */
47
+ features?: Partial<TableFeatures>
48
+ }
49
+
50
+ /** `useTable` return — the live table instance plus the flat reactive state.
51
+ * `state` re-reads on every slice change; row models come from
52
+ * `table.getRowModel()` (memoized per state/option revision). */
53
+ export interface UseTable<TFeatures extends TableFeatures, TData extends RowData> {
54
+ table: Table<TFeatures, TData>
55
+ state: TableState<TFeatures>
56
+ }
57
+
58
+ export interface DataGridProps<
59
+ TData extends RowData = any,
60
+ TFeatures extends TableFeatures = DataGridFeatures,
61
+ >
62
+ extends LayoutChildProps, AccessibilityProps {
63
+ id?: string
64
+ className?: any
65
+ style?: any
66
+ /** Row data — pass a new array reference when data changes. */
67
+ data: TData[]
68
+ columns: ColumnDef<TFeatures, TData, any>[]
69
+ /** Feature overrides merged onto the defaults (see `dataGridFeatures`). */
70
+ features?: Partial<TableFeatures>
71
+ /** Core options (atoms, change callbacks, manual row models, selection
72
+ * predicates). Data/columns and the explicit props below take precedence. */
73
+ options?: Omit<
74
+ DataGridOptions<TFeatures, TData>,
75
+ 'data' | 'columns' | 'features' | 'state' | 'initialState' | 'getRowId'
76
+ >
77
+ /** Partial controlled state. Pair with options.atoms or per-slice
78
+ * options callbacks; a state snapshot alone is read-only. */
79
+ state?: Partial<TableState<TFeatures>>
80
+ initialState?: Partial<TableState<TFeatures>>
81
+ getRowId?: (row: TData, index: number) => string
82
+ /** Per-cell content override. Defaults to the column's `cell` template,
83
+ * then `cell.getValue()`. */
84
+ renderCell?: (cell: Cell<TFeatures, TData, any>) => any
85
+ /** Per-header content override. Defaults to the column's `header`
86
+ * template plus a sort affordance when the column can sort. */
87
+ renderHeader?: (header: Header<TFeatures, TData, any>) => any
88
+ /** Row tap — marks rows pressable. */
89
+ onRowPress?: (row: Row<TFeatures, TData>) => void
90
+ renderEmpty?: () => any
91
+ /** Show a text query across globally filterable columns. */
92
+ showGlobalFilter?: boolean
93
+ /** Show text filters beneath filterable leaf-column headings. */
94
+ showColumnFilters?: boolean
95
+ /** Show previous/next and page-size controls. Client-side by default;
96
+ * use options.manualPagination with pageCount/rowCount for server data. */
97
+ showPagination?: boolean
98
+ /** Positive integer page sizes offered by the pager. Defaults to [10, 25, 50]. */
99
+ pageSizeOptions?: readonly number[]
100
+ /** Show row checkboxes and a current-page select-all checkbox. Selection
101
+ * survives filtering/pagination by ID; provide getRowId for mutable data. */
102
+ showRowSelection?: boolean
103
+ /** Optional label for each selection checkbox. */
104
+ getRowLabel?: (row: Row<TFeatures, TData>) => string
105
+ /** Show a column visibility/order panel, reset to initial state, and
106
+ * keyboard/touch move buttons. Non-hideable columns stay visible. */
107
+ showColumnSettings?: boolean
108
+ /** Fixed-width columns with pan resize handles and accessible +/- actions.
109
+ * Widths use column minSize/maxSize; cancellation restores prior state. */
110
+ resizableColumns?: boolean
111
+ /** Separate pan handles reorder leaf columns on release. Cancel preserves
112
+ * order; settings move buttons provide a non-drag alternative. */
113
+ reorderableColumns?: boolean
114
+ /** Horizontal gesture direction. Defaults to ltr; use rtl for mirrored layouts. */
115
+ direction?: 'ltr' | 'rtl'
116
+ /** Caller-owned bulk actions; selected IDs include off-page selections. */
117
+ renderSelectionActions?: (table: Table<TFeatures, TData>) => any
118
+ /** Platform escape bags — applied after shared props on the root view. */
119
+ ios?: Record<string, any>
120
+ android?: Record<string, any>
121
+ web?: Record<string, any>
122
+ }