@budibase/frontend-core 2.5.9 → 2.5.10-alpha.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.
Files changed (68) hide show
  1. package/package.json +6 -4
  2. package/src/api/other.js +0 -9
  3. package/src/components/grid/cells/AttachmentCell.svelte +154 -0
  4. package/src/components/grid/cells/BooleanCell.svelte +44 -0
  5. package/src/components/grid/cells/DataCell.svelte +91 -0
  6. package/src/components/grid/cells/DateCell.svelte +77 -0
  7. package/src/components/grid/cells/FormulaCell.svelte +5 -0
  8. package/src/components/grid/cells/GridCell.svelte +165 -0
  9. package/src/components/grid/cells/GutterCell.svelte +127 -0
  10. package/src/components/grid/cells/HeaderCell.svelte +248 -0
  11. package/src/components/grid/cells/JSONCell.svelte +36 -0
  12. package/src/components/grid/cells/LongFormCell.svelte +122 -0
  13. package/src/components/grid/cells/MultiSelectCell.svelte +7 -0
  14. package/src/components/grid/cells/NumberCell.svelte +14 -0
  15. package/src/components/grid/cells/OptionsCell.svelte +249 -0
  16. package/src/components/grid/cells/RelationshipCell.svelte +518 -0
  17. package/src/components/grid/cells/TextCell.svelte +111 -0
  18. package/src/components/grid/controls/AddColumnButton.svelte +16 -0
  19. package/src/components/grid/controls/AddRowButton.svelte +18 -0
  20. package/src/components/grid/controls/BetaButton.svelte +46 -0
  21. package/src/components/grid/controls/ColumnWidthButton.svelte +91 -0
  22. package/src/components/grid/controls/DeleteButton.svelte +66 -0
  23. package/src/components/grid/controls/HideColumnsButton.svelte +95 -0
  24. package/src/components/grid/controls/RowHeightButton.svelte +70 -0
  25. package/src/components/grid/controls/SortButton.svelte +129 -0
  26. package/src/components/grid/index.js +1 -0
  27. package/src/components/grid/layout/Avatar.svelte +24 -0
  28. package/src/components/grid/layout/Grid.svelte +254 -0
  29. package/src/components/grid/layout/GridBody.svelte +73 -0
  30. package/src/components/grid/layout/GridRow.svelte +59 -0
  31. package/src/components/grid/layout/GridScrollWrapper.svelte +81 -0
  32. package/src/components/grid/layout/HeaderRow.svelte +62 -0
  33. package/src/components/grid/layout/KeyboardShortcut.svelte +53 -0
  34. package/src/components/grid/layout/NewRow.svelte +334 -0
  35. package/src/components/grid/layout/RowCount.svelte +9 -0
  36. package/src/components/grid/layout/StickyColumn.svelte +176 -0
  37. package/src/components/grid/layout/UserAvatars.svelte +20 -0
  38. package/src/components/grid/lib/constants.js +14 -0
  39. package/src/components/grid/lib/events.js +29 -0
  40. package/src/components/grid/lib/renderers.js +29 -0
  41. package/src/components/grid/lib/utils.js +26 -0
  42. package/src/components/grid/lib/websocket.js +63 -0
  43. package/src/components/grid/overlays/KeyboardManager.svelte +232 -0
  44. package/src/components/grid/overlays/MenuOverlay.svelte +96 -0
  45. package/src/components/grid/overlays/ReorderOverlay.svelte +63 -0
  46. package/src/components/grid/overlays/ResizeOverlay.svelte +70 -0
  47. package/src/components/grid/overlays/ScrollOverlay.svelte +121 -0
  48. package/src/components/grid/stores/bounds.js +16 -0
  49. package/src/components/grid/stores/clipboard.js +34 -0
  50. package/src/components/grid/stores/columns.js +206 -0
  51. package/src/components/grid/stores/index.js +48 -0
  52. package/src/components/grid/stores/menu.js +49 -0
  53. package/src/components/grid/stores/pagination.js +24 -0
  54. package/src/components/grid/stores/reorder.js +155 -0
  55. package/src/components/grid/stores/resize.js +124 -0
  56. package/src/components/grid/stores/rows.js +467 -0
  57. package/src/components/grid/stores/scroll.js +201 -0
  58. package/src/components/grid/stores/ui.js +159 -0
  59. package/src/components/grid/stores/users.js +115 -0
  60. package/src/components/grid/stores/validation.js +45 -0
  61. package/src/components/grid/stores/viewport.js +145 -0
  62. package/src/components/index.js +1 -0
  63. package/src/constants.js +1 -0
  64. package/src/fetch/DataFetch.js +27 -21
  65. package/src/fetch/QueryFetch.js +1 -1
  66. package/src/themes/midnight.css +3 -0
  67. package/src/themes/nord.css +3 -0
  68. package/src/utils/utils.js +20 -0
@@ -0,0 +1,467 @@
1
+ import { writable, derived, get } from "svelte/store"
2
+ import { fetchData } from "../../../fetch/fetchData"
3
+ import { notifications } from "@budibase/bbui"
4
+ import { NewRowID, RowPageSize } from "../lib/constants"
5
+
6
+ const initialSortState = {
7
+ column: null,
8
+ order: "ascending",
9
+ }
10
+
11
+ export const createStores = () => {
12
+ const rows = writable([])
13
+ const table = writable(null)
14
+ const filter = writable([])
15
+ const loading = writable(false)
16
+ const loaded = writable(false)
17
+ const sort = writable(initialSortState)
18
+ const rowChangeCache = writable({})
19
+ const inProgressChanges = writable({})
20
+ const hasNextPage = writable(false)
21
+
22
+ // Generate a lookup map to quick find a row by ID
23
+ const rowLookupMap = derived(
24
+ rows,
25
+ $rows => {
26
+ let map = {}
27
+ for (let i = 0; i < $rows.length; i++) {
28
+ map[$rows[i]._id] = i
29
+ }
30
+ return map
31
+ },
32
+ {}
33
+ )
34
+
35
+ // Mark loaded as true if we've ever stopped loading
36
+ let hasStartedLoading = false
37
+ loading.subscribe($loading => {
38
+ if ($loading) {
39
+ hasStartedLoading = true
40
+ } else if (hasStartedLoading) {
41
+ loaded.set(true)
42
+ }
43
+ })
44
+
45
+ return {
46
+ rows,
47
+ rowLookupMap,
48
+ table,
49
+ filter,
50
+ loaded,
51
+ loading,
52
+ sort,
53
+ rowChangeCache,
54
+ inProgressChanges,
55
+ hasNextPage,
56
+ }
57
+ }
58
+
59
+ export const deriveStores = context => {
60
+ const {
61
+ rows,
62
+ rowLookupMap,
63
+ table,
64
+ filter,
65
+ loading,
66
+ sort,
67
+ tableId,
68
+ API,
69
+ scroll,
70
+ validation,
71
+ focusedCellId,
72
+ columns,
73
+ rowChangeCache,
74
+ inProgressChanges,
75
+ previousFocusedRowId,
76
+ hasNextPage,
77
+ } = context
78
+ const instanceLoaded = writable(false)
79
+ const fetch = writable(null)
80
+
81
+ // Local cache of row IDs to speed up checking if a row exists
82
+ let rowCacheMap = {}
83
+
84
+ // Enrich rows with an index property and any pending changes
85
+ const enrichedRows = derived(
86
+ [rows, rowChangeCache],
87
+ ([$rows, $rowChangeCache]) => {
88
+ return $rows.map((row, idx) => ({
89
+ ...row,
90
+ ...$rowChangeCache[row._id],
91
+ __idx: idx,
92
+ }))
93
+ },
94
+ []
95
+ )
96
+
97
+ // Reset everything when table ID changes
98
+ let unsubscribe = null
99
+ let lastResetKey = null
100
+ tableId.subscribe($tableId => {
101
+ // Unsub from previous fetch if one exists
102
+ unsubscribe?.()
103
+ fetch.set(null)
104
+ instanceLoaded.set(false)
105
+ loading.set(true)
106
+
107
+ // Reset state
108
+ filter.set([])
109
+
110
+ // Create new fetch model
111
+ const newFetch = fetchData({
112
+ API,
113
+ datasource: {
114
+ type: "table",
115
+ tableId: $tableId,
116
+ },
117
+ options: {
118
+ filter: [],
119
+ sortColumn: initialSortState.column,
120
+ sortOrder: initialSortState.order,
121
+ limit: RowPageSize,
122
+ paginate: true,
123
+ },
124
+ })
125
+
126
+ // Subscribe to changes of this fetch model
127
+ unsubscribe = newFetch.subscribe($fetch => {
128
+ if ($fetch.loaded && !$fetch.loading) {
129
+ hasNextPage.set($fetch.hasNextPage)
130
+ const $instanceLoaded = get(instanceLoaded)
131
+ const resetRows = $fetch.resetKey !== lastResetKey
132
+ lastResetKey = $fetch.resetKey
133
+
134
+ // Reset state properties when dataset changes
135
+ if (!$instanceLoaded || resetRows) {
136
+ table.set($fetch.definition)
137
+ sort.set({
138
+ column: $fetch.sortColumn,
139
+ order: $fetch.sortOrder,
140
+ })
141
+ }
142
+
143
+ // Reset scroll state when data changes
144
+ if (!$instanceLoaded) {
145
+ // Reset both top and left for a new table ID
146
+ instanceLoaded.set(true)
147
+ scroll.set({ top: 0, left: 0 })
148
+ } else if (resetRows) {
149
+ // Only reset top scroll position when resetting rows
150
+ scroll.update(state => ({ ...state, top: 0 }))
151
+ }
152
+
153
+ // Process new rows
154
+ handleNewRows($fetch.rows, resetRows)
155
+
156
+ // Notify that we're loaded
157
+ loading.set(false)
158
+ }
159
+ })
160
+
161
+ fetch.set(newFetch)
162
+ })
163
+
164
+ // Update fetch when filter or sort config changes
165
+ filter.subscribe($filter => {
166
+ get(fetch)?.update({
167
+ filter: $filter,
168
+ })
169
+ })
170
+ sort.subscribe($sort => {
171
+ get(fetch)?.update({
172
+ sortOrder: $sort.order,
173
+ sortColumn: $sort.column,
174
+ })
175
+ })
176
+
177
+ // Gets a row by ID
178
+ const getRow = id => {
179
+ const index = get(rowLookupMap)[id]
180
+ return index >= 0 ? get(rows)[index] : null
181
+ }
182
+
183
+ // Handles validation errors from the rows API and updates local validation
184
+ // state, storing error messages against relevant cells
185
+ const handleValidationError = (rowId, error) => {
186
+ if (error?.json?.validationErrors) {
187
+ // Normal validation error
188
+ const keys = Object.keys(error.json.validationErrors)
189
+ const $columns = get(columns)
190
+ for (let column of keys) {
191
+ validation.actions.setError(
192
+ `${rowId}-${column}`,
193
+ `${column} ${error.json.validationErrors[column]}`
194
+ )
195
+
196
+ // Ensure the column is visible
197
+ const index = $columns.findIndex(x => x.name === column)
198
+ if (index !== -1 && !$columns[index].visible) {
199
+ columns.update(state => {
200
+ state[index].visible = true
201
+ return state.slice()
202
+ })
203
+ }
204
+ }
205
+ // Focus the first cell with an error
206
+ focusedCellId.set(`${rowId}-${keys[0]}`)
207
+ } else {
208
+ // Some other error - just update the current cell
209
+ validation.actions.setError(get(focusedCellId), error?.message || "Error")
210
+ }
211
+ }
212
+
213
+ // Adds a new row
214
+ const addRow = async (row, idx, bubble = false) => {
215
+ try {
216
+ // Create row
217
+ let newRow = await API.saveRow({ ...row, tableId: get(tableId) })
218
+ newRow = await fetchRow(newRow._id)
219
+
220
+ // Update state
221
+ if (idx != null) {
222
+ rowCacheMap[newRow._id] = true
223
+ rows.update(state => {
224
+ state.splice(idx, 0, newRow)
225
+ return state.slice()
226
+ })
227
+ } else {
228
+ handleNewRows([newRow])
229
+ }
230
+
231
+ // Refresh row to ensure data is in the correct format
232
+ notifications.success("Row created successfully")
233
+ return newRow
234
+ } catch (error) {
235
+ if (bubble) {
236
+ throw error
237
+ } else {
238
+ handleValidationError(NewRowID, error)
239
+ }
240
+ }
241
+ }
242
+
243
+ // Duplicates a row, inserting the duplicate row after the existing one
244
+ const duplicateRow = async row => {
245
+ let clone = { ...row }
246
+ delete clone._id
247
+ delete clone._rev
248
+ delete clone.__idx
249
+ try {
250
+ return await addRow(clone, row.__idx + 1, true)
251
+ } catch (error) {
252
+ handleValidationError(row._id, error)
253
+ }
254
+ }
255
+
256
+ // Fetches a row by ID using the search endpoint
257
+ const fetchRow = async id => {
258
+ const res = await API.searchTable({
259
+ tableId: get(tableId),
260
+ limit: 1,
261
+ query: {
262
+ equal: {
263
+ _id: id,
264
+ },
265
+ },
266
+ paginate: false,
267
+ })
268
+ return res?.rows?.[0]
269
+ }
270
+
271
+ // Refreshes a specific row, handling updates, addition or deletion
272
+ const refreshRow = async id => {
273
+ // Fetch row from the server again
274
+ const newRow = await fetchRow(id)
275
+
276
+ // Get index of row to check if it exists
277
+ const $rows = get(rows)
278
+ const $rowLookupMap = get(rowLookupMap)
279
+ const index = $rowLookupMap[id]
280
+
281
+ // Process as either an update, addition or deletion
282
+ if (newRow) {
283
+ if (index != null) {
284
+ // An existing row was updated
285
+ rows.update(state => {
286
+ state[index] = { ...newRow }
287
+ return state
288
+ })
289
+ } else {
290
+ // A new row was created
291
+ handleNewRows([newRow])
292
+ }
293
+ } else if (index != null) {
294
+ // A row was removed
295
+ handleRemoveRows([$rows[index]])
296
+ }
297
+ }
298
+
299
+ // Refreshes all data
300
+ const refreshData = () => {
301
+ get(fetch)?.getInitialData()
302
+ }
303
+
304
+ // Patches a row with some changes
305
+ const updateRow = async (rowId, changes) => {
306
+ const $rows = get(rows)
307
+ const $rowLookupMap = get(rowLookupMap)
308
+ const index = $rowLookupMap[rowId]
309
+ const row = $rows[index]
310
+ if (index == null || !Object.keys(changes || {}).length) {
311
+ return
312
+ }
313
+
314
+ // Abandon if no changes
315
+ let same = true
316
+ for (let column of Object.keys(changes)) {
317
+ if (row[column] !== changes[column]) {
318
+ same = false
319
+ break
320
+ }
321
+ }
322
+ if (same) {
323
+ return
324
+ }
325
+
326
+ // Immediately update state so that the change is reflected
327
+ rowChangeCache.update(state => ({
328
+ ...state,
329
+ [rowId]: {
330
+ ...state[rowId],
331
+ ...changes,
332
+ },
333
+ }))
334
+
335
+ // Save change
336
+ try {
337
+ inProgressChanges.update(state => ({
338
+ ...state,
339
+ [rowId]: true,
340
+ }))
341
+ const newRow = { ...row, ...get(rowChangeCache)[rowId] }
342
+ const saved = await API.saveRow(newRow)
343
+
344
+ // Update state after a successful change
345
+ rows.update(state => {
346
+ state[index] = {
347
+ ...newRow,
348
+ _rev: saved._rev,
349
+ }
350
+ return state.slice()
351
+ })
352
+ rowChangeCache.update(state => {
353
+ delete state[rowId]
354
+ return state
355
+ })
356
+ } catch (error) {
357
+ handleValidationError(rowId, error)
358
+ }
359
+ inProgressChanges.update(state => ({
360
+ ...state,
361
+ [rowId]: false,
362
+ }))
363
+ }
364
+
365
+ // Updates a value of a row
366
+ const updateValue = async (rowId, column, value) => {
367
+ return await updateRow(rowId, { [column]: value })
368
+ }
369
+
370
+ // Deletes an array of rows
371
+ const deleteRows = async rowsToDelete => {
372
+ if (!rowsToDelete?.length) {
373
+ return
374
+ }
375
+
376
+ // Actually delete rows
377
+ rowsToDelete.forEach(row => {
378
+ delete row.__idx
379
+ })
380
+ await API.deleteRows({
381
+ tableId: get(tableId),
382
+ rows: rowsToDelete,
383
+ })
384
+
385
+ // Update state
386
+ handleRemoveRows(rowsToDelete)
387
+ }
388
+
389
+ // Local handler to process new rows inside the fetch, and append any new
390
+ // rows to state that we haven't encountered before
391
+ const handleNewRows = (newRows, resetRows) => {
392
+ if (resetRows) {
393
+ rowCacheMap = {}
394
+ }
395
+ let rowsToAppend = []
396
+ let newRow
397
+ for (let i = 0; i < newRows.length; i++) {
398
+ newRow = newRows[i]
399
+ if (!rowCacheMap[newRow._id]) {
400
+ rowCacheMap[newRow._id] = true
401
+ rowsToAppend.push(newRow)
402
+ }
403
+ }
404
+ if (resetRows) {
405
+ rows.set(rowsToAppend)
406
+ } else if (rowsToAppend.length) {
407
+ rows.update(state => [...state, ...rowsToAppend])
408
+ }
409
+ }
410
+
411
+ // Local handler to remove rows from state
412
+ const handleRemoveRows = rowsToRemove => {
413
+ const deletedIds = rowsToRemove.map(row => row._id)
414
+
415
+ // We deliberately do not remove IDs from the cache map as the data may
416
+ // still exist inside the fetch, but we don't want to add it again
417
+ rows.update(state => {
418
+ return state.filter(row => !deletedIds.includes(row._id))
419
+ })
420
+ }
421
+
422
+ // Loads the next page of data if available
423
+ const loadNextPage = () => {
424
+ get(fetch)?.nextPage()
425
+ }
426
+
427
+ // Refreshes the schema of the data fetch subscription
428
+ const refreshTableDefinition = async () => {
429
+ const definition = await API.fetchTableDefinition(get(tableId))
430
+ table.set(definition)
431
+ }
432
+
433
+ // Checks if we have a row with a certain ID
434
+ const hasRow = id => {
435
+ return get(rowLookupMap)[id] != null
436
+ }
437
+
438
+ // Wipe the row change cache when changing row
439
+ previousFocusedRowId.subscribe(id => {
440
+ if (id && !get(inProgressChanges)[id]) {
441
+ rowChangeCache.update(state => {
442
+ delete state[id]
443
+ return state
444
+ })
445
+ }
446
+ })
447
+
448
+ return {
449
+ enrichedRows,
450
+ rows: {
451
+ ...rows,
452
+ actions: {
453
+ addRow,
454
+ duplicateRow,
455
+ getRow,
456
+ updateValue,
457
+ updateRow,
458
+ deleteRows,
459
+ hasRow,
460
+ loadNextPage,
461
+ refreshRow,
462
+ refreshData,
463
+ refreshTableDefinition,
464
+ },
465
+ },
466
+ }
467
+ }
@@ -0,0 +1,201 @@
1
+ import { writable, derived, get } from "svelte/store"
2
+ import { tick } from "svelte"
3
+ import { Padding, GutterWidth } from "../lib/constants"
4
+
5
+ export const createStores = () => {
6
+ const scroll = writable({
7
+ left: 0,
8
+ top: 0,
9
+ })
10
+
11
+ // Derive height and width as primitives to avoid wasted computation
12
+ const scrollTop = derived(scroll, $scroll => $scroll.top, 0)
13
+ const scrollLeft = derived(scroll, $scroll => $scroll.left, 0)
14
+
15
+ return {
16
+ scroll,
17
+ scrollTop,
18
+ scrollLeft,
19
+ }
20
+ }
21
+
22
+ export const deriveStores = context => {
23
+ const { rows, visibleColumns, stickyColumn, rowHeight, width, height } =
24
+ context
25
+
26
+ // Memoize store primitives
27
+ const stickyColumnWidth = derived(stickyColumn, $col => $col?.width || 0, 0)
28
+
29
+ // Derive vertical limits
30
+ const contentHeight = derived(
31
+ [rows, rowHeight],
32
+ ([$rows, $rowHeight]) => ($rows.length + 1) * $rowHeight + Padding,
33
+ 0
34
+ )
35
+ const maxScrollTop = derived(
36
+ [height, contentHeight],
37
+ ([$height, $contentHeight]) => Math.max($contentHeight - $height, 0),
38
+ 0
39
+ )
40
+
41
+ // Derive horizontal limits
42
+ const contentWidth = derived(
43
+ [visibleColumns, stickyColumnWidth],
44
+ ([$visibleColumns, $stickyColumnWidth]) => {
45
+ let width = GutterWidth + Padding + $stickyColumnWidth
46
+ $visibleColumns.forEach(col => {
47
+ width += col.width
48
+ })
49
+ return width
50
+ },
51
+ 0
52
+ )
53
+ const screenWidth = derived(
54
+ [width, stickyColumnWidth],
55
+ ([$width, $stickyColumnWidth]) => $width + GutterWidth + $stickyColumnWidth,
56
+ 0
57
+ )
58
+ const maxScrollLeft = derived(
59
+ [contentWidth, screenWidth],
60
+ ([$contentWidth, $screenWidth]) => {
61
+ return Math.max($contentWidth - $screenWidth, 0)
62
+ },
63
+ 0
64
+ )
65
+
66
+ // Derive whether to show scrollbars or not
67
+ const showVScrollbar = derived(
68
+ [contentHeight, height],
69
+ ([$contentHeight, $height]) => {
70
+ return $contentHeight > $height
71
+ }
72
+ )
73
+ const showHScrollbar = derived(
74
+ [contentWidth, screenWidth],
75
+ ([$contentWidth, $screenWidth]) => {
76
+ return $contentWidth > $screenWidth
77
+ }
78
+ )
79
+
80
+ return {
81
+ contentHeight,
82
+ contentWidth,
83
+ screenWidth,
84
+ maxScrollTop,
85
+ maxScrollLeft,
86
+ showHScrollbar,
87
+ showVScrollbar,
88
+ }
89
+ }
90
+
91
+ export const initialise = context => {
92
+ const {
93
+ focusedCellId,
94
+ focusedRow,
95
+ scroll,
96
+ bounds,
97
+ rowHeight,
98
+ visibleColumns,
99
+ scrollTop,
100
+ maxScrollTop,
101
+ scrollLeft,
102
+ maxScrollLeft,
103
+ } = context
104
+
105
+ // Ensure scroll state never goes invalid, which can happen when changing
106
+ // rows or tables
107
+ const overscrollTop = derived(
108
+ [scrollTop, maxScrollTop],
109
+ ([$scrollTop, $maxScrollTop]) => $scrollTop > $maxScrollTop,
110
+ false
111
+ )
112
+ const overscrollLeft = derived(
113
+ [scrollLeft, maxScrollLeft],
114
+ ([$scrollLeft, $maxScrollLeft]) => $scrollLeft > $maxScrollLeft,
115
+ false
116
+ )
117
+ overscrollTop.subscribe(overscroll => {
118
+ if (overscroll) {
119
+ scroll.update(state => ({
120
+ ...state,
121
+ top: get(maxScrollTop),
122
+ }))
123
+ }
124
+ })
125
+ overscrollLeft.subscribe(overscroll => {
126
+ if (overscroll) {
127
+ scroll.update(state => ({
128
+ ...state,
129
+ left: get(maxScrollLeft),
130
+ }))
131
+ }
132
+ })
133
+
134
+ // Ensure the selected cell is visible
135
+ focusedCellId.subscribe(async $focusedCellId => {
136
+ await tick()
137
+ const $focusedRow = get(focusedRow)
138
+ const $scroll = get(scroll)
139
+ const $bounds = get(bounds)
140
+ const $rowHeight = get(rowHeight)
141
+ const verticalOffset = 60
142
+
143
+ // Ensure vertical position is viewable
144
+ if ($focusedRow) {
145
+ // Ensure row is not below bottom of screen
146
+ const rowYPos = $focusedRow.__idx * $rowHeight
147
+ const bottomCutoff =
148
+ $scroll.top + $bounds.height - $rowHeight - verticalOffset
149
+ let delta = rowYPos - bottomCutoff
150
+ if (delta > 0) {
151
+ scroll.update(state => ({
152
+ ...state,
153
+ top: state.top + delta,
154
+ }))
155
+ }
156
+
157
+ // Ensure row is not above top of screen
158
+ else {
159
+ const delta = $scroll.top - rowYPos + verticalOffset
160
+ if (delta > 0) {
161
+ scroll.update(state => ({
162
+ ...state,
163
+ top: Math.max(0, state.top - delta),
164
+ }))
165
+ }
166
+ }
167
+ }
168
+
169
+ // Ensure horizontal position is viewable
170
+ // Check horizontal position of columns next
171
+ const $visibleColumns = get(visibleColumns)
172
+ const columnName = $focusedCellId?.split("-")[1]
173
+ const column = $visibleColumns.find(col => col.name === columnName)
174
+ const horizontalOffset = 50
175
+ if (!column) {
176
+ return
177
+ }
178
+
179
+ // Ensure column is not cutoff on left edge
180
+ let delta = $scroll.left - column.left + horizontalOffset
181
+ if (delta > 0) {
182
+ scroll.update(state => ({
183
+ ...state,
184
+ left: Math.max(0, state.left - delta),
185
+ }))
186
+ }
187
+
188
+ // Ensure column is not cutoff on right edge
189
+ else {
190
+ const rightEdge = column.left + column.width
191
+ const rightBound = $bounds.width + $scroll.left - horizontalOffset
192
+ delta = rightEdge - rightBound
193
+ if (delta > 0) {
194
+ scroll.update(state => ({
195
+ ...state,
196
+ left: state.left + delta,
197
+ }))
198
+ }
199
+ }
200
+ })
201
+ }