@postxl/generators 1.1.1 → 1.2.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 (161) hide show
  1. package/dist/frontend-core/frontend.generator.d.ts +0 -58
  2. package/dist/frontend-core/frontend.generator.js +6 -172
  3. package/dist/frontend-core/frontend.generator.js.map +1 -1
  4. package/dist/frontend-core/template/README.md +1 -1
  5. package/dist/frontend-core/template/src/components/admin/table-filter.tsx +1 -5
  6. package/dist/frontend-core/template/src/components/ui/color-mode-toggle/color-mode-toggle.tsx +10 -4
  7. package/dist/frontend-core/template/src/pages/dashboard/dashboard.page.tsx +2 -3
  8. package/dist/frontend-core/template/src/pages/error/default-error.page.tsx +1 -1
  9. package/dist/frontend-core/template/src/pages/error/not-found-error.page.tsx +1 -1
  10. package/dist/frontend-core/template/src/styles/styles.css +13 -1
  11. package/dist/frontend-core/template/tsconfig.json +2 -0
  12. package/dist/frontend-core/types/component.d.ts +1 -1
  13. package/dist/frontend-forms/generators/discriminated-union/fields.generator.js +4 -6
  14. package/dist/frontend-forms/generators/discriminated-union/fields.generator.js.map +1 -1
  15. package/dist/frontend-forms/generators/discriminated-union/inputs.generator.js +1 -1
  16. package/dist/frontend-forms/generators/discriminated-union/inputs.generator.js.map +1 -1
  17. package/dist/frontend-forms/generators/enum/inputs.generator.js +1 -1
  18. package/dist/frontend-forms/generators/enum/inputs.generator.js.map +1 -1
  19. package/dist/frontend-forms/generators/model/forms.generator.js +8 -12
  20. package/dist/frontend-forms/generators/model/forms.generator.js.map +1 -1
  21. package/dist/frontend-forms/generators/model/inputs.generator.js +2 -6
  22. package/dist/frontend-forms/generators/model/inputs.generator.js.map +1 -1
  23. package/dist/frontend-forms/template/src/components/ui/field/field.tsx +1 -4
  24. package/dist/frontend-tables/generators/model-table.generator.js +1 -5
  25. package/dist/frontend-tables/generators/model-table.generator.js.map +1 -1
  26. package/package.json +3 -2
  27. package/dist/frontend-core/template/src/components/ui/accordion/accordion.stories.tsx +0 -47
  28. package/dist/frontend-core/template/src/components/ui/accordion/accordion.tsx +0 -52
  29. package/dist/frontend-core/template/src/components/ui/admin-sidebar/admin-sidebar.tsx +0 -195
  30. package/dist/frontend-core/template/src/components/ui/alert/alert.stories.tsx +0 -61
  31. package/dist/frontend-core/template/src/components/ui/alert/alert.tsx +0 -45
  32. package/dist/frontend-core/template/src/components/ui/alert-dialog/alert-dialog.stories.tsx +0 -52
  33. package/dist/frontend-core/template/src/components/ui/alert-dialog/alert-dialog.tsx +0 -105
  34. package/dist/frontend-core/template/src/components/ui/avatar/avatar.stories.tsx +0 -30
  35. package/dist/frontend-core/template/src/components/ui/avatar/avatar.tsx +0 -39
  36. package/dist/frontend-core/template/src/components/ui/badge/badge.stories.tsx +0 -78
  37. package/dist/frontend-core/template/src/components/ui/badge/badge.tsx +0 -48
  38. package/dist/frontend-core/template/src/components/ui/breadcrumb/breadcrumb.stories.tsx +0 -67
  39. package/dist/frontend-core/template/src/components/ui/breadcrumb/breadcrumb.tsx +0 -85
  40. package/dist/frontend-core/template/src/components/ui/button/button.stories.tsx +0 -150
  41. package/dist/frontend-core/template/src/components/ui/button/button.tsx +0 -68
  42. package/dist/frontend-core/template/src/components/ui/calendar/calendar.stories.tsx +0 -160
  43. package/dist/frontend-core/template/src/components/ui/calendar/calendar.tsx +0 -293
  44. package/dist/frontend-core/template/src/components/ui/card/card.stories.tsx +0 -77
  45. package/dist/frontend-core/template/src/components/ui/card/card.tsx +0 -45
  46. package/dist/frontend-core/template/src/components/ui/card-hover/card-hover.stories.tsx +0 -29
  47. package/dist/frontend-core/template/src/components/ui/card-hover/card-hover.tsx +0 -28
  48. package/dist/frontend-core/template/src/components/ui/carousel/carousel.stories.tsx +0 -154
  49. package/dist/frontend-core/template/src/components/ui/carousel/carousel.tsx +0 -227
  50. package/dist/frontend-core/template/src/components/ui/checkbox/checkbox.stories.tsx +0 -106
  51. package/dist/frontend-core/template/src/components/ui/checkbox/checkbox.tsx +0 -88
  52. package/dist/frontend-core/template/src/components/ui/checkbox/shadcn-checkbox.stories.tsx +0 -90
  53. package/dist/frontend-core/template/src/components/ui/checkbox/shadcn-checkbox.tsx +0 -54
  54. package/dist/frontend-core/template/src/components/ui/collapse/collapse.stories.tsx +0 -52
  55. package/dist/frontend-core/template/src/components/ui/collapse/collapse.tsx +0 -9
  56. package/dist/frontend-core/template/src/components/ui/combobox/combobox.stories.tsx +0 -207
  57. package/dist/frontend-core/template/src/components/ui/combobox/combobox.tsx +0 -79
  58. package/dist/frontend-core/template/src/components/ui/command/command.stories.tsx +0 -186
  59. package/dist/frontend-core/template/src/components/ui/command/command.tsx +0 -165
  60. package/dist/frontend-core/template/src/components/ui/command-palette/command-palette.stories.tsx +0 -160
  61. package/dist/frontend-core/template/src/components/ui/command-palette/command-palette.tsx +0 -134
  62. package/dist/frontend-core/template/src/components/ui/content-frame/content-frame.stories.tsx +0 -198
  63. package/dist/frontend-core/template/src/components/ui/content-frame/content-frame.tsx +0 -100
  64. package/dist/frontend-core/template/src/components/ui/context-menu/context-menu.stories.tsx +0 -78
  65. package/dist/frontend-core/template/src/components/ui/context-menu/context-menu.tsx +0 -179
  66. package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/cell-variant-types.ts +0 -11
  67. package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/checkbox-cell.tsx +0 -116
  68. package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/date-cell.tsx +0 -157
  69. package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/gantt-cell.tsx +0 -82
  70. package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/long-text-cell.tsx +0 -180
  71. package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/multi-select-cell.tsx +0 -280
  72. package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/number-cell.tsx +0 -169
  73. package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/react-node-cell.tsx +0 -33
  74. package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/select-cell.tsx +0 -175
  75. package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/short-text-cell.tsx +0 -138
  76. package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/utils/gantt-timeline.tsx +0 -92
  77. package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/utils/gantt-timerange-picker.tsx +0 -330
  78. package/dist/frontend-core/template/src/components/ui/data-grid/data-grid-cell-wrapper.tsx +0 -212
  79. package/dist/frontend-core/template/src/components/ui/data-grid/data-grid-cell.tsx +0 -157
  80. package/dist/frontend-core/template/src/components/ui/data-grid/data-grid-column-header.tsx +0 -340
  81. package/dist/frontend-core/template/src/components/ui/data-grid/data-grid-context-menu.tsx +0 -271
  82. package/dist/frontend-core/template/src/components/ui/data-grid/data-grid-row.tsx +0 -123
  83. package/dist/frontend-core/template/src/components/ui/data-grid/data-grid-search.tsx +0 -211
  84. package/dist/frontend-core/template/src/components/ui/data-grid/data-grid-types.ts +0 -159
  85. package/dist/frontend-core/template/src/components/ui/data-grid/data-grid-utils.ts +0 -67
  86. package/dist/frontend-core/template/src/components/ui/data-grid/data-grid-view-menu.tsx +0 -360
  87. package/dist/frontend-core/template/src/components/ui/data-grid/data-grid.stories.tsx +0 -780
  88. package/dist/frontend-core/template/src/components/ui/data-grid/data-grid.tsx +0 -217
  89. package/dist/frontend-core/template/src/components/ui/data-grid/hooks/use-callback-ref.ts +0 -22
  90. package/dist/frontend-core/template/src/components/ui/data-grid/hooks/use-data-grid.tsx +0 -1892
  91. package/dist/frontend-core/template/src/components/ui/data-grid/hooks/use-debounced-callback.ts +0 -19
  92. package/dist/frontend-core/template/src/components/ui/data-grid/styles.css +0 -3
  93. package/dist/frontend-core/template/src/components/ui/data-table/context-menu-simple.tsx +0 -141
  94. package/dist/frontend-core/template/src/components/ui/data-table/data-table.stories.tsx +0 -146
  95. package/dist/frontend-core/template/src/components/ui/data-table/data-table.tsx +0 -447
  96. package/dist/frontend-core/template/src/components/ui/data-table/renderers/country-array-cell-renderer.tsx +0 -77
  97. package/dist/frontend-core/template/src/components/ui/data-table/renderers/country-cell-renderer.tsx +0 -56
  98. package/dist/frontend-core/template/src/components/ui/data-table/renderers/favorite-cell-renderer.tsx +0 -68
  99. package/dist/frontend-core/template/src/components/ui/data-table/renderers/links-cell-renderer.tsx +0 -205
  100. package/dist/frontend-core/template/src/components/ui/data-table/utils/columns.ts +0 -351
  101. package/dist/frontend-core/template/src/components/ui/data-table/utils/data-table.utils.ts +0 -49
  102. package/dist/frontend-core/template/src/components/ui/date-picker/date-picker.stories.tsx +0 -149
  103. package/dist/frontend-core/template/src/components/ui/date-picker/date-picker.tsx +0 -30
  104. package/dist/frontend-core/template/src/components/ui/dialog/dialog.stories.tsx +0 -80
  105. package/dist/frontend-core/template/src/components/ui/dialog/dialog.tsx +0 -134
  106. package/dist/frontend-core/template/src/components/ui/drawer/drawer.stories.tsx +0 -104
  107. package/dist/frontend-core/template/src/components/ui/drawer/drawer.tsx +0 -87
  108. package/dist/frontend-core/template/src/components/ui/dropdown-menu/dropdown-menu.stories.tsx +0 -168
  109. package/dist/frontend-core/template/src/components/ui/dropdown-menu/dropdown-menu.tsx +0 -225
  110. package/dist/frontend-core/template/src/components/ui/input/input.stories.tsx +0 -141
  111. package/dist/frontend-core/template/src/components/ui/input/input.tsx +0 -47
  112. package/dist/frontend-core/template/src/components/ui/label/label.stories.tsx +0 -41
  113. package/dist/frontend-core/template/src/components/ui/label/label.tsx +0 -20
  114. package/dist/frontend-core/template/src/components/ui/loader/loader.stories.tsx +0 -45
  115. package/dist/frontend-core/template/src/components/ui/loader/loader.tsx +0 -17
  116. package/dist/frontend-core/template/src/components/ui/mark-value-renderer/mark-value-renderer.stories.tsx +0 -114
  117. package/dist/frontend-core/template/src/components/ui/mark-value-renderer/mark-value-renderer.tsx +0 -48
  118. package/dist/frontend-core/template/src/components/ui/menubar/menu.stories.tsx +0 -134
  119. package/dist/frontend-core/template/src/components/ui/menubar/menubar.tsx +0 -208
  120. package/dist/frontend-core/template/src/components/ui/modal/modal.stories.tsx +0 -297
  121. package/dist/frontend-core/template/src/components/ui/modal/modal.tsx +0 -80
  122. package/dist/frontend-core/template/src/components/ui/navigation-menu/navigation-menu.stories.tsx +0 -213
  123. package/dist/frontend-core/template/src/components/ui/navigation-menu/navigation-menu.tsx +0 -142
  124. package/dist/frontend-core/template/src/components/ui/pagination/pagination.stories.tsx +0 -49
  125. package/dist/frontend-core/template/src/components/ui/pagination/pagination.tsx +0 -84
  126. package/dist/frontend-core/template/src/components/ui/popover/popover.stories.tsx +0 -82
  127. package/dist/frontend-core/template/src/components/ui/popover/popover.tsx +0 -55
  128. package/dist/frontend-core/template/src/components/ui/progress/progress.stories.tsx +0 -80
  129. package/dist/frontend-core/template/src/components/ui/progress/progress.tsx +0 -17
  130. package/dist/frontend-core/template/src/components/ui/radio-group/radio-group.stories.tsx +0 -154
  131. package/dist/frontend-core/template/src/components/ui/radio-group/radio-group.tsx +0 -68
  132. package/dist/frontend-core/template/src/components/ui/resizable/resizable.stories.tsx +0 -73
  133. package/dist/frontend-core/template/src/components/ui/resizable/resizeable.tsx +0 -38
  134. package/dist/frontend-core/template/src/components/ui/scroll-area/scroll-area.stories.tsx +0 -55
  135. package/dist/frontend-core/template/src/components/ui/scroll-area/scroll-area.tsx +0 -39
  136. package/dist/frontend-core/template/src/components/ui/select/select.stories.tsx +0 -297
  137. package/dist/frontend-core/template/src/components/ui/select/select.tsx +0 -227
  138. package/dist/frontend-core/template/src/components/ui/separator/separator.tsx +0 -21
  139. package/dist/frontend-core/template/src/components/ui/separator/seperator.stories.tsx +0 -25
  140. package/dist/frontend-core/template/src/components/ui/sheet/sheet.stories.tsx +0 -45
  141. package/dist/frontend-core/template/src/components/ui/sheet/sheet.tsx +0 -107
  142. package/dist/frontend-core/template/src/components/ui/skeleton/skeleton.stories.tsx +0 -26
  143. package/dist/frontend-core/template/src/components/ui/skeleton/skeleton.tsx +0 -7
  144. package/dist/frontend-core/template/src/components/ui/slider/slider.stories.tsx +0 -101
  145. package/dist/frontend-core/template/src/components/ui/slider/slider.tsx +0 -98
  146. package/dist/frontend-core/template/src/components/ui/spinner/spinner.stories.tsx +0 -19
  147. package/dist/frontend-core/template/src/components/ui/spinner/spinner.tsx +0 -21
  148. package/dist/frontend-core/template/src/components/ui/switch/switch.stories.tsx +0 -33
  149. package/dist/frontend-core/template/src/components/ui/switch/switch.tsx +0 -28
  150. package/dist/frontend-core/template/src/components/ui/tabs/tabs.stories.tsx +0 -215
  151. package/dist/frontend-core/template/src/components/ui/tabs/tabs.tsx +0 -70
  152. package/dist/frontend-core/template/src/components/ui/textarea/textarea.stories.tsx +0 -138
  153. package/dist/frontend-core/template/src/components/ui/textarea/textarea.tsx +0 -40
  154. package/dist/frontend-core/template/src/components/ui/toast/toast.mdx +0 -31
  155. package/dist/frontend-core/template/src/components/ui/toast/toast.stories.tsx +0 -89
  156. package/dist/frontend-core/template/src/components/ui/toggle/toggle.stories.tsx +0 -65
  157. package/dist/frontend-core/template/src/components/ui/toggle/toggle.tsx +0 -38
  158. package/dist/frontend-core/template/src/components/ui/toggle-group/toggle-group.stories.tsx +0 -85
  159. package/dist/frontend-core/template/src/components/ui/toggle-group/toggle-group.tsx +0 -54
  160. package/dist/frontend-core/template/src/components/ui/tooltip/tooltip.stories.tsx +0 -29
  161. package/dist/frontend-core/template/src/components/ui/tooltip/tooltip.tsx +0 -29
@@ -1,1892 +0,0 @@
1
- import {
2
- type ColumnDef,
3
- getCoreRowModel,
4
- getSortedRowModel,
5
- type RowSelectionState,
6
- type SortingState,
7
- type TableOptions,
8
- type Updater,
9
- useReactTable,
10
- } from '@tanstack/react-table'
11
- import { useVirtualizer, type Virtualizer } from '@tanstack/react-virtual'
12
-
13
- import * as React from 'react'
14
-
15
- import { DataGridCell } from '@components/ui/data-grid/data-grid-cell'
16
- import type {
17
- CellPosition,
18
- ContextMenuState,
19
- NavigationDirection,
20
- RowHeightValue,
21
- SearchState,
22
- SelectionState,
23
- UpdateCell,
24
- } from '@components/ui/data-grid/data-grid-types'
25
- import { getCellKey, getRowHeightValue, parseCellKey } from '@components/ui/data-grid/data-grid-utils'
26
-
27
- const DEFAULT_ROW_HEIGHT = 'short'
28
- const OVERSCAN = 3
29
- const VIEWPORT_OFFSET = 1
30
- const MIN_COLUMN_SIZE = 60
31
- const MAX_COLUMN_SIZE = 800
32
- const SEARCH_SHORTCUT_KEY = 'f'
33
- const NON_NAVIGABLE_COLUMN_IDS = ['select', 'actions']
34
-
35
- const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect
36
-
37
- function useLazyRef<T>(fn: () => T): React.RefObject<T> {
38
- const ref = React.useRef<T | null>(null)
39
- if (ref.current === null) {
40
- ref.current = fn()
41
- }
42
- return ref as React.RefObject<T>
43
- }
44
-
45
- function useAsRef<T>(data: T) {
46
- const ref = React.useRef<T>(data)
47
-
48
- useIsomorphicLayoutEffect(() => {
49
- ref.current = data
50
- })
51
-
52
- return ref
53
- }
54
-
55
- type DataGridState = {
56
- sorting: SortingState
57
- rowHeight: RowHeightValue
58
- rowSelection: RowSelectionState
59
- selectionState: SelectionState
60
- focusedCell: CellPosition | null
61
- editingCell: CellPosition | null
62
- contextMenu: ContextMenuState
63
- searchQuery: string
64
- searchMatches: CellPosition[]
65
- matchIndex: number
66
- searchOpen: boolean
67
- lastClickedRowIndex: number | null
68
- isScrolling: boolean
69
- }
70
-
71
- type DataGridStore = {
72
- subscribe: (callback: () => void) => () => void
73
- getState: () => DataGridState
74
- setState: <K extends keyof DataGridState>(key: K, value: DataGridState[K]) => void
75
- notify: () => void
76
- batch: (fn: () => void) => void
77
- }
78
-
79
- function useStore<T>(store: DataGridStore, selector: (state: DataGridState) => T): T {
80
- const getSnapshot = React.useCallback(() => selector(store.getState()), [store, selector])
81
-
82
- return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot)
83
- }
84
-
85
- type UseDataGridProps<TData> = {
86
- onDataChange?: (data: TData[]) => void
87
- onRowAdd?: (event?: React.MouseEvent<HTMLDivElement>) =>
88
- | Partial<CellPosition>
89
- | Promise<Partial<CellPosition>>
90
- | null
91
- // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
92
- | void // void is needed here to allow functions without explicit return
93
- onRowsDelete?: (rows: TData[], rowIndices: number[]) => void | Promise<void>
94
- onCellFocus?: (args: { rowIndex: number; columnId: string }) => void
95
- rowHeight?: RowHeightValue
96
- overscan?: number
97
- autoFocus?: boolean | Partial<CellPosition>
98
- enableColumnSelection?: boolean
99
- enableSearch?: boolean
100
- } & Omit<TableOptions<TData>, 'pageCount' | 'getCoreRowModel'>
101
-
102
- function useDataGrid<TData>({
103
- columns,
104
- data,
105
- onDataChange,
106
- onRowAdd: onRowAddProp,
107
- onRowsDelete: onRowsDeleteProp,
108
- onCellFocus: onCellFocusProp,
109
- rowHeight: rowHeightProp = DEFAULT_ROW_HEIGHT,
110
- overscan = OVERSCAN,
111
- initialState,
112
- autoFocus = false,
113
- enableColumnSelection = false,
114
- enableSearch = false,
115
- ...dataGridProps
116
- }: UseDataGridProps<TData>) {
117
- const dataGridRef = React.useRef<HTMLDivElement>(null)
118
- const tableRef = React.useRef<ReturnType<typeof useReactTable<TData>>>(null)
119
- const rowVirtualizerRef = React.useRef<Virtualizer<HTMLDivElement, Element>>(null)
120
- const headerRef = React.useRef<HTMLDivElement>(null)
121
- const rowMapRef = React.useRef<Map<number, HTMLDivElement>>(new Map())
122
- const footerRef = React.useRef<HTMLDivElement>(null)
123
-
124
- const dataGridPropsRef = useAsRef(dataGridProps)
125
- const listenersRef = useLazyRef(() => new Set<() => void>())
126
-
127
- const stateRef = useLazyRef<DataGridState>(() => {
128
- return {
129
- sorting: initialState?.sorting ?? [],
130
- rowHeight: rowHeightProp,
131
- rowSelection: initialState?.rowSelection ?? {},
132
- selectionState: {
133
- selectedCells: new Set(),
134
- selectionRange: null,
135
- isSelecting: false,
136
- },
137
- focusedCell: null,
138
- editingCell: null,
139
- contextMenu: {
140
- open: false,
141
- x: 0,
142
- y: 0,
143
- },
144
- searchQuery: '',
145
- searchMatches: [],
146
- matchIndex: -1,
147
- searchOpen: false,
148
- lastClickedRowIndex: null,
149
- isScrolling: false,
150
- }
151
- })
152
-
153
- const store = React.useMemo<DataGridStore>(() => {
154
- let isBatching = false
155
- let pendingNotification = false
156
-
157
- return {
158
- subscribe: (callback) => {
159
- listenersRef.current.add(callback)
160
- return () => listenersRef.current.delete(callback)
161
- },
162
- getState: () => stateRef.current,
163
- setState: (key, value) => {
164
- if (Object.is(stateRef.current[key], value)) {
165
- return
166
- }
167
- stateRef.current[key] = value
168
-
169
- if (isBatching) {
170
- pendingNotification = true
171
- } else {
172
- if (!pendingNotification) {
173
- pendingNotification = true
174
- queueMicrotask(() => {
175
- pendingNotification = false
176
- store.notify()
177
- })
178
- }
179
- }
180
- },
181
- notify: () => {
182
- for (const listener of listenersRef.current) {
183
- listener()
184
- }
185
- },
186
- batch: (fn) => {
187
- if (isBatching) {
188
- fn()
189
- return
190
- }
191
-
192
- isBatching = true
193
- const wasPending = pendingNotification
194
- pendingNotification = false
195
-
196
- try {
197
- fn()
198
- } finally {
199
- isBatching = false
200
- if (pendingNotification || wasPending) {
201
- pendingNotification = false
202
- store.notify()
203
- }
204
- }
205
- },
206
- }
207
- }, [listenersRef, stateRef])
208
-
209
- React.useEffect(() => {
210
- store.setState('rowHeight', rowHeightProp)
211
- }, [rowHeightProp, store])
212
-
213
- const focusedCell = useStore(store, (state) => state.focusedCell)
214
- const editingCell = useStore(store, (state) => state.editingCell)
215
- const selectionState = useStore(store, (state) => state.selectionState)
216
- const searchQuery = useStore(store, (state) => state.searchQuery)
217
- const searchMatches = useStore(store, (state) => state.searchMatches)
218
- const matchIndex = useStore(store, (state) => state.matchIndex)
219
- const searchOpen = useStore(store, (state) => state.searchOpen)
220
- const sorting = useStore(store, (state) => state.sorting)
221
- const rowSelection = useStore(store, (state) => state.rowSelection)
222
- const contextMenu = useStore(store, (state) => state.contextMenu)
223
- const rowHeight = useStore(store, (state) => state.rowHeight)
224
- const isScrolling = useStore(store, (state) => state.isScrolling)
225
-
226
- const rowHeightValue = getRowHeightValue(rowHeight)
227
-
228
- const columnIds = React.useMemo(() => {
229
- return columns
230
- .map((c) => {
231
- if (c.id) {
232
- return c.id
233
- }
234
- if ('accessorKey' in c) {
235
- return c.accessorKey as string
236
- }
237
- return undefined
238
- })
239
- .filter((id): id is string => Boolean(id))
240
- }, [columns])
241
-
242
- // Build a storage key for this table. We use the current pathname plus the column ids so different tables/pages get separate storage slots
243
- const storageKey = React.useMemo(() => {
244
- if (typeof window === 'undefined') {
245
- return undefined
246
- }
247
- const path = window.location.pathname || 'unknown'
248
- const cols = columnIds.join('|')
249
- return `pxl.dataGrid:${path}:${cols}`
250
- }, [columnIds])
251
-
252
- // Try to load persisted table state (order/visibility/pinning) from localStorage and merge it into the initial state the table will be created with.
253
- const persistedState = React.useMemo(() => {
254
- if (!storageKey) {
255
- return undefined
256
- }
257
- try {
258
- const raw = localStorage.getItem(storageKey)
259
- if (!raw) {
260
- return undefined
261
- }
262
- const parsed = JSON.parse(raw)
263
- return parsed
264
- } catch (_) {
265
- return undefined
266
- }
267
- }, [storageKey])
268
-
269
- // Merge persisted state (if any) into the table initial state so stored column order / visibility / pinning get reapplied on load.
270
- const mergedInitialState = React.useMemo(() => {
271
- const base = { ...(initialState ?? {}) } as Record<string, unknown>
272
-
273
- if (persistedState) {
274
- try {
275
- if (persistedState.columnOrder) {
276
- base.columnOrder = persistedState.columnOrder
277
- }
278
- if (persistedState.columnVisibility) {
279
- base.columnVisibility = persistedState.columnVisibility
280
- }
281
- if (persistedState.columnPinning) {
282
- base.columnPinning = persistedState.columnPinning
283
- }
284
- } catch (_) {
285
- // ignore malformed persisted state
286
- }
287
- }
288
-
289
- return base as typeof initialState
290
- }, [initialState, persistedState])
291
-
292
- // Derive the current visible, ordered column ids from the table when available.
293
- // This respects `table.setColumnOrder(...)` and column visibility.
294
- const getNavigableColumnIds = React.useCallback(() => {
295
- const t = tableRef.current
296
- if (t) {
297
- return t
298
- .getVisibleLeafColumns()
299
- .map((c) => c.id)
300
- .filter((c) => !NON_NAVIGABLE_COLUMN_IDS.includes(c))
301
- }
302
- return columnIds.filter((c) => !NON_NAVIGABLE_COLUMN_IDS.includes(c))
303
- }, [columnIds])
304
-
305
- const onDataUpdate = React.useCallback(
306
- (updates: UpdateCell | UpdateCell[]) => {
307
- const updateArray = Array.isArray(updates) ? updates : [updates]
308
-
309
- if (updateArray.length === 0) {
310
- return
311
- }
312
-
313
- const currentTable = tableRef.current
314
- const rows = currentTable?.getRowModel().rows
315
-
316
- const rowUpdatesMap = new Map<number, Omit<UpdateCell, 'rowIndex'>[]>()
317
-
318
- for (const update of updateArray) {
319
- if (!rows || !currentTable) {
320
- const existingUpdates = rowUpdatesMap.get(update.rowIndex) ?? []
321
- existingUpdates.push({
322
- columnId: update.columnId,
323
- value: update.value,
324
- })
325
- rowUpdatesMap.set(update.rowIndex, existingUpdates)
326
- } else {
327
- const row = rows[update.rowIndex]
328
- if (!row) {
329
- continue
330
- }
331
-
332
- const originalData = row.original
333
- const originalRowIndex = data.indexOf(originalData)
334
- if (originalRowIndex === -1) {
335
- continue
336
- }
337
-
338
- const existingUpdates = rowUpdatesMap.get(originalRowIndex) ?? []
339
- existingUpdates.push({
340
- columnId: update.columnId,
341
- value: update.value,
342
- })
343
- rowUpdatesMap.set(originalRowIndex, existingUpdates)
344
- }
345
- }
346
- // Also notify any per-cell change handler if present on the table meta.
347
- const currentMeta = tableRef.current?.options.meta
348
- if (currentMeta?.onCellChange) {
349
- for (const u of updateArray) {
350
- try {
351
- currentMeta.onCellChange(u)
352
- } catch (_) {
353
- // Ignore handler errors here to avoid breaking the grid behavior
354
- }
355
- }
356
- }
357
-
358
- const newData = data.map((row, index) => {
359
- const updates = rowUpdatesMap.get(index)
360
- if (!updates) {
361
- return row
362
- }
363
-
364
- const updatedRow = { ...row } as Record<string, unknown>
365
- for (const { columnId, value } of updates) {
366
- updatedRow[columnId] = value
367
- }
368
- return updatedRow as TData
369
- })
370
-
371
- onDataChange?.(newData)
372
- },
373
- [data, onDataChange],
374
- )
375
-
376
- const getIsCellSelected = React.useCallback(
377
- (rowIndex: number, columnId: string) => {
378
- return selectionState.selectedCells.has(getCellKey(rowIndex, columnId))
379
- },
380
- [selectionState.selectedCells],
381
- )
382
-
383
- const clearSelection = React.useCallback(() => {
384
- store.batch(() => {
385
- store.setState('selectionState', {
386
- selectedCells: new Set(),
387
- selectionRange: null,
388
- isSelecting: false,
389
- })
390
- store.setState('rowSelection', {})
391
- })
392
- }, [store])
393
-
394
- const selectAll = React.useCallback(() => {
395
- const allCells = new Set<string>()
396
- const currentTable = tableRef.current
397
- const rows = currentTable?.getRowModel().rows ?? []
398
- const rowCount = rows.length ?? data.length
399
-
400
- for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
401
- for (const columnId of columnIds) {
402
- allCells.add(getCellKey(rowIndex, columnId))
403
- }
404
- }
405
-
406
- const firstColumnId = columnIds[0]
407
- const lastColumnId = columnIds[columnIds.length - 1]
408
-
409
- store.setState('selectionState', {
410
- selectedCells: allCells,
411
- selectionRange:
412
- columnIds.length > 0 && rowCount > 0 && firstColumnId && lastColumnId
413
- ? {
414
- start: { rowIndex: 0, columnId: firstColumnId },
415
- end: { rowIndex: rowCount - 1, columnId: lastColumnId },
416
- }
417
- : null,
418
- isSelecting: false,
419
- })
420
- }, [columnIds, data.length, store])
421
-
422
- const selectColumn = React.useCallback(
423
- (columnId: string) => {
424
- const currentTable = tableRef.current
425
- const rows = currentTable?.getRowModel().rows ?? []
426
- const rowCount = rows.length ?? data.length
427
-
428
- if (rowCount === 0) {
429
- return
430
- }
431
-
432
- const selectedCells = new Set<string>()
433
-
434
- for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
435
- selectedCells.add(getCellKey(rowIndex, columnId))
436
- }
437
-
438
- store.setState('selectionState', {
439
- selectedCells,
440
- selectionRange: {
441
- start: { rowIndex: 0, columnId },
442
- end: { rowIndex: rowCount - 1, columnId },
443
- },
444
- isSelecting: false,
445
- })
446
- },
447
- [data.length, store],
448
- )
449
-
450
- const selectRange = React.useCallback(
451
- (start: CellPosition, end: CellPosition, isSelecting = false) => {
452
- // Use the current visible/displayed column order so drag selection
453
- // follows the UI when users reorder columns.
454
- const visibleCols = getNavigableColumnIds()
455
- const startColIndex = visibleCols.indexOf(start.columnId)
456
- const endColIndex = visibleCols.indexOf(end.columnId)
457
-
458
- const minRow = Math.min(start.rowIndex, end.rowIndex)
459
- const maxRow = Math.max(start.rowIndex, end.rowIndex)
460
- const minCol = Math.min(startColIndex, endColIndex)
461
- const maxCol = Math.max(startColIndex, endColIndex)
462
-
463
- const selectedCells = new Set<string>()
464
-
465
- for (let rowIndex = minRow; rowIndex <= maxRow; rowIndex++) {
466
- for (let colIndex = minCol; colIndex <= maxCol; colIndex++) {
467
- const columnId = visibleCols[colIndex]
468
- if (columnId) {
469
- selectedCells.add(getCellKey(rowIndex, columnId))
470
- }
471
- }
472
- }
473
-
474
- store.setState('selectionState', {
475
- selectedCells,
476
- selectionRange: { start, end },
477
- isSelecting,
478
- })
479
- },
480
- [getNavigableColumnIds, store],
481
- )
482
-
483
- const focusCell = React.useCallback(
484
- (rowIndex: number, columnId: string) => {
485
- store.batch(() => {
486
- store.setState('focusedCell', { rowIndex, columnId })
487
- store.setState('editingCell', null)
488
- })
489
-
490
- // Notify parent component about cell focus change
491
- onCellFocusProp?.({ rowIndex, columnId })
492
-
493
- const currentState = store.getState()
494
-
495
- if (currentState.searchOpen) {
496
- return
497
- }
498
-
499
- if (dataGridRef.current && document.activeElement !== dataGridRef.current) {
500
- dataGridRef.current.focus()
501
- }
502
- },
503
- [store, onCellFocusProp],
504
- )
505
-
506
- const onRowsDelete = React.useCallback(
507
- async (rowIndices: number[]) => {
508
- if (!onRowsDeleteProp || rowIndices.length === 0) {
509
- return
510
- }
511
-
512
- const currentTable = tableRef.current
513
- const rows = currentTable?.getRowModel().rows
514
-
515
- if (!rows || rows.length === 0) {
516
- return
517
- }
518
-
519
- const currentState = store.getState()
520
- const currentFocusedColumn = currentState.focusedCell?.columnId ?? getNavigableColumnIds()[0]
521
-
522
- const minDeletedRowIndex = Math.min(...rowIndices)
523
-
524
- const rowsToDelete: TData[] = []
525
- for (const rowIndex of rowIndices) {
526
- const row = rows[rowIndex]
527
- if (row) {
528
- rowsToDelete.push(row.original)
529
- }
530
- }
531
-
532
- await onRowsDeleteProp(rowsToDelete, rowIndices)
533
-
534
- store.batch(() => {
535
- store.setState('selectionState', {
536
- selectedCells: new Set(),
537
- selectionRange: null,
538
- isSelecting: false,
539
- })
540
- store.setState('rowSelection', {})
541
- store.setState('editingCell', null)
542
- })
543
-
544
- requestAnimationFrame(() => {
545
- const currentTable = tableRef.current
546
- const currentRows = currentTable?.getRowModel().rows ?? []
547
- const newRowCount = currentRows.length ?? data.length
548
-
549
- if (newRowCount > 0 && currentFocusedColumn) {
550
- const targetRowIndex = Math.min(minDeletedRowIndex, newRowCount - 1)
551
- focusCell(targetRowIndex, currentFocusedColumn)
552
- }
553
- })
554
- },
555
- [onRowsDeleteProp, data.length, store, getNavigableColumnIds, focusCell],
556
- )
557
-
558
- const navigateCell = React.useCallback(
559
- (direction: NavigationDirection) => {
560
- const currentState = store.getState()
561
- if (!currentState.focusedCell) {
562
- return
563
- }
564
-
565
- const { rowIndex, columnId } = currentState.focusedCell
566
- const _navCols = getNavigableColumnIds()
567
- const currentColIndex = _navCols.indexOf(columnId)
568
- const rowVirtualizer = rowVirtualizerRef.current
569
- const currentTable = tableRef.current
570
- const rows = currentTable?.getRowModel().rows ?? []
571
- const rowCount = rows.length ?? data.length
572
-
573
- let newRowIndex = rowIndex
574
- let newColumnId = columnId
575
-
576
- switch (direction) {
577
- case 'up':
578
- newRowIndex = Math.max(0, rowIndex - 1)
579
- break
580
- case 'down':
581
- newRowIndex = Math.min(rowCount - 1, rowIndex + 1)
582
- break
583
- case 'left':
584
- if (currentColIndex > 0) {
585
- const prevColumnId = _navCols[currentColIndex - 1]
586
- if (prevColumnId) {
587
- newColumnId = prevColumnId
588
- }
589
- }
590
- break
591
- case 'right':
592
- if (currentColIndex < _navCols.length - 1) {
593
- const nextColumnId = _navCols[currentColIndex + 1]
594
- if (nextColumnId) {
595
- newColumnId = nextColumnId
596
- }
597
- }
598
- break
599
- case 'home': {
600
- const _navCols2 = getNavigableColumnIds()
601
- if (_navCols2.length > 0) {
602
- newColumnId = _navCols2[0] ?? columnId
603
- }
604
- break
605
- }
606
- case 'end': {
607
- const _navCols3 = getNavigableColumnIds()
608
- if (_navCols3.length > 0) {
609
- newColumnId = _navCols3[_navCols3.length - 1] ?? columnId
610
- }
611
- break
612
- }
613
- case 'ctrl+home': {
614
- newRowIndex = 0
615
- const _navCols4 = getNavigableColumnIds()
616
- if (_navCols4.length > 0) {
617
- newColumnId = _navCols4[0] ?? columnId
618
- }
619
- break
620
- }
621
- case 'ctrl+end': {
622
- newRowIndex = Math.max(0, rowCount - 1)
623
- const _navCols5 = getNavigableColumnIds()
624
- if (_navCols5.length > 0) {
625
- newColumnId = _navCols5[_navCols5.length - 1] ?? columnId
626
- }
627
- break
628
- }
629
- case 'pageup':
630
- if (rowVirtualizer) {
631
- const visibleRange = rowVirtualizer.getVirtualItems()
632
- const pageSize = visibleRange.length ?? 10
633
- newRowIndex = Math.max(0, rowIndex - pageSize)
634
- } else {
635
- newRowIndex = Math.max(0, rowIndex - 10)
636
- }
637
- break
638
- case 'pagedown':
639
- if (rowVirtualizer) {
640
- const visibleRange = rowVirtualizer.getVirtualItems()
641
- const pageSize = visibleRange.length ?? 10
642
- newRowIndex = Math.min(rowCount - 1, rowIndex + pageSize)
643
- } else {
644
- newRowIndex = Math.min(rowCount - 1, rowIndex + 10)
645
- }
646
- break
647
- }
648
-
649
- if (newRowIndex !== rowIndex || newColumnId !== columnId) {
650
- const rowDiff = newRowIndex - rowIndex
651
-
652
- // For single-row vertical navigation (up/down arrows)
653
- if (Math.abs(rowDiff) === 1 && (direction === 'up' || direction === 'down')) {
654
- const container = dataGridRef.current
655
- const currentRow = rowMapRef.current.get(rowIndex)
656
- const targetRow = rowMapRef.current.get(newRowIndex)
657
-
658
- if (!container || !currentRow) {
659
- // Fallback to simple focus if we can't find elements
660
- focusCell(newRowIndex, newColumnId)
661
- return
662
- }
663
-
664
- // Check viewport boundaries
665
- const containerRect = container.getBoundingClientRect()
666
- const headerHeight = headerRef.current?.getBoundingClientRect().height ?? 0
667
- const footerHeight = footerRef.current?.getBoundingClientRect().height ?? 0
668
-
669
- const viewportTop = containerRect.top + headerHeight + VIEWPORT_OFFSET
670
- const viewportBottom = containerRect.bottom - footerHeight - VIEWPORT_OFFSET
671
-
672
- // If target row already exists, check if it's visible
673
- if (targetRow) {
674
- const rowRect = targetRow.getBoundingClientRect()
675
- const isFullyVisible = rowRect.top >= viewportTop && rowRect.bottom <= viewportBottom
676
-
677
- if (isFullyVisible) {
678
- // Row is fully visible, just focus it
679
- focusCell(newRowIndex, newColumnId)
680
- return
681
- }
682
-
683
- // Row exists but not fully visible, scroll it into view
684
- focusCell(newRowIndex, newColumnId)
685
-
686
- if (direction === 'down') {
687
- // Scroll just enough to show the row at the bottom
688
- const scrollNeeded = rowRect.bottom - viewportBottom
689
- container.scrollTop += scrollNeeded
690
- } else {
691
- // Scroll just enough to show the row at the top
692
- const scrollNeeded = viewportTop - rowRect.top
693
- container.scrollTop -= scrollNeeded
694
- }
695
- return
696
- }
697
-
698
- // Target row is not rendered yet
699
- // Focus immediately so the ring appears as the row is revealed
700
- focusCell(newRowIndex, newColumnId)
701
-
702
- // Scroll by exactly one row height to reveal it smoothly
703
- if (direction === 'down') {
704
- container.scrollTop += rowHeightValue
705
- } else {
706
- // For arrow up, ensure we don't go below 0
707
- const currentScrollTop = container.scrollTop
708
- const targetScrollTop = Math.max(0, currentScrollTop - rowHeightValue)
709
- container.scrollTop = targetScrollTop
710
- }
711
- return
712
- }
713
-
714
- // For larger jumps (page up/down, ctrl+home/end, etc.)
715
- if (rowVirtualizer && Math.abs(rowDiff) > 1) {
716
- const align =
717
- direction === 'pageup' || direction === 'ctrl+home'
718
- ? 'start'
719
- : direction === 'pagedown' || direction === 'ctrl+end'
720
- ? 'end'
721
- : 'center'
722
- rowVirtualizer.scrollToIndex(newRowIndex, { align })
723
- requestAnimationFrame(() => {
724
- focusCell(newRowIndex, newColumnId)
725
- })
726
- return
727
- }
728
-
729
- // For horizontal navigation or when row is already visible
730
- focusCell(newRowIndex, newColumnId)
731
- }
732
- },
733
- [store, getNavigableColumnIds, focusCell, data.length, rowHeightValue],
734
- )
735
-
736
- const onCellEditingStart = React.useCallback(
737
- (rowIndex: number, columnId: string) => {
738
- store.batch(() => {
739
- store.setState('focusedCell', { rowIndex, columnId })
740
- store.setState('editingCell', { rowIndex, columnId })
741
- })
742
- },
743
- [store],
744
- )
745
-
746
- const onCellEditingStop = React.useCallback(
747
- (opts?: { moveToNextRow?: boolean; direction?: NavigationDirection }) => {
748
- const currentState = store.getState()
749
- const currentEditing = currentState.editingCell
750
-
751
- store.setState('editingCell', null)
752
-
753
- if (opts?.moveToNextRow && currentEditing) {
754
- const { rowIndex, columnId } = currentEditing
755
- const currentTable = tableRef.current
756
- const rows = currentTable?.getRowModel().rows ?? []
757
- const rowCount = rows.length ?? data.length
758
-
759
- const nextRowIndex = rowIndex + 1
760
- if (nextRowIndex < rowCount) {
761
- requestAnimationFrame(() => {
762
- focusCell(nextRowIndex, columnId)
763
- })
764
- }
765
- } else if (opts?.direction && currentEditing) {
766
- // Focus the current editing cell first, then navigate
767
- const { rowIndex, columnId } = currentEditing
768
- focusCell(rowIndex, columnId)
769
- requestAnimationFrame(() => {
770
- navigateCell(opts.direction ?? 'right')
771
- })
772
- }
773
- },
774
- [store, data.length, focusCell, navigateCell],
775
- )
776
-
777
- const onSearchOpenChange = React.useCallback(
778
- (open: boolean) => {
779
- if (open) {
780
- store.setState('searchOpen', true)
781
- return
782
- }
783
-
784
- const currentState = store.getState()
785
- const currentMatch = currentState.matchIndex >= 0 && currentState.searchMatches[currentState.matchIndex]
786
-
787
- store.batch(() => {
788
- store.setState('searchOpen', false)
789
- store.setState('searchQuery', '')
790
- store.setState('searchMatches', [])
791
- store.setState('matchIndex', -1)
792
-
793
- if (currentMatch) {
794
- store.setState('focusedCell', {
795
- rowIndex: currentMatch.rowIndex,
796
- columnId: currentMatch.columnId,
797
- })
798
- }
799
- })
800
-
801
- if (dataGridRef.current && document.activeElement !== dataGridRef.current) {
802
- dataGridRef.current.focus()
803
- }
804
- },
805
- [store],
806
- )
807
-
808
- const onSearch = React.useCallback(
809
- (query: string) => {
810
- if (!query.trim()) {
811
- store.batch(() => {
812
- store.setState('searchMatches', [])
813
- store.setState('matchIndex', -1)
814
- })
815
- return
816
- }
817
-
818
- const matches: CellPosition[] = []
819
- const currentTable = tableRef.current
820
- const rows = currentTable?.getRowModel().rows ?? []
821
-
822
- const lowerQuery = query.toLowerCase()
823
-
824
- for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
825
- const row = rows[rowIndex]
826
- if (!row) {
827
- continue
828
- }
829
-
830
- for (const columnId of columnIds) {
831
- const cell = row.getVisibleCells().find((c) => c.column.id === columnId)
832
- if (!cell) {
833
- continue
834
- }
835
-
836
- const value = cell.getValue()
837
- const stringValue = String(value ?? '').toLowerCase()
838
-
839
- if (stringValue.includes(lowerQuery)) {
840
- matches.push({ rowIndex, columnId })
841
- }
842
- }
843
- }
844
-
845
- store.batch(() => {
846
- store.setState('searchMatches', matches)
847
- store.setState('matchIndex', matches.length > 0 ? 0 : -1)
848
- })
849
-
850
- // Scroll to first match but don't focus it (to keep focus in search input)
851
- if (matches.length > 0 && matches[0]) {
852
- const firstMatch = matches[0]
853
- rowVirtualizerRef.current?.scrollToIndex(firstMatch.rowIndex, {
854
- align: 'center',
855
- })
856
- }
857
- },
858
- [columnIds, store],
859
- )
860
-
861
- const onSearchQueryChange = React.useCallback((query: string) => store.setState('searchQuery', query), [store])
862
-
863
- const onNavigateToPrevMatch = React.useCallback(() => {
864
- const currentState = store.getState()
865
- if (currentState.searchMatches.length === 0) {
866
- return
867
- }
868
-
869
- const prevIndex =
870
- currentState.matchIndex - 1 < 0 ? currentState.searchMatches.length - 1 : currentState.matchIndex - 1
871
- const match = currentState.searchMatches[prevIndex]
872
-
873
- if (match) {
874
- rowVirtualizerRef.current?.scrollToIndex(match.rowIndex, {
875
- align: 'center',
876
- })
877
-
878
- requestAnimationFrame(() => {
879
- store.setState('matchIndex', prevIndex)
880
- requestAnimationFrame(() => {
881
- focusCell(match.rowIndex, match.columnId)
882
- })
883
- })
884
- }
885
- }, [store, focusCell])
886
-
887
- const onNavigateToNextMatch = React.useCallback(() => {
888
- const currentState = store.getState()
889
- if (currentState.searchMatches.length === 0) {
890
- return
891
- }
892
-
893
- const nextIndex = (currentState.matchIndex + 1) % currentState.searchMatches.length
894
- const match = currentState.searchMatches[nextIndex]
895
-
896
- if (match) {
897
- rowVirtualizerRef.current?.scrollToIndex(match.rowIndex, {
898
- align: 'center',
899
- })
900
-
901
- requestAnimationFrame(() => {
902
- store.setState('matchIndex', nextIndex)
903
- requestAnimationFrame(() => {
904
- focusCell(match.rowIndex, match.columnId)
905
- })
906
- })
907
- }
908
- }, [store, focusCell])
909
-
910
- const getIsSearchMatch = React.useCallback(
911
- (rowIndex: number, columnId: string) => {
912
- return searchMatches.some((match) => match.rowIndex === rowIndex && match.columnId === columnId)
913
- },
914
- [searchMatches],
915
- )
916
-
917
- const getIsActiveSearchMatch = React.useCallback(
918
- (rowIndex: number, columnId: string) => {
919
- if (matchIndex < 0) {
920
- return false
921
- }
922
- const currentMatch = searchMatches[matchIndex]
923
- return currentMatch?.rowIndex === rowIndex && currentMatch?.columnId === columnId
924
- },
925
- [searchMatches, matchIndex],
926
- )
927
-
928
- const blurCell = React.useCallback(() => {
929
- const currentState = store.getState()
930
- if (currentState.editingCell && document.activeElement instanceof HTMLElement) {
931
- document.activeElement.blur()
932
- }
933
-
934
- store.batch(() => {
935
- store.setState('focusedCell', null)
936
- store.setState('editingCell', null)
937
- })
938
- }, [store])
939
-
940
- const onCellClick = React.useCallback(
941
- (rowIndex: number, columnId: string, event?: React.MouseEvent) => {
942
- // Ignore right-click (button 2) - let onCellContextMenu handle it
943
- if (event?.button === 2) {
944
- return
945
- }
946
-
947
- const currentState = store.getState()
948
- const currentFocused = currentState.focusedCell
949
-
950
- if (event) {
951
- if (event.ctrlKey || event.metaKey) {
952
- event.preventDefault()
953
- const cellKey = getCellKey(rowIndex, columnId)
954
- const newSelectedCells = new Set(currentState.selectionState.selectedCells)
955
-
956
- if (newSelectedCells.has(cellKey)) {
957
- newSelectedCells.delete(cellKey)
958
- } else {
959
- newSelectedCells.add(cellKey)
960
- }
961
-
962
- store.setState('selectionState', {
963
- selectedCells: newSelectedCells,
964
- selectionRange: null,
965
- isSelecting: false,
966
- })
967
- focusCell(rowIndex, columnId)
968
- return
969
- }
970
-
971
- if (event.shiftKey && currentState.focusedCell) {
972
- event.preventDefault()
973
- selectRange(currentState.focusedCell, { rowIndex, columnId })
974
- return
975
- }
976
- }
977
-
978
- // Clear selection if there are selected cells or rows
979
- const hasSelectedCells = currentState.selectionState.selectedCells.size > 0
980
- const hasSelectedRows = Object.keys(currentState.rowSelection).length > 0
981
-
982
- if (hasSelectedCells && !currentState.selectionState.isSelecting) {
983
- // If there's a cell selection but we're not actively selecting (drag just finished),
984
- // don't clear it - keep the selection
985
- // Only clear if clicking elsewhere
986
- const cellKey = getCellKey(rowIndex, columnId)
987
- const isClickingSelectedCell = currentState.selectionState.selectedCells.has(cellKey)
988
-
989
- if (!isClickingSelectedCell) {
990
- clearSelection()
991
- } else {
992
- // Clicking on an already selected cell - just focus it
993
- focusCell(rowIndex, columnId)
994
- return
995
- }
996
- } else if (hasSelectedRows && columnId !== 'select') {
997
- // If there are selected rows but we're clicking on a non-checkbox cell, clear selections
998
- clearSelection()
999
- }
1000
-
1001
- if (currentFocused?.rowIndex === rowIndex && currentFocused?.columnId === columnId) {
1002
- onCellEditingStart(rowIndex, columnId)
1003
- } else {
1004
- focusCell(rowIndex, columnId)
1005
- }
1006
- },
1007
- [store, focusCell, onCellEditingStart, selectRange, clearSelection],
1008
- )
1009
-
1010
- const onCellDoubleClick = React.useCallback(
1011
- (rowIndex: number, columnId: string, event?: React.MouseEvent) => {
1012
- if (event?.defaultPrevented) {
1013
- return
1014
- }
1015
-
1016
- onCellEditingStart(rowIndex, columnId)
1017
- },
1018
- [onCellEditingStart],
1019
- )
1020
-
1021
- const onCellMouseDown = React.useCallback(
1022
- (rowIndex: number, columnId: string, event: React.MouseEvent) => {
1023
- // Ignore right-click (button 2) - let onCellContextMenu handle it
1024
- if (event.button === 2) {
1025
- return
1026
- }
1027
-
1028
- event.preventDefault()
1029
-
1030
- // Only start drag selection if no modifier keys are pressed
1031
- // Clear any existing selection and prepare for potential drag
1032
- if (!event.ctrlKey && !event.metaKey && !event.shiftKey) {
1033
- store.batch(() => {
1034
- store.setState('selectionState', {
1035
- selectedCells: new Set(),
1036
- selectionRange: {
1037
- start: { rowIndex, columnId },
1038
- end: { rowIndex, columnId },
1039
- },
1040
- isSelecting: true,
1041
- })
1042
- store.setState('rowSelection', {})
1043
- })
1044
- }
1045
- },
1046
- [store],
1047
- )
1048
-
1049
- const onCellMouseEnter = React.useCallback(
1050
- (rowIndex: number, columnId: string, _event: React.MouseEvent) => {
1051
- const currentState = store.getState()
1052
- if (currentState.selectionState.isSelecting && currentState.selectionState.selectionRange) {
1053
- const start = currentState.selectionState.selectionRange.start
1054
- const end = { rowIndex, columnId }
1055
-
1056
- if (
1057
- currentState.focusedCell?.rowIndex !== start.rowIndex ||
1058
- currentState.focusedCell?.columnId !== start.columnId
1059
- ) {
1060
- focusCell(start.rowIndex, start.columnId)
1061
- }
1062
-
1063
- selectRange(start, end, true)
1064
- }
1065
- },
1066
- [store, selectRange, focusCell],
1067
- )
1068
-
1069
- const onCellMouseUp = React.useCallback(() => {
1070
- const currentState = store.getState()
1071
- store.setState('selectionState', {
1072
- ...currentState.selectionState,
1073
- isSelecting: false,
1074
- })
1075
- }, [store])
1076
-
1077
- const onCellContextMenu = React.useCallback(
1078
- (rowIndex: number, columnId: string, event: React.MouseEvent) => {
1079
- event.preventDefault()
1080
- event.stopPropagation()
1081
-
1082
- const currentState = store.getState()
1083
- const cellKey = getCellKey(rowIndex, columnId)
1084
- const isTargetCellSelected = currentState.selectionState.selectedCells.has(cellKey)
1085
-
1086
- // If right-clicking on a non-selected cell, select only that cell
1087
- if (!isTargetCellSelected) {
1088
- store.batch(() => {
1089
- store.setState('selectionState', {
1090
- selectedCells: new Set([cellKey]),
1091
- selectionRange: {
1092
- start: { rowIndex, columnId },
1093
- end: { rowIndex, columnId },
1094
- },
1095
- isSelecting: false,
1096
- })
1097
- store.setState('focusedCell', { rowIndex, columnId })
1098
- })
1099
- }
1100
-
1101
- // Open context menu at cursor position
1102
- store.setState('contextMenu', {
1103
- open: true,
1104
- x: event.clientX,
1105
- y: event.clientY,
1106
- })
1107
- },
1108
- [store],
1109
- )
1110
-
1111
- const onContextMenuOpenChange = React.useCallback(
1112
- (open: boolean) => {
1113
- if (!open) {
1114
- const currentMenu = store.getState().contextMenu
1115
- store.setState('contextMenu', {
1116
- open: false,
1117
- x: currentMenu.x,
1118
- y: currentMenu.y,
1119
- })
1120
- }
1121
- },
1122
- [store],
1123
- )
1124
-
1125
- const onDataGridKeyDown = React.useCallback(
1126
- (event: KeyboardEvent) => {
1127
- const currentState = store.getState()
1128
- const { key, ctrlKey, metaKey, shiftKey } = event
1129
- const isCtrlPressed = ctrlKey || metaKey
1130
-
1131
- // Handle Cmd+F / Ctrl+F to open search (highest priority, works even when editing)
1132
- if (enableSearch && isCtrlPressed && key === SEARCH_SHORTCUT_KEY) {
1133
- event.preventDefault()
1134
- onSearchOpenChange(true)
1135
- return
1136
- }
1137
-
1138
- // Handle search navigation when search is open
1139
- if (enableSearch && currentState.searchOpen && !currentState.editingCell) {
1140
- if (key === 'Enter') {
1141
- event.preventDefault()
1142
- if (shiftKey) {
1143
- onNavigateToPrevMatch()
1144
- } else {
1145
- onNavigateToNextMatch()
1146
- }
1147
- return
1148
- }
1149
- if (key === 'Escape') {
1150
- event.preventDefault()
1151
- onSearchOpenChange(false)
1152
- return
1153
- }
1154
- // When search is open, don't let data grid handle any other keys
1155
- // (they should only affect the search input)
1156
- return
1157
- }
1158
-
1159
- if (currentState.editingCell) {
1160
- return
1161
- }
1162
-
1163
- if (!currentState.focusedCell) {
1164
- return
1165
- }
1166
-
1167
- let direction: NavigationDirection | null = null
1168
-
1169
- if (isCtrlPressed && key === 'a') {
1170
- event.preventDefault()
1171
- selectAll()
1172
- return
1173
- }
1174
-
1175
- if (key === 'Delete' || key === 'Backspace') {
1176
- if (currentState.selectionState.selectedCells.size > 0) {
1177
- event.preventDefault()
1178
- // If any selected cell is not editable, do nothing (mirror context-menu Clear behavior)
1179
- const currentTable = tableRef.current
1180
- const visibleCols = currentTable?.getVisibleLeafColumns() ?? []
1181
- let canClear = true
1182
- for (const cellKey of currentState.selectionState.selectedCells) {
1183
- if (!canClear) {
1184
- break
1185
- }
1186
- const { columnId } = parseCellKey(cellKey)
1187
- const col = visibleCols.find((c) => c.id === columnId)
1188
- const editable = col?.columnDef?.meta?.editable
1189
- if (editable === false) {
1190
- canClear = false
1191
- }
1192
- }
1193
-
1194
- if (!canClear) {
1195
- return
1196
- }
1197
-
1198
- const updates: {
1199
- rowIndex: number
1200
- columnId: string
1201
- value: unknown
1202
- }[] = []
1203
-
1204
- currentState.selectionState.selectedCells.forEach((cellKey) => {
1205
- const { rowIndex, columnId } = parseCellKey(cellKey)
1206
- updates.push({ rowIndex, columnId, value: '' })
1207
- })
1208
-
1209
- onDataUpdate(updates)
1210
- clearSelection()
1211
- }
1212
- return
1213
- }
1214
-
1215
- switch (key) {
1216
- case 'ArrowUp':
1217
- direction = 'up'
1218
- break
1219
- case 'ArrowDown':
1220
- direction = 'down'
1221
- break
1222
- case 'ArrowLeft':
1223
- direction = 'left'
1224
- break
1225
- case 'ArrowRight':
1226
- direction = 'right'
1227
- break
1228
- case 'Home':
1229
- direction = isCtrlPressed ? 'ctrl+home' : 'home'
1230
- break
1231
- case 'End':
1232
- direction = isCtrlPressed ? 'ctrl+end' : 'end'
1233
- break
1234
- case 'PageUp':
1235
- direction = 'pageup'
1236
- break
1237
- case 'PageDown':
1238
- direction = 'pagedown'
1239
- break
1240
- case 'Escape':
1241
- event.preventDefault()
1242
- if (currentState.selectionState.selectedCells.size > 0 || Object.keys(currentState.rowSelection).length > 0) {
1243
- clearSelection()
1244
- } else {
1245
- blurCell()
1246
- }
1247
- return
1248
- case 'Tab':
1249
- event.preventDefault()
1250
- direction = event.shiftKey ? 'left' : 'right'
1251
- break
1252
- }
1253
-
1254
- if (direction) {
1255
- event.preventDefault()
1256
-
1257
- // Tab navigation should not trigger selection, even with Shift
1258
- if (shiftKey && key !== 'Tab' && currentState.focusedCell) {
1259
- const _navCols = getNavigableColumnIds()
1260
- const currentColIndex = _navCols.indexOf(currentState.focusedCell.columnId)
1261
- let newRowIndex = currentState.focusedCell.rowIndex
1262
- let newColumnId = currentState.focusedCell.columnId
1263
-
1264
- switch (direction) {
1265
- case 'up':
1266
- newRowIndex = Math.max(0, currentState.focusedCell.rowIndex - 1)
1267
- break
1268
- case 'down':
1269
- newRowIndex = Math.min(
1270
- (tableRef.current?.getRowModel().rows.length || data.length) - 1,
1271
- currentState.focusedCell.rowIndex + 1,
1272
- )
1273
- break
1274
- case 'left':
1275
- if (currentColIndex > 0) {
1276
- const prevColumnId = _navCols[currentColIndex - 1]
1277
- if (prevColumnId) {
1278
- newColumnId = prevColumnId
1279
- }
1280
- }
1281
- break
1282
- case 'right':
1283
- if (currentColIndex < _navCols.length - 1) {
1284
- const nextColumnId = _navCols[currentColIndex + 1]
1285
- if (nextColumnId) {
1286
- newColumnId = nextColumnId
1287
- }
1288
- }
1289
- break
1290
- }
1291
-
1292
- const selectionStart = currentState.selectionState.selectionRange?.start || currentState.focusedCell
1293
- selectRange(selectionStart, {
1294
- rowIndex: newRowIndex,
1295
- columnId: newColumnId,
1296
- })
1297
- focusCell(newRowIndex, newColumnId)
1298
- } else {
1299
- if (currentState.selectionState.selectedCells.size > 0) {
1300
- clearSelection()
1301
- }
1302
- navigateCell(direction)
1303
- }
1304
- }
1305
- },
1306
- [
1307
- store,
1308
- blurCell,
1309
- navigateCell,
1310
- selectAll,
1311
- onDataUpdate,
1312
- clearSelection,
1313
- getNavigableColumnIds,
1314
- data.length,
1315
- selectRange,
1316
- focusCell,
1317
- onSearchOpenChange,
1318
- onNavigateToNextMatch,
1319
- onNavigateToPrevMatch,
1320
- enableSearch,
1321
- ],
1322
- )
1323
-
1324
- const onSortingChange = React.useCallback(
1325
- (updater: Updater<SortingState>) => {
1326
- const currentState = store.getState()
1327
- const newSorting = typeof updater === 'function' ? updater(currentState.sorting) : updater
1328
- store.setState('sorting', newSorting)
1329
- },
1330
- [store],
1331
- )
1332
-
1333
- const onRowSelectionChange = React.useCallback(
1334
- (updater: Updater<RowSelectionState>) => {
1335
- const currentState = store.getState()
1336
- const newRowSelection = typeof updater === 'function' ? updater(currentState.rowSelection) : updater
1337
-
1338
- const selectedRows = Object.keys(newRowSelection).filter((key) => newRowSelection[key])
1339
-
1340
- const selectedCells = new Set<string>()
1341
- const rows = tableRef.current?.getRowModel().rows ?? []
1342
-
1343
- for (const rowId of selectedRows) {
1344
- const rowIndex = rows.findIndex((r) => r.id === rowId)
1345
- if (rowIndex === -1) {
1346
- continue
1347
- }
1348
-
1349
- for (const columnId of columnIds) {
1350
- selectedCells.add(getCellKey(rowIndex, columnId))
1351
- }
1352
- }
1353
-
1354
- store.batch(() => {
1355
- store.setState('rowSelection', newRowSelection)
1356
- store.setState('selectionState', {
1357
- selectedCells,
1358
- selectionRange: null,
1359
- isSelecting: false,
1360
- })
1361
- store.setState('focusedCell', null)
1362
- store.setState('editingCell', null)
1363
- })
1364
- },
1365
- [store, columnIds],
1366
- )
1367
-
1368
- const onRowSelect = React.useCallback(
1369
- (rowIndex: number, selected: boolean, shiftKey: boolean) => {
1370
- const currentState = store.getState()
1371
- const rows = tableRef.current?.getRowModel().rows ?? []
1372
- const currentRow = rows[rowIndex]
1373
- if (!currentRow) {
1374
- return
1375
- }
1376
-
1377
- if (shiftKey && currentState.lastClickedRowIndex !== null) {
1378
- const startIndex = Math.min(currentState.lastClickedRowIndex, rowIndex)
1379
- const endIndex = Math.max(currentState.lastClickedRowIndex, rowIndex)
1380
-
1381
- const newRowSelection: RowSelectionState = {
1382
- ...currentState.rowSelection,
1383
- }
1384
-
1385
- for (let i = startIndex; i <= endIndex; i++) {
1386
- const row = rows[i]
1387
- if (row) {
1388
- newRowSelection[row.id] = selected
1389
- }
1390
- }
1391
-
1392
- onRowSelectionChange(newRowSelection)
1393
- } else {
1394
- onRowSelectionChange({
1395
- ...currentState.rowSelection,
1396
- [currentRow.id]: selected,
1397
- })
1398
- }
1399
-
1400
- store.setState('lastClickedRowIndex', rowIndex)
1401
- },
1402
- [store, onRowSelectionChange],
1403
- )
1404
-
1405
- const onRowHeightChange = React.useCallback(
1406
- (updater: Updater<RowHeightValue>) => {
1407
- const currentState = store.getState()
1408
- const newRowHeight = typeof updater === 'function' ? updater(currentState.rowHeight) : updater
1409
- store.setState('rowHeight', newRowHeight)
1410
- },
1411
- [store],
1412
- )
1413
-
1414
- const onColumnClick = React.useCallback(
1415
- (columnId: string) => {
1416
- if (!enableColumnSelection) {
1417
- clearSelection()
1418
- return
1419
- }
1420
-
1421
- selectColumn(columnId)
1422
- },
1423
- [enableColumnSelection, selectColumn, clearSelection],
1424
- )
1425
-
1426
- const defaultColumn: Partial<ColumnDef<TData>> = React.useMemo(
1427
- () => ({
1428
- cell: DataGridCell,
1429
- minSize: MIN_COLUMN_SIZE,
1430
- maxSize: MAX_COLUMN_SIZE,
1431
- }),
1432
- [],
1433
- )
1434
-
1435
- const tableOptions = React.useMemo<TableOptions<TData>>(
1436
- () => ({
1437
- ...dataGridPropsRef.current,
1438
- data,
1439
- columns,
1440
- defaultColumn,
1441
- initialState: mergedInitialState,
1442
- state: {
1443
- ...dataGridPropsRef.current.state,
1444
- sorting,
1445
- rowSelection,
1446
- },
1447
- onRowSelectionChange,
1448
- onSortingChange,
1449
- columnResizeMode: 'onChange',
1450
- getCoreRowModel: getCoreRowModel(),
1451
- getSortedRowModel: getSortedRowModel(),
1452
- meta: {
1453
- ...dataGridPropsRef.current.meta,
1454
- dataGridRef,
1455
- focusedCell,
1456
- editingCell,
1457
- selectionState,
1458
- searchOpen,
1459
- rowHeight,
1460
- isScrolling,
1461
- getIsCellSelected,
1462
- getIsSearchMatch,
1463
- getIsActiveSearchMatch,
1464
- onRowHeightChange,
1465
- onRowSelect,
1466
- onRowsDelete: onRowsDeleteProp ? onRowsDelete : undefined,
1467
- onDataUpdate,
1468
- onColumnClick,
1469
- onCellClick,
1470
- onCellDoubleClick,
1471
- onCellMouseDown,
1472
- onCellMouseEnter,
1473
- onCellMouseUp,
1474
- onCellContextMenu,
1475
- onCellEditingStart,
1476
- onCellEditingStop,
1477
- contextMenu,
1478
- onContextMenuOpenChange,
1479
- },
1480
- }),
1481
- [
1482
- dataGridPropsRef,
1483
- data,
1484
- columns,
1485
- defaultColumn,
1486
- mergedInitialState,
1487
- sorting,
1488
- rowSelection,
1489
- onRowSelectionChange,
1490
- onSortingChange,
1491
- focusedCell,
1492
- editingCell,
1493
- selectionState,
1494
- searchOpen,
1495
- isScrolling,
1496
- getIsCellSelected,
1497
- getIsSearchMatch,
1498
- getIsActiveSearchMatch,
1499
- onDataUpdate,
1500
- onRowsDeleteProp,
1501
- onRowsDelete,
1502
- onColumnClick,
1503
- onCellClick,
1504
- onCellDoubleClick,
1505
- onCellMouseDown,
1506
- onCellMouseEnter,
1507
- onCellMouseUp,
1508
- onCellContextMenu,
1509
- onCellEditingStart,
1510
- onCellEditingStop,
1511
- contextMenu,
1512
- onContextMenuOpenChange,
1513
- rowHeight,
1514
- onRowHeightChange,
1515
- onRowSelect,
1516
- ],
1517
- )
1518
-
1519
- const table = useReactTable(tableOptions)
1520
-
1521
- if (!tableRef.current) {
1522
- tableRef.current = table
1523
- }
1524
-
1525
- // Extract columnSizing once so it can be used in hook dependency arrays
1526
- const columnSizing = table.getState().columnSizing
1527
-
1528
- // biome-ignore lint/correctness/useExhaustiveDependencies: we need to memoize the column size vars
1529
- const columnSizeVars = React.useMemo(() => {
1530
- // reference columnSizing so it is a used dependency (recomputes when column sizing changes)
1531
- void columnSizing
1532
- const headers = table.getFlatHeaders()
1533
- const colSizes: Record<string, number> = {}
1534
- for (const header of headers) {
1535
- // Prefer the current header size, but fall back to the columnDef `size`
1536
- // or the MIN_COLUMN_SIZE so the CSS variables exist on first render.
1537
- const headerSize =
1538
- (typeof header.getSize === 'function' ? header.getSize() : undefined) ??
1539
- (header.column.columnDef as any)?.size ??
1540
- MIN_COLUMN_SIZE
1541
- const colSize =
1542
- (typeof header.column.getSize === 'function' ? header.column.getSize() : undefined) ??
1543
- (header.column.columnDef as any)?.size ??
1544
- MIN_COLUMN_SIZE
1545
-
1546
- colSizes[`--header-${header.id}-size`] = headerSize
1547
- colSizes[`--col-${header.column.id}-size`] = colSize
1548
- }
1549
- return colSizes
1550
- // Recompute whenever the table's column sizing state changes so CSS vars reflect new sizes
1551
- }, [table, columnSizing])
1552
-
1553
- const rowVirtualizer = useVirtualizer({
1554
- count: table.getRowModel().rows.length,
1555
- getScrollElement: () => dataGridRef.current,
1556
- estimateSize: () => rowHeightValue,
1557
- overscan,
1558
- measureElement:
1559
- typeof window !== 'undefined' && navigator.userAgent.indexOf('Firefox') === -1
1560
- ? (element) => element?.getBoundingClientRect().height
1561
- : undefined,
1562
- onChange: (instance) => {
1563
- // Sync virtualizer's isScrolling state to our store
1564
- const virtualizerIsScrolling = instance.isScrolling
1565
- const currentIsScrolling = store.getState().isScrolling
1566
-
1567
- if (virtualizerIsScrolling !== currentIsScrolling) {
1568
- store.setState('isScrolling', virtualizerIsScrolling)
1569
- }
1570
-
1571
- // Batch DOM updates in a single animation frame
1572
- const virtualItems = instance.getVirtualItems()
1573
- if (virtualItems.length === 0) {
1574
- return
1575
- }
1576
-
1577
- requestAnimationFrame(() => {
1578
- for (const virtualRow of virtualItems) {
1579
- if (!virtualRow) {
1580
- continue
1581
- }
1582
- const rowRef = rowMapRef.current.get(virtualRow.index)
1583
- if (rowRef) {
1584
- rowRef.style.transform = `translateY(${virtualRow.start}px)`
1585
- }
1586
- }
1587
- })
1588
- },
1589
- })
1590
-
1591
- if (!rowVirtualizerRef.current) {
1592
- rowVirtualizerRef.current = rowVirtualizer
1593
- }
1594
-
1595
- const onScrollToRow = React.useCallback(
1596
- (opts: Partial<CellPosition>) => {
1597
- const rowIndex = opts?.rowIndex ?? 0
1598
- const columnId = opts?.columnId
1599
-
1600
- rowVirtualizer.scrollToIndex(rowIndex, {
1601
- align: 'center',
1602
- })
1603
-
1604
- const targetColumnId = columnId ?? getNavigableColumnIds()[0]
1605
-
1606
- if (!targetColumnId) {
1607
- return
1608
- }
1609
-
1610
- queueMicrotask(() => {
1611
- requestAnimationFrame(() => {
1612
- requestAnimationFrame(() => {
1613
- store.batch(() => {
1614
- store.setState('focusedCell', {
1615
- rowIndex,
1616
- columnId: targetColumnId,
1617
- })
1618
- store.setState('editingCell', null)
1619
- })
1620
- })
1621
- })
1622
- })
1623
- },
1624
- [rowVirtualizer, getNavigableColumnIds, store],
1625
- )
1626
-
1627
- const onRowAdd = React.useCallback(
1628
- async (event?: React.MouseEvent<HTMLDivElement>) => {
1629
- if (!onRowAddProp) {
1630
- return
1631
- }
1632
-
1633
- const result = await onRowAddProp(event)
1634
-
1635
- if (event?.defaultPrevented || result === null) {
1636
- return
1637
- }
1638
-
1639
- const currentTable = tableRef.current
1640
- const rows = currentTable?.getRowModel().rows ?? []
1641
-
1642
- if (result) {
1643
- const adjustedRowIndex = (result.rowIndex ?? 0) >= rows.length ? rows.length : result.rowIndex
1644
-
1645
- onScrollToRow({
1646
- rowIndex: adjustedRowIndex,
1647
- columnId: result.columnId,
1648
- })
1649
- return
1650
- }
1651
-
1652
- onScrollToRow({ rowIndex: rows.length })
1653
- },
1654
- [onRowAddProp, onScrollToRow],
1655
- )
1656
-
1657
- // Persist table column state (order, visibility, pinning) whenever it changes.
1658
- // Use a layout effect so persistence stays in sync with table layout updates.
1659
- useIsomorphicLayoutEffect(() => {
1660
- if (!storageKey) {
1661
- return
1662
- }
1663
-
1664
- try {
1665
- const state = table.getState()
1666
- const payload = {
1667
- columnOrder: state.columnOrder,
1668
- columnVisibility: state.columnVisibility,
1669
- columnPinning: state.columnPinning,
1670
- }
1671
- localStorage.setItem(storageKey, JSON.stringify(payload))
1672
- } catch (_) {
1673
- // ignore storage errors
1674
- }
1675
- }, [storageKey, table.getState().columnOrder, table.getState().columnVisibility, table.getState().columnPinning])
1676
-
1677
- const searchState = React.useMemo<SearchState | undefined>(() => {
1678
- if (!enableSearch) {
1679
- return undefined
1680
- }
1681
-
1682
- return {
1683
- searchMatches,
1684
- matchIndex,
1685
- searchOpen,
1686
- onSearchOpenChange,
1687
- searchQuery,
1688
- onSearchQueryChange,
1689
- onSearch,
1690
- onNavigateToNextMatch,
1691
- onNavigateToPrevMatch,
1692
- }
1693
- }, [
1694
- enableSearch,
1695
- searchMatches,
1696
- matchIndex,
1697
- searchOpen,
1698
- onSearchOpenChange,
1699
- searchQuery,
1700
- onSearchQueryChange,
1701
- onSearch,
1702
- onNavigateToNextMatch,
1703
- onNavigateToPrevMatch,
1704
- ])
1705
-
1706
- React.useEffect(() => {
1707
- const dataGridElement = dataGridRef.current
1708
- if (!dataGridElement) {
1709
- return
1710
- }
1711
-
1712
- dataGridElement.addEventListener('keydown', onDataGridKeyDown)
1713
- return () => {
1714
- dataGridElement.removeEventListener('keydown', onDataGridKeyDown)
1715
- }
1716
- }, [onDataGridKeyDown])
1717
-
1718
- React.useEffect(() => {
1719
- function onGlobalKeyDown(event: KeyboardEvent) {
1720
- const dataGridElement = dataGridRef.current
1721
- if (!dataGridElement) {
1722
- return
1723
- }
1724
-
1725
- const target = event.target
1726
- if (!(target instanceof HTMLElement)) {
1727
- return
1728
- }
1729
-
1730
- const { key, ctrlKey, metaKey } = event
1731
- const isCtrlPressed = ctrlKey || metaKey
1732
-
1733
- if (enableSearch && isCtrlPressed && key === SEARCH_SHORTCUT_KEY) {
1734
- const isInInput = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA'
1735
- const isInDataGrid = dataGridElement.contains(target)
1736
- const isInSearchInput = target.closest('[role=search]') !== null
1737
-
1738
- if (isInDataGrid || isInSearchInput || !isInInput) {
1739
- event.preventDefault()
1740
- event.stopPropagation()
1741
- onSearchOpenChange(true)
1742
-
1743
- if (!isInDataGrid && !isInSearchInput) {
1744
- requestAnimationFrame(() => {
1745
- dataGridElement.focus()
1746
- })
1747
- }
1748
- return
1749
- }
1750
- }
1751
-
1752
- const isInDataGrid = dataGridElement.contains(target)
1753
- if (!isInDataGrid) {
1754
- return
1755
- }
1756
-
1757
- if (key === 'Escape') {
1758
- const currentState = store.getState()
1759
- const hasSelections =
1760
- currentState.selectionState.selectedCells.size > 0 || Object.keys(currentState.rowSelection).length > 0
1761
-
1762
- if (hasSelections) {
1763
- event.preventDefault()
1764
- event.stopPropagation()
1765
- clearSelection()
1766
- }
1767
- }
1768
- }
1769
-
1770
- window.addEventListener('keydown', onGlobalKeyDown, true)
1771
- return () => {
1772
- window.removeEventListener('keydown', onGlobalKeyDown, true)
1773
- }
1774
- }, [enableSearch, onSearchOpenChange, store, clearSelection])
1775
-
1776
- React.useEffect(() => {
1777
- const currentState = store.getState()
1778
- if (autoFocus && data.length > 0 && columns.length > 0 && !currentState.focusedCell) {
1779
- const _nav = getNavigableColumnIds()
1780
- if (_nav.length > 0) {
1781
- const rafId = requestAnimationFrame(() => {
1782
- if (typeof autoFocus === 'object') {
1783
- const { rowIndex, columnId } = autoFocus
1784
- if (columnId) {
1785
- focusCell(rowIndex ?? 0, columnId)
1786
- }
1787
- return
1788
- }
1789
-
1790
- const firstColumnId = _nav[0]
1791
- if (firstColumnId) {
1792
- focusCell(0, firstColumnId)
1793
- }
1794
- })
1795
- return () => cancelAnimationFrame(rafId)
1796
- }
1797
- }
1798
- }, [autoFocus, data.length, columns.length, store, getNavigableColumnIds, focusCell])
1799
-
1800
- React.useEffect(() => {
1801
- function onOutsideClick(event: MouseEvent) {
1802
- if (event.button === 2) {
1803
- return
1804
- }
1805
-
1806
- if (dataGridRef.current && !dataGridRef.current.contains(event.target as Node)) {
1807
- const target = event.target
1808
- const isInsidePopover =
1809
- target instanceof HTMLElement &&
1810
- (target.closest('[data-grid-cell-editor]') || target.closest('[data-grid-popover]'))
1811
-
1812
- if (!isInsidePopover) {
1813
- blurCell()
1814
- const currentState = store.getState()
1815
- if (currentState.selectionState.selectedCells.size > 0 || Object.keys(currentState.rowSelection).length > 0) {
1816
- clearSelection()
1817
- }
1818
- }
1819
- }
1820
- }
1821
-
1822
- document.addEventListener('mousedown', onOutsideClick)
1823
- return () => {
1824
- document.removeEventListener('mousedown', onOutsideClick)
1825
- }
1826
- }, [store, blurCell, clearSelection])
1827
-
1828
- React.useEffect(() => {
1829
- function onCleanup() {
1830
- document.removeEventListener('selectstart', preventSelection)
1831
- document.removeEventListener('contextmenu', preventContextMenu)
1832
- document.body.style.userSelect = ''
1833
- }
1834
-
1835
- function preventSelection(event: Event) {
1836
- event.preventDefault()
1837
- }
1838
- function preventContextMenu(event: Event) {
1839
- event.preventDefault()
1840
- }
1841
-
1842
- const onUnsubscribe = store.subscribe(() => {
1843
- const currentState = store.getState()
1844
- if (currentState.selectionState.isSelecting) {
1845
- document.addEventListener('selectstart', preventSelection)
1846
- document.addEventListener('contextmenu', preventContextMenu)
1847
- document.body.style.userSelect = 'none'
1848
- } else {
1849
- onCleanup()
1850
- }
1851
- })
1852
-
1853
- return () => {
1854
- onCleanup()
1855
- onUnsubscribe()
1856
- }
1857
- }, [store])
1858
-
1859
- useIsomorphicLayoutEffect(() => {
1860
- const rafId = requestAnimationFrame(() => {
1861
- rowVirtualizer.measure()
1862
- })
1863
- return () => cancelAnimationFrame(rafId)
1864
- }, [
1865
- data,
1866
- table.getState().columnFilters,
1867
- table.getState().columnOrder,
1868
- table.getState().columnPinning,
1869
- table.getState().columnSizing,
1870
- table.getState().columnVisibility,
1871
- table.getState().expanded,
1872
- table.getState().globalFilter,
1873
- table.getState().grouping,
1874
- table.getState().rowSelection,
1875
- table.getState().sorting,
1876
- rowHeight,
1877
- ])
1878
-
1879
- return {
1880
- dataGridRef,
1881
- headerRef,
1882
- rowMapRef,
1883
- footerRef,
1884
- table,
1885
- rowVirtualizer,
1886
- searchState,
1887
- columnSizeVars,
1888
- onRowAdd: onRowAddProp ? onRowAdd : undefined,
1889
- }
1890
- }
1891
-
1892
- export { useDataGrid, type UseDataGridProps }