@lovett/ui 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -0
- package/package.json +53 -0
- package/src/accordion.tsx +153 -0
- package/src/atoms.tsx +197 -0
- package/src/badge.tsx +91 -0
- package/src/brand-logo-tile.tsx +200 -0
- package/src/bulk-action-bar.tsx +104 -0
- package/src/button-group.tsx +46 -0
- package/src/button.tsx +93 -0
- package/src/calculator-shell-v2.tsx +380 -0
- package/src/calculator-shell.tsx +310 -0
- package/src/card.tsx +369 -0
- package/src/checkbox.tsx +114 -0
- package/src/chip-nav.tsx +347 -0
- package/src/choropleth.tsx +62 -0
- package/src/code-block.tsx +235 -0
- package/src/completion-ring.tsx +119 -0
- package/src/copy-field.tsx +169 -0
- package/src/data-grid/column-options-menu.tsx +127 -0
- package/src/data-grid/data-grid.tsx +391 -0
- package/src/data-grid/drawer-panel.tsx +146 -0
- package/src/data-grid/index.ts +29 -0
- package/src/data-grid/sortable-parts.tsx +215 -0
- package/src/data-grid/table-body.tsx +238 -0
- package/src/data-grid/table-elements.tsx +135 -0
- package/src/data-grid/table-header.tsx +95 -0
- package/src/data-grid/table-overlays.tsx +78 -0
- package/src/data-grid/table-parts.tsx +15 -0
- package/src/data-grid/table-summary-footer.tsx +101 -0
- package/src/data-grid/table-view.tsx +176 -0
- package/src/data-grid/types.ts +141 -0
- package/src/data-grid/use-grid-columns.ts +311 -0
- package/src/data-grid/use-grid-editing.ts +154 -0
- package/src/device-frame.tsx +274 -0
- package/src/dropdown-button.tsx +221 -0
- package/src/dropdown-menu.tsx +450 -0
- package/src/empty-state.tsx +85 -0
- package/src/file-upload/file-drop-zone.tsx +156 -0
- package/src/file-upload/file-preview-grid.tsx +39 -0
- package/src/file-upload/file-preview-item.tsx +175 -0
- package/src/file-upload/file-thumbnail.tsx +98 -0
- package/src/file-upload/file-upload-button.tsx +95 -0
- package/src/file-upload/index.ts +13 -0
- package/src/file-upload/use-file-upload.ts +371 -0
- package/src/filter-bar.tsx +125 -0
- package/src/filter-dropdown.tsx +289 -0
- package/src/floating-drawer.tsx +420 -0
- package/src/folder-card.tsx +188 -0
- package/src/folder-tree-picker.tsx +511 -0
- package/src/format.ts +117 -0
- package/src/frame-stack.tsx +73 -0
- package/src/hero-form-card.tsx +471 -0
- package/src/index.ts +372 -0
- package/src/input.tsx +130 -0
- package/src/kbd.tsx +41 -0
- package/src/lib/layout.ts +22 -0
- package/src/lib/utils.ts +6 -0
- package/src/markdown.tsx +214 -0
- package/src/menu-button.tsx +142 -0
- package/src/meta-previews/CreativeMedia.tsx +119 -0
- package/src/meta-previews/InstagramExplorePreview.tsx +340 -0
- package/src/meta-previews/InstagramFeedCarousel.tsx +245 -0
- package/src/meta-previews/InstagramFeedPreview.tsx +259 -0
- package/src/meta-previews/MetaFeedCarousel.tsx +228 -0
- package/src/meta-previews/MetaFeedPreview.tsx +294 -0
- package/src/meta-previews/MetaInstreamPreview.tsx +135 -0
- package/src/meta-previews/MetaMessengerPreview.tsx +795 -0
- package/src/meta-previews/MetaReelPreview.tsx +750 -0
- package/src/meta-previews/MetaRightColumnPreview.tsx +245 -0
- package/src/meta-previews/MetaSearchPreview.tsx +113 -0
- package/src/meta-previews/MetaStoryPreview.tsx +1132 -0
- package/src/meta-previews/ProfileAvatar.tsx +79 -0
- package/src/meta-previews/defaults.ts +27 -0
- package/src/meta-previews/index.ts +37 -0
- package/src/meta-previews/types.ts +219 -0
- package/src/metric-card.tsx +117 -0
- package/src/modal.tsx +131 -0
- package/src/option-tile.tsx +252 -0
- package/src/page-hero.tsx +78 -0
- package/src/page-shell.tsx +440 -0
- package/src/pagination.tsx +86 -0
- package/src/pill-button.tsx +103 -0
- package/src/profile-section.tsx +389 -0
- package/src/progress-bar.tsx +84 -0
- package/src/radio.tsx +172 -0
- package/src/range-slider.tsx +161 -0
- package/src/resizable.tsx +282 -0
- package/src/search-bar.tsx +81 -0
- package/src/segmented-pill.tsx +143 -0
- package/src/shell.tsx +464 -0
- package/src/slider.tsx +107 -0
- package/src/sortable-table.tsx +198 -0
- package/src/sortable.tsx +137 -0
- package/src/stat-row.tsx +132 -0
- package/src/stats-grid.tsx +38 -0
- package/src/step-loader.tsx +141 -0
- package/src/styles.css +562 -0
- package/src/tabs.tsx +143 -0
- package/src/tag-chip-input.tsx +394 -0
- package/src/textarea.tsx +39 -0
- package/src/toast.tsx +59 -0
- package/src/tokens.css +579 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useGridColumns — column width / visibility / drag-reorder state.
|
|
3
|
+
*
|
|
4
|
+
* Ported 2026-05-18 from gray-ui-csm
|
|
5
|
+
* https://github.com/Jason-uxui/gray-ui-csm/blob/baf8346ac0c1160282e0b173de1bffc594d9c9c3/components/data-grid/use-grid-columns.ts
|
|
6
|
+
* License: MIT (see packages/ui/licenses/gray-ui-csm-LICENSE.txt)
|
|
7
|
+
* Workspace ADR: ADR-025
|
|
8
|
+
*
|
|
9
|
+
* BRING_WITH_REFACTOR: pure TS port — no styling changes; only
|
|
10
|
+
* relative-path adjustment for the workspace tree.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
useCallback,
|
|
15
|
+
useEffect,
|
|
16
|
+
useMemo,
|
|
17
|
+
useRef,
|
|
18
|
+
useState,
|
|
19
|
+
type PointerEvent as ReactPointerEvent,
|
|
20
|
+
type RefObject,
|
|
21
|
+
} from 'react'
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
type DragEndEvent,
|
|
25
|
+
type DragOverEvent,
|
|
26
|
+
type DragStartEvent,
|
|
27
|
+
} from '@dnd-kit/core'
|
|
28
|
+
import { arrayMove } from '@dnd-kit/sortable'
|
|
29
|
+
|
|
30
|
+
import type { DataGridColumn } from './types'
|
|
31
|
+
|
|
32
|
+
function createInitialColumnWidths<ColumnId extends string>(
|
|
33
|
+
columns: DataGridColumn<ColumnId>[],
|
|
34
|
+
): Record<ColumnId, number> {
|
|
35
|
+
return columns.reduce<Record<ColumnId, number>>(
|
|
36
|
+
(acc, column) => {
|
|
37
|
+
acc[column.id] = column.defaultWidth
|
|
38
|
+
return acc
|
|
39
|
+
},
|
|
40
|
+
{} as Record<ColumnId, number>,
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type UseGridColumnsParams<ColumnId extends string> = {
|
|
45
|
+
columns: DataGridColumn<ColumnId>[]
|
|
46
|
+
tableRef: RefObject<HTMLTableElement | null>
|
|
47
|
+
minColumnWidth: number
|
|
48
|
+
controlColumnWidth: number
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function useGridColumns<ColumnId extends string>({
|
|
52
|
+
columns,
|
|
53
|
+
tableRef,
|
|
54
|
+
minColumnWidth,
|
|
55
|
+
controlColumnWidth,
|
|
56
|
+
}: UseGridColumnsParams<ColumnId>) {
|
|
57
|
+
const columnOrder = useMemo(
|
|
58
|
+
() => columns.map((column) => column.id),
|
|
59
|
+
[columns],
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const [columnWidths, setColumnWidths] = useState<
|
|
63
|
+
Record<ColumnId, number>
|
|
64
|
+
>(() => createInitialColumnWidths(columns))
|
|
65
|
+
const [visibleColumnIds, setVisibleColumnIds] = useState<ColumnId[]>(
|
|
66
|
+
() => columnOrder,
|
|
67
|
+
)
|
|
68
|
+
const [draggingColumnId, setDraggingColumnId] =
|
|
69
|
+
useState<ColumnId | null>(null)
|
|
70
|
+
const [dragOverColumnId, setDragOverColumnId] =
|
|
71
|
+
useState<ColumnId | null>(null)
|
|
72
|
+
const [dragOverlayHeight, setDragOverlayHeight] = useState(280)
|
|
73
|
+
const [dropIndicatorLeft, setDropIndicatorLeft] = useState<
|
|
74
|
+
number | null
|
|
75
|
+
>(null)
|
|
76
|
+
const [dragTableRect, setDragTableRect] = useState<{
|
|
77
|
+
top: number
|
|
78
|
+
height: number
|
|
79
|
+
} | null>(null)
|
|
80
|
+
|
|
81
|
+
const resizingRef = useRef<{
|
|
82
|
+
columnId: ColumnId
|
|
83
|
+
startX: number
|
|
84
|
+
startWidth: number
|
|
85
|
+
} | null>(null)
|
|
86
|
+
const orderBeforeDragRef = useRef<ColumnId[]>(columnOrder)
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
const allowedIds = new Set(columnOrder)
|
|
90
|
+
|
|
91
|
+
setColumnWidths((current) => {
|
|
92
|
+
const next = createInitialColumnWidths(columns)
|
|
93
|
+
columns.forEach((column) => {
|
|
94
|
+
if (typeof current[column.id] === 'number') {
|
|
95
|
+
next[column.id] = current[column.id]
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
return next
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
setVisibleColumnIds((current) => {
|
|
102
|
+
const kept = current.filter((id) => allowedIds.has(id))
|
|
103
|
+
const missing = columnOrder.filter((id) => !kept.includes(id))
|
|
104
|
+
return [...kept, ...missing]
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
orderBeforeDragRef.current = columnOrder
|
|
108
|
+
}, [columns, columnOrder])
|
|
109
|
+
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
const handlePointerMove = (event: PointerEvent) => {
|
|
112
|
+
const activeResize = resizingRef.current
|
|
113
|
+
if (!activeResize) return
|
|
114
|
+
|
|
115
|
+
const column = columns.find((item) => item.id === activeResize.columnId)
|
|
116
|
+
const resolvedMinWidth = column?.minWidth ?? minColumnWidth
|
|
117
|
+
const deltaX = event.clientX - activeResize.startX
|
|
118
|
+
const nextWidth = Math.max(
|
|
119
|
+
resolvedMinWidth,
|
|
120
|
+
activeResize.startWidth + deltaX,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
setColumnWidths((currentWidths) => ({
|
|
124
|
+
...currentWidths,
|
|
125
|
+
[activeResize.columnId]: nextWidth,
|
|
126
|
+
}))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const handlePointerUp = () => {
|
|
130
|
+
if (!resizingRef.current) return
|
|
131
|
+
resizingRef.current = null
|
|
132
|
+
document.body.style.userSelect = ''
|
|
133
|
+
document.body.style.cursor = ''
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
window.addEventListener('pointermove', handlePointerMove)
|
|
137
|
+
window.addEventListener('pointerup', handlePointerUp)
|
|
138
|
+
|
|
139
|
+
return () => {
|
|
140
|
+
window.removeEventListener('pointermove', handlePointerMove)
|
|
141
|
+
window.removeEventListener('pointerup', handlePointerUp)
|
|
142
|
+
document.body.style.userSelect = ''
|
|
143
|
+
document.body.style.cursor = ''
|
|
144
|
+
}
|
|
145
|
+
}, [columns, minColumnWidth])
|
|
146
|
+
|
|
147
|
+
const visibleColumns = useMemo(() => {
|
|
148
|
+
return visibleColumnIds
|
|
149
|
+
.map((columnId) => columns.find((column) => column.id === columnId))
|
|
150
|
+
.filter((column): column is DataGridColumn<ColumnId> => Boolean(column))
|
|
151
|
+
}, [columns, visibleColumnIds])
|
|
152
|
+
|
|
153
|
+
const gridMinWidth = useMemo(() => {
|
|
154
|
+
const contentWidth = visibleColumns.reduce((sum, column) => {
|
|
155
|
+
return sum + (columnWidths[column.id] ?? column.defaultWidth)
|
|
156
|
+
}, 0)
|
|
157
|
+
|
|
158
|
+
return controlColumnWidth + contentWidth
|
|
159
|
+
}, [columnWidths, controlColumnWidth, visibleColumns])
|
|
160
|
+
|
|
161
|
+
const hiddenColumns = useMemo(() => {
|
|
162
|
+
return columns.filter((column) => !visibleColumnIds.includes(column.id))
|
|
163
|
+
}, [columns, visibleColumnIds])
|
|
164
|
+
|
|
165
|
+
const toggleColumnVisibility = useCallback(
|
|
166
|
+
(columnId: ColumnId, visible: boolean) => {
|
|
167
|
+
setVisibleColumnIds((currentColumns) => {
|
|
168
|
+
if (visible) {
|
|
169
|
+
if (currentColumns.includes(columnId)) return currentColumns
|
|
170
|
+
return [...currentColumns, columnId]
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (currentColumns.length === 1 && currentColumns.includes(columnId)) {
|
|
174
|
+
return currentColumns
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return currentColumns.filter((id) => id !== columnId)
|
|
178
|
+
})
|
|
179
|
+
},
|
|
180
|
+
[],
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
const handleColumnDragStart = useCallback(
|
|
184
|
+
(event: DragStartEvent) => {
|
|
185
|
+
const activeId = String(event.active.id) as ColumnId
|
|
186
|
+
const tableRect = tableRef.current?.getBoundingClientRect()
|
|
187
|
+
orderBeforeDragRef.current = visibleColumnIds
|
|
188
|
+
setDraggingColumnId(activeId)
|
|
189
|
+
setDragOverColumnId(activeId)
|
|
190
|
+
setDragOverlayHeight(tableRect?.height ?? 280)
|
|
191
|
+
if (tableRect) {
|
|
192
|
+
setDragTableRect({ top: tableRect.top, height: tableRect.height })
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
[tableRef, visibleColumnIds],
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
const handleColumnDragOver = useCallback(
|
|
199
|
+
(event: DragOverEvent) => {
|
|
200
|
+
if (!event.over) return
|
|
201
|
+
|
|
202
|
+
const activeId = String(event.active.id) as ColumnId
|
|
203
|
+
const overId = String(event.over.id) as ColumnId
|
|
204
|
+
const overRect = event.over.rect
|
|
205
|
+
setDragOverColumnId(overId)
|
|
206
|
+
|
|
207
|
+
const currentIndex = visibleColumnIds.indexOf(activeId)
|
|
208
|
+
const targetIndex = visibleColumnIds.indexOf(overId)
|
|
209
|
+
const indicatorLeft =
|
|
210
|
+
currentIndex > -1 && targetIndex > -1 && currentIndex < targetIndex
|
|
211
|
+
? overRect.left + overRect.width
|
|
212
|
+
: overRect.left
|
|
213
|
+
|
|
214
|
+
setDropIndicatorLeft(indicatorLeft)
|
|
215
|
+
|
|
216
|
+
if (activeId === overId) return
|
|
217
|
+
|
|
218
|
+
setVisibleColumnIds((currentColumns) => {
|
|
219
|
+
const oldIndex = currentColumns.indexOf(activeId)
|
|
220
|
+
const newIndex = currentColumns.indexOf(overId)
|
|
221
|
+
|
|
222
|
+
if (oldIndex === -1 || newIndex === -1 || oldIndex === newIndex) {
|
|
223
|
+
return currentColumns
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return arrayMove(currentColumns, oldIndex, newIndex)
|
|
227
|
+
})
|
|
228
|
+
},
|
|
229
|
+
[visibleColumnIds],
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
const handleColumnDragEnd = useCallback((event: DragEndEvent) => {
|
|
233
|
+
if (!event.over) {
|
|
234
|
+
setVisibleColumnIds(orderBeforeDragRef.current)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
setDraggingColumnId(null)
|
|
238
|
+
setDragOverColumnId(null)
|
|
239
|
+
setDropIndicatorLeft(null)
|
|
240
|
+
setDragTableRect(null)
|
|
241
|
+
}, [])
|
|
242
|
+
|
|
243
|
+
const handleOptionColumnDragEnd = useCallback((event: DragEndEvent) => {
|
|
244
|
+
if (!event.over) return
|
|
245
|
+
|
|
246
|
+
const activeId = String(event.active.id) as ColumnId
|
|
247
|
+
const overId = String(event.over.id) as ColumnId
|
|
248
|
+
|
|
249
|
+
if (activeId === overId) return
|
|
250
|
+
|
|
251
|
+
setVisibleColumnIds((currentColumns) => {
|
|
252
|
+
const oldIndex = currentColumns.indexOf(activeId)
|
|
253
|
+
const newIndex = currentColumns.indexOf(overId)
|
|
254
|
+
|
|
255
|
+
if (oldIndex === -1 || newIndex === -1) return currentColumns
|
|
256
|
+
|
|
257
|
+
return arrayMove(currentColumns, oldIndex, newIndex)
|
|
258
|
+
})
|
|
259
|
+
}, [])
|
|
260
|
+
|
|
261
|
+
const handleColumnDragCancel = useCallback(() => {
|
|
262
|
+
setVisibleColumnIds(orderBeforeDragRef.current)
|
|
263
|
+
setDraggingColumnId(null)
|
|
264
|
+
setDragOverColumnId(null)
|
|
265
|
+
setDropIndicatorLeft(null)
|
|
266
|
+
setDragTableRect(null)
|
|
267
|
+
}, [])
|
|
268
|
+
|
|
269
|
+
const beginResize = useCallback(
|
|
270
|
+
(event: ReactPointerEvent<HTMLButtonElement>, columnId: ColumnId) => {
|
|
271
|
+
event.preventDefault()
|
|
272
|
+
event.stopPropagation()
|
|
273
|
+
|
|
274
|
+
resizingRef.current = {
|
|
275
|
+
columnId,
|
|
276
|
+
startX: event.clientX,
|
|
277
|
+
startWidth: columnWidths[columnId],
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
document.body.style.userSelect = 'none'
|
|
281
|
+
document.body.style.cursor = 'col-resize'
|
|
282
|
+
},
|
|
283
|
+
[columnWidths],
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
const dragOverlayColumn = useMemo(() => {
|
|
287
|
+
if (!draggingColumnId) return null
|
|
288
|
+
return columns.find((column) => column.id === draggingColumnId) ?? null
|
|
289
|
+
}, [columns, draggingColumnId])
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
columnWidths,
|
|
293
|
+
visibleColumnIds,
|
|
294
|
+
draggingColumnId,
|
|
295
|
+
dragOverColumnId,
|
|
296
|
+
dragOverlayHeight,
|
|
297
|
+
dropIndicatorLeft,
|
|
298
|
+
dragTableRect,
|
|
299
|
+
visibleColumns,
|
|
300
|
+
gridMinWidth,
|
|
301
|
+
hiddenColumns,
|
|
302
|
+
toggleColumnVisibility,
|
|
303
|
+
handleColumnDragStart,
|
|
304
|
+
handleColumnDragOver,
|
|
305
|
+
handleColumnDragEnd,
|
|
306
|
+
handleOptionColumnDragEnd,
|
|
307
|
+
handleColumnDragCancel,
|
|
308
|
+
beginResize,
|
|
309
|
+
dragOverlayColumn,
|
|
310
|
+
}
|
|
311
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useGridEditing — cell editing draft / commit / keyboard navigation.
|
|
3
|
+
*
|
|
4
|
+
* Ported 2026-05-18 from gray-ui-csm
|
|
5
|
+
* https://github.com/Jason-uxui/gray-ui-csm/blob/baf8346ac0c1160282e0b173de1bffc594d9c9c3/components/data-grid/use-grid-editing.ts
|
|
6
|
+
* License: MIT (see packages/ui/licenses/gray-ui-csm-LICENSE.txt)
|
|
7
|
+
* Workspace ADR: ADR-025
|
|
8
|
+
*
|
|
9
|
+
* BRING_WITH_REFACTOR: pure TS port — relative-path adjustment only.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
useCallback,
|
|
14
|
+
useEffect,
|
|
15
|
+
useState,
|
|
16
|
+
type KeyboardEvent as ReactKeyboardEvent,
|
|
17
|
+
type RefObject,
|
|
18
|
+
} from 'react'
|
|
19
|
+
|
|
20
|
+
import type { DataGridColumn, DataGridRowBase, EditingCell } from './types'
|
|
21
|
+
|
|
22
|
+
type UseGridEditingParams<
|
|
23
|
+
Row extends DataGridRowBase,
|
|
24
|
+
ColumnId extends string,
|
|
25
|
+
> = {
|
|
26
|
+
gridRef: RefObject<HTMLDivElement | null>
|
|
27
|
+
inputRef: RefObject<HTMLInputElement | null>
|
|
28
|
+
visibleRowCount: number
|
|
29
|
+
visibleColumnCount: number
|
|
30
|
+
isEditableColumn: (columnId: ColumnId) => boolean
|
|
31
|
+
getCellEditValue: (row: Row, columnId: ColumnId) => string
|
|
32
|
+
applyCellEdit: (row: Row, columnId: ColumnId, nextValue: string) => Row
|
|
33
|
+
commitRows: (updater: (currentRows: Row[]) => Row[]) => void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function useGridEditing<
|
|
37
|
+
Row extends DataGridRowBase,
|
|
38
|
+
ColumnId extends string,
|
|
39
|
+
>({
|
|
40
|
+
gridRef,
|
|
41
|
+
inputRef,
|
|
42
|
+
visibleRowCount,
|
|
43
|
+
visibleColumnCount,
|
|
44
|
+
isEditableColumn,
|
|
45
|
+
getCellEditValue,
|
|
46
|
+
applyCellEdit,
|
|
47
|
+
commitRows,
|
|
48
|
+
}: UseGridEditingParams<Row, ColumnId>) {
|
|
49
|
+
const [editingCell, setEditingCell] =
|
|
50
|
+
useState<EditingCell<ColumnId> | null>(null)
|
|
51
|
+
const [draftValue, setDraftValue] = useState('')
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!editingCell) return
|
|
55
|
+
inputRef.current?.focus()
|
|
56
|
+
inputRef.current?.select()
|
|
57
|
+
}, [editingCell, inputRef])
|
|
58
|
+
|
|
59
|
+
const startEditing = useCallback(
|
|
60
|
+
(row: Row, columnId: ColumnId) => {
|
|
61
|
+
if (!isEditableColumn(columnId)) return
|
|
62
|
+
setEditingCell({ rowId: row.id, columnId })
|
|
63
|
+
setDraftValue(getCellEditValue(row, columnId))
|
|
64
|
+
},
|
|
65
|
+
[getCellEditValue, isEditableColumn],
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
const commitEdit = useCallback(() => {
|
|
69
|
+
if (!editingCell) return
|
|
70
|
+
|
|
71
|
+
commitRows((currentRows) =>
|
|
72
|
+
currentRows.map((row) => {
|
|
73
|
+
if (row.id !== editingCell.rowId) return row
|
|
74
|
+
return applyCellEdit(row, editingCell.columnId, draftValue)
|
|
75
|
+
}),
|
|
76
|
+
)
|
|
77
|
+
setEditingCell(null)
|
|
78
|
+
setDraftValue('')
|
|
79
|
+
}, [applyCellEdit, commitRows, draftValue, editingCell])
|
|
80
|
+
|
|
81
|
+
const cancelEdit = useCallback(() => {
|
|
82
|
+
setEditingCell(null)
|
|
83
|
+
setDraftValue('')
|
|
84
|
+
}, [])
|
|
85
|
+
|
|
86
|
+
const focusCell = useCallback(
|
|
87
|
+
(rowIndex: number, colIndex: number) => {
|
|
88
|
+
const targetCell = gridRef.current?.querySelector<HTMLElement>(
|
|
89
|
+
`[data-grid-cell="true"][data-row-index="${rowIndex}"][data-col-index="${colIndex}"]`,
|
|
90
|
+
)
|
|
91
|
+
targetCell?.focus()
|
|
92
|
+
},
|
|
93
|
+
[gridRef],
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
const handleCellKeyDown = useCallback(
|
|
97
|
+
(
|
|
98
|
+
event: ReactKeyboardEvent<HTMLTableCellElement>,
|
|
99
|
+
row: Row,
|
|
100
|
+
rowIndex: number,
|
|
101
|
+
column: DataGridColumn<ColumnId>,
|
|
102
|
+
colIndex: number,
|
|
103
|
+
) => {
|
|
104
|
+
const maxRowIndex = visibleRowCount - 1
|
|
105
|
+
const maxColIndex = visibleColumnCount - 1
|
|
106
|
+
|
|
107
|
+
if (event.key === 'Enter' && isEditableColumn(column.id)) {
|
|
108
|
+
event.preventDefault()
|
|
109
|
+
startEditing(row, column.id)
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (event.key === 'ArrowRight') {
|
|
114
|
+
event.preventDefault()
|
|
115
|
+
focusCell(rowIndex, Math.min(colIndex + 1, maxColIndex))
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (event.key === 'ArrowLeft') {
|
|
120
|
+
event.preventDefault()
|
|
121
|
+
focusCell(rowIndex, Math.max(colIndex - 1, 0))
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (event.key === 'ArrowDown') {
|
|
126
|
+
event.preventDefault()
|
|
127
|
+
focusCell(Math.min(rowIndex + 1, maxRowIndex), colIndex)
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (event.key === 'ArrowUp') {
|
|
132
|
+
event.preventDefault()
|
|
133
|
+
focusCell(Math.max(rowIndex - 1, 0), colIndex)
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
[
|
|
137
|
+
focusCell,
|
|
138
|
+
isEditableColumn,
|
|
139
|
+
startEditing,
|
|
140
|
+
visibleColumnCount,
|
|
141
|
+
visibleRowCount,
|
|
142
|
+
],
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
editingCell,
|
|
147
|
+
draftValue,
|
|
148
|
+
setDraftValue,
|
|
149
|
+
startEditing,
|
|
150
|
+
commitEdit,
|
|
151
|
+
cancelEdit,
|
|
152
|
+
handleCellKeyDown,
|
|
153
|
+
}
|
|
154
|
+
}
|