@coreui/data-grid 0.1.0-rc.0 → 0.3.0-rc.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 (166) hide show
  1. package/README.md +9 -6
  2. package/dist/css/data-grid.css +156 -38
  3. package/dist/css/data-grid.css.map +1 -1
  4. package/dist/css/data-grid.min.css +2 -2
  5. package/dist/css/data-grid.min.css.map +1 -1
  6. package/dist/css/data-grid.standalone.css +1066 -0
  7. package/dist/css/data-grid.standalone.css.map +1 -0
  8. package/dist/css/data-grid.standalone.min.css +5 -0
  9. package/dist/css/data-grid.standalone.min.css.map +1 -0
  10. package/dist/js/clipboard.cjs +56 -0
  11. package/dist/js/clipboard.cjs.map +1 -0
  12. package/dist/js/clipboard.mjs +54 -0
  13. package/dist/js/clipboard.mjs.map +1 -0
  14. package/dist/js/csv.cjs +36 -59
  15. package/dist/js/csv.cjs.map +1 -1
  16. package/dist/js/csv.mjs +35 -55
  17. package/dist/js/csv.mjs.map +1 -1
  18. package/dist/js/data-grid.esm.js +14108 -9331
  19. package/dist/js/data-grid.esm.js.map +1 -1
  20. package/dist/js/data-grid.esm.min.js +6 -6
  21. package/dist/js/data-grid.esm.min.js.map +1 -1
  22. package/dist/js/data-grid.js +14408 -9634
  23. package/dist/js/data-grid.js.map +1 -1
  24. package/dist/js/data-grid.lite.esm.js +10507 -0
  25. package/dist/js/data-grid.lite.esm.js.map +1 -0
  26. package/dist/js/data-grid.lite.esm.min.js +8 -0
  27. package/dist/js/data-grid.lite.esm.min.js.map +1 -0
  28. package/dist/js/data-grid.lite.js +10520 -0
  29. package/dist/js/data-grid.lite.js.map +1 -0
  30. package/dist/js/data-grid.lite.min.js +8 -0
  31. package/dist/js/data-grid.lite.min.js.map +1 -0
  32. package/dist/js/data-grid.lite.mjs +10507 -0
  33. package/dist/js/data-grid.lite.mjs.map +1 -0
  34. package/dist/js/data-grid.min.js +6 -6
  35. package/dist/js/data-grid.min.js.map +1 -1
  36. package/dist/js/data-grid.mjs +14108 -9331
  37. package/dist/js/data-grid.mjs.map +1 -1
  38. package/dist/js/direction.cjs +26 -0
  39. package/dist/js/direction.cjs.map +1 -0
  40. package/dist/js/direction.mjs +24 -0
  41. package/dist/js/direction.mjs.map +1 -0
  42. package/dist/js/drag.cjs +467 -0
  43. package/dist/js/drag.cjs.map +1 -0
  44. package/dist/js/drag.mjs +464 -0
  45. package/dist/js/drag.mjs.map +1 -0
  46. package/dist/js/export-table-BdabEbJG.mjs +26 -0
  47. package/dist/js/export-table-BdabEbJG.mjs.map +1 -0
  48. package/dist/js/export-table-D762wrZ1.cjs +55 -0
  49. package/dist/js/export-table-D762wrZ1.cjs.map +1 -0
  50. package/dist/js/filters.cjs +89 -132
  51. package/dist/js/filters.cjs.map +1 -1
  52. package/dist/js/filters.mjs +88 -128
  53. package/dist/js/filters.mjs.map +1 -1
  54. package/dist/js/labels.cjs +65 -70
  55. package/dist/js/labels.cjs.map +1 -1
  56. package/dist/js/labels.mjs +64 -66
  57. package/dist/js/labels.mjs.map +1 -1
  58. package/dist/js/print.cjs +66 -0
  59. package/dist/js/print.cjs.map +1 -0
  60. package/dist/js/print.mjs +64 -0
  61. package/dist/js/print.mjs.map +1 -0
  62. package/dist/js/validate.cjs +43 -0
  63. package/dist/js/validate.cjs.map +1 -0
  64. package/dist/js/validate.mjs +42 -0
  65. package/dist/js/validate.mjs.map +1 -0
  66. package/dist/js/xlsx.cjs +240 -0
  67. package/dist/js/xlsx.cjs.map +1 -0
  68. package/dist/js/xlsx.mjs +238 -0
  69. package/dist/js/xlsx.mjs.map +1 -0
  70. package/dist/types/base-component.d.ts +28 -18
  71. package/dist/types/data-grid.d.ts +26 -13
  72. package/dist/types/dom/data.d.ts +15 -0
  73. package/dist/types/dom/event-handler.d.ts +21 -10
  74. package/dist/types/dom/manipulator.d.ts +15 -8
  75. package/dist/types/engine/active-cell.d.ts +1 -0
  76. package/dist/types/engine/data-source.d.ts +1 -1
  77. package/dist/types/engine/features-lite.d.ts +25 -0
  78. package/dist/types/engine/features.d.ts +35 -0
  79. package/dist/types/engine/table.d.ts +3 -3
  80. package/dist/types/engine/validate.d.mts +1 -0
  81. package/dist/types/engine/validate.d.ts +20 -0
  82. package/dist/types/engine/virtualizer.d.ts +2 -0
  83. package/dist/types/icons.d.ts +6 -4
  84. package/dist/types/index-lite.d.mts +1 -0
  85. package/dist/types/index-lite.d.ts +19 -0
  86. package/dist/types/index.d.ts +11 -2
  87. package/dist/types/types.d.ts +53 -31
  88. package/dist/types/ui/body.d.ts +6 -3
  89. package/dist/types/ui/header.d.ts +3 -2
  90. package/dist/types/ui/loading.d.ts +1 -1
  91. package/dist/types/ui/pinning.d.ts +1 -1
  92. package/dist/types/ui/toolbar.d.ts +10 -2
  93. package/dist/types/util/clipboard.d.mts +1 -0
  94. package/dist/types/util/clipboard.d.ts +29 -0
  95. package/dist/types/util/config.d.ts +27 -10
  96. package/dist/types/util/csv.d.ts +5 -5
  97. package/dist/types/util/direction.d.mts +1 -0
  98. package/dist/types/util/direction.d.ts +17 -0
  99. package/dist/types/util/drag.d.mts +1 -0
  100. package/dist/types/util/drag.d.ts +48 -0
  101. package/dist/types/util/export-table.d.ts +47 -0
  102. package/dist/types/util/index.d.ts +66 -10
  103. package/dist/types/util/print.d.mts +1 -0
  104. package/dist/types/util/print.d.ts +32 -0
  105. package/dist/types/util/sanitizer.d.ts +15 -11
  106. package/dist/types/util/xlsx.d.mts +1 -0
  107. package/dist/types/util/xlsx.d.ts +17 -0
  108. package/dist/types/util/zip.d.ts +11 -0
  109. package/package.json +101 -10
  110. package/scss/_base.scss +13 -0
  111. package/scss/_config.scss +14 -0
  112. package/scss/_functions.scss +1 -0
  113. package/scss/_mixins.scss +1 -0
  114. package/scss/base/_buttons.scss +113 -0
  115. package/scss/base/_forms.scss +157 -0
  116. package/scss/base/_helpers.scss +24 -0
  117. package/scss/base/_pagination.scss +100 -0
  118. package/scss/base/_reboot.scss +93 -0
  119. package/scss/base/_root.scss +85 -0
  120. package/scss/base/_spinner.scss +45 -0
  121. package/scss/base/_table.scss +78 -0
  122. package/scss/components/_data-grid.scss +807 -0
  123. package/scss/data-grid.scss +7 -555
  124. package/scss/data-grid.standalone.scss +16 -0
  125. package/scss/functions/_defaults.scss +25 -0
  126. package/scss/mixins/_tokens.scss +12 -0
  127. package/src/{base-component.js → base-component.ts} +21 -17
  128. package/src/data-grid.ts +523 -201
  129. package/src/dom/{data.js → data.ts} +13 -10
  130. package/src/dom/event-handler.ts +380 -0
  131. package/src/dom/{manipulator.js → manipulator.ts} +11 -11
  132. package/src/engine/active-cell.ts +10 -3
  133. package/src/engine/data-source.ts +7 -3
  134. package/src/engine/features-lite.ts +47 -0
  135. package/src/engine/features.ts +68 -0
  136. package/src/engine/table.ts +86 -45
  137. package/src/engine/validate.ts +64 -0
  138. package/src/engine/virtualizer.ts +17 -1
  139. package/src/icons.ts +11 -5
  140. package/src/index-lite.ts +85 -0
  141. package/src/index.ts +30 -2
  142. package/src/labels.ts +6 -4
  143. package/src/types.ts +87 -32
  144. package/src/ui/body.ts +73 -17
  145. package/src/ui/header.ts +51 -26
  146. package/src/ui/loading.ts +2 -2
  147. package/src/ui/pinning.ts +16 -16
  148. package/src/ui/toolbar.ts +27 -3
  149. package/src/util/clipboard.ts +112 -0
  150. package/src/util/{config.js → config.ts} +25 -11
  151. package/src/util/csv.ts +11 -30
  152. package/src/util/direction.ts +37 -0
  153. package/src/util/drag.ts +832 -0
  154. package/src/util/export-table.ts +86 -0
  155. package/src/util/{index.js → index.ts} +55 -35
  156. package/src/util/print.ts +125 -0
  157. package/src/util/{sanitizer.js → sanitizer.ts} +17 -13
  158. package/src/util/xlsx.ts +183 -0
  159. package/src/util/zip.ts +170 -0
  160. package/src/base-component.d.ts +0 -19
  161. package/src/dom/event-handler.d.ts +0 -11
  162. package/src/dom/event-handler.js +0 -331
  163. package/src/dom/manipulator.d.ts +0 -9
  164. package/src/util/config.d.ts +0 -11
  165. package/src/util/index.d.ts +0 -10
  166. package/src/util/sanitizer.d.ts +0 -11
package/src/types.ts CHANGED
@@ -6,33 +6,58 @@
6
6
  */
7
7
 
8
8
  import type {
9
- Column, ColumnFiltersState, ColumnOrderState, ColumnPinningState, ColumnSizingState, PaginationState,
10
- RowSelectionState, SortingState, Table, VisibilityState
9
+ Column, ColumnFiltersState, ColumnOrderState, ColumnPinningState, ColumnSizingState,
10
+ ColumnVisibilityState, PaginationState, Row, RowSelectionState, SortingState, Table,
11
+ TableFeatures, TableState
11
12
  } from '@tanstack/table-core'
12
- import type { CsvDownloadOptions } from './util/csv'
13
+ import type { DataGridFeatures } from './engine/features'
14
+ import type { DataGridExportOptions } from './ui/toolbar'
13
15
 
14
16
  export type {
15
17
  Column,
16
18
  ColumnFiltersState,
17
19
  ColumnOrderState,
18
20
  ColumnPinningState,
21
+ ColumnPinningPosition,
19
22
  ColumnSizingState,
23
+ ColumnVisibilityState,
20
24
  PaginationState,
21
25
  RowSelectionState,
22
26
  SortingState,
23
27
  Table,
24
- VisibilityState
28
+ TableFeatures
25
29
  } from '@tanstack/table-core'
30
+ export type { DataGridFeatures } from './engine/features'
31
+ export type { DataGridLiteFeatures } from './engine/features-lite'
26
32
 
27
33
  export type DataGridItem = Record<string, unknown>
28
34
 
29
- export interface DataGridColumnFilterContext {
30
- column: Column<DataGridItem, unknown>
31
- table: Table<DataGridItem>
35
+ // The table-core instance as this grid registers it - every public surface
36
+ // that hands out a table (slot contexts, CSV helpers, `grid.table`) carries
37
+ // the grid's feature set as TFeatures. v9's TFeatures is invariant, so a
38
+ // lite-set table is NOT assignable to the full-set default - the parameter is
39
+ // what keeps a reduced grid's surface honest.
40
+ export type DataGridTable<TFeatures extends TableFeatures = DataGridFeatures> =
41
+ Table<TFeatures, DataGridItem>
42
+
43
+ export type DataGridTableState<TFeatures extends TableFeatures = DataGridFeatures> =
44
+ TableState<TFeatures>
45
+
46
+ export type DataGridRow<TFeatures extends TableFeatures = DataGridFeatures> =
47
+ Row<TFeatures, DataGridItem>
48
+
49
+ // v9 requires both sides on ColumnPinningState; the option stays partial so
50
+ // `{ start: ['id'] }` is still a valid seed.
51
+ export type DataGridColumnPinning = Partial<ColumnPinningState>
52
+
53
+ export interface DataGridColumnFilterContext<TFeatures extends TableFeatures = DataGridFeatures> {
54
+ column: Column<TFeatures, DataGridItem, unknown>
55
+ table: DataGridTable<TFeatures>
32
56
  labels: DataGridLabels
33
57
  }
34
58
 
35
- export type DataGridColumnFilterFactory = (context: DataGridColumnFilterContext) => DataGridSlot
59
+ export type DataGridColumnFilterFactory<TFeatures extends TableFeatures = DataGridFeatures> =
60
+ (context: DataGridColumnFilterContext<TFeatures>) => DataGridSlot
36
61
 
37
62
  export type DataGridFilterType = 'text' | 'number' | 'date' | 'select'
38
63
 
@@ -112,12 +137,12 @@ export interface DataGridEditCommitEvent {
112
137
  previousValue: unknown
113
138
  }
114
139
 
115
- export interface DataGridColumn {
140
+ export interface DataGridColumn<TFeatures extends TableFeatures = DataGridFeatures> {
116
141
  key: string
117
142
  editable?: boolean | DataGridEditableOptions
118
143
  editor?: DataGridEditorFactory
119
144
  editValidate?: (value: unknown, item: DataGridItem) => true | string
120
- filter?: DataGridColumnFilterFactory
145
+ filter?: DataGridColumnFilterFactory<TFeatures>
121
146
  filterable?: boolean
122
147
  filterFn?: (value: unknown, filterValue: unknown, item: DataGridItem) => boolean
123
148
  filterType?: DataGridFilterType
@@ -166,8 +191,8 @@ export interface DataGridLabels {
166
191
  lastPage: string
167
192
  loadError: string
168
193
  loading: string
169
- moveLeft: string
170
- moveRight: string
194
+ moveEnd: string
195
+ moveStart: string
171
196
  nextPage: string
172
197
  operatorBetween: string
173
198
  operatorBlank: string
@@ -184,10 +209,11 @@ export interface DataGridLabels {
184
209
  operatorStartsWith: string
185
210
  pageSizeLabel: string
186
211
  paginationLabel: string
187
- pinLeft: string
188
- pinRight: string
212
+ pinEnd: string
213
+ pinStart: string
189
214
  previousPage: string
190
215
  redoneAnnouncement: string
216
+ reorderRow: string
191
217
  resetColumns: string
192
218
  resultsAnnouncement: string
193
219
  searchValues: string
@@ -199,6 +225,7 @@ export interface DataGridLabels {
199
225
  sortDescending: string
200
226
  toolbarColumns: string
201
227
  toolbarExport: string
228
+ toolbarPrint: string
202
229
  toolbarRedo: string
203
230
  toolbarUndo: string
204
231
  undoneAnnouncement: string
@@ -209,8 +236,8 @@ export interface DataGridColumnSizingOptions {
209
236
  mode?: 'onChange' | 'onEnd'
210
237
  }
211
238
 
212
- export interface DataGridSlotContext {
213
- table: Table<DataGridItem>
239
+ export interface DataGridSlotContext<TFeatures extends TableFeatures = DataGridFeatures> {
240
+ table: DataGridTable<TFeatures>
214
241
  labels: DataGridLabels
215
242
  }
216
243
 
@@ -220,18 +247,20 @@ export interface DataGridSlot {
220
247
  dispose?: () => void
221
248
  }
222
249
 
223
- export type DataGridSlotFactory = (context: DataGridSlotContext) => DataGridSlot
250
+ export type DataGridSlotFactory<TFeatures extends TableFeatures = DataGridFeatures> =
251
+ (context: DataGridSlotContext<TFeatures>) => DataGridSlot
224
252
 
225
- export interface DataGridSlots {
226
- toolbar: DataGridSlotFactory
227
- pagination: DataGridSlotFactory
228
- empty: DataGridSlotFactory
253
+ export interface DataGridSlots<TFeatures extends TableFeatures = DataGridFeatures> {
254
+ toolbar: DataGridSlotFactory<TFeatures>
255
+ pagination: DataGridSlotFactory<TFeatures>
256
+ empty: DataGridSlotFactory<TFeatures>
229
257
  }
230
258
 
231
259
  export interface DataGridToolbarOptions {
232
260
  columns?: boolean
233
- export?: boolean | CsvDownloadOptions
261
+ export?: boolean | DataGridExportOptions
234
262
  history?: boolean
263
+ print?: boolean
235
264
  search?: boolean
236
265
  }
237
266
 
@@ -253,6 +282,13 @@ export interface DataGridPaginationInfoState {
253
282
  totalRows: number
254
283
  }
255
284
 
285
+ export interface DataGridInfiniteScrollOptions {
286
+ pageSize?: number
287
+ // Rows left below the last rendered one when the next page is requested.
288
+ // Defaults to the grid's overscan.
289
+ threshold?: number
290
+ }
291
+
256
292
  export interface DataGridPaginationOptions {
257
293
  info?: boolean | ((state: DataGridPaginationInfoState) => string)
258
294
  pageSize?: number
@@ -268,7 +304,7 @@ export interface DataGridState {
268
304
  columnOrder: ColumnOrderState
269
305
  columnPinning: ColumnPinningState
270
306
  columnSizing: ColumnSizingState
271
- columnVisibility: VisibilityState
307
+ columnVisibility: ColumnVisibilityState
272
308
  globalFilter: string
273
309
  pagination: PaginationState
274
310
  rowSelection: RowSelectionState
@@ -287,37 +323,55 @@ export interface DataGridDataResponse {
287
323
  totalRows: number
288
324
  }
289
325
 
290
- export interface DataGridOptions {
326
+ export interface DataGridOptions<TFeatures extends TableFeatures = DataGridFeatures> {
327
+ // Measure each rendered row instead of trusting rowHeight, so rows can grow
328
+ // with their content. rowHeight stays the estimate for unmeasured rows.
329
+ autoRowHeight: boolean
291
330
  cellNavigation: boolean
331
+ // Spreadsheet-style cell ranges: shift-click, drag and shift+arrows, with
332
+ // Ctrl/Cmd+C copying the range as TSV. Implies cellNavigation.
333
+ cellSelection: boolean
292
334
  columnFilters: boolean
293
335
  columnMenu: boolean | DataGridColumnMenuBuilder
294
336
  columnMenuIcon: string
295
337
  columnOrder: boolean | ColumnOrderState
296
- columnPinning: boolean | ColumnPinningState
338
+ columnPinning: boolean | DataGridColumnPinning
297
339
  columnSizing: boolean | DataGridColumnSizingOptions
298
- columnVisibility: boolean | VisibilityState
299
- columns: DataGridColumn[]
340
+ columnVisibility: boolean | ColumnVisibilityState
341
+ columns: Array<DataGridColumn<TFeatures>>
300
342
  dataProvider: ((request: DataGridDataRequest) => Promise<DataGridDataResponse>) | null
301
343
  editing: boolean
302
344
  empty: string | (() => Node | string)
345
+ // The registered table-core feature set; null means the full built-in
346
+ // dataGridFeatures. Presets: dataGridFeatures, dataGridLiteFeatures.
347
+ features: TFeatures | null
303
348
  filterIcon: string
304
349
  globalFilter: boolean
305
350
  hideColumnIcon: string
306
351
  history: boolean
352
+ // Append each loaded page to the rows already in view instead of replacing
353
+ // them. Requires dataProvider, cannot be combined with pagination.
354
+ infiniteScroll: boolean | DataGridInfiniteScrollOptions
307
355
  itemKey: ((item: DataGridItem, index: number) => string) | null
308
356
  items: DataGridItem[]
309
357
  labels: Partial<DataGridLabels>
310
- moveLeftIcon: string
311
- moveRightIcon: string
358
+ moveEndIcon: string
359
+ moveStartIcon: string
312
360
  overscan: number
313
361
  pagination: boolean | DataGridPaginationOptions
314
- pinLeftIcon: string
315
- pinRightIcon: string
362
+ pinEndIcon: string
363
+ pinStartIcon: string
364
+ // Heading printed above the table; omitted when null.
365
+ printTitle: string | null
366
+ rowHandleIcon: string
316
367
  rowHeight: number
368
+ // A drag handle per row; the grid never mutates `items` — it emits
369
+ // rowOrderChange with the reordered array for the app to apply.
370
+ rowOrder: boolean
317
371
  rowSelection: boolean | DataGridRowSelectionOptions
318
372
  sanitize: boolean
319
373
  sanitizeFn: ((html: string) => string) | null
320
- slots: Partial<DataGridSlots>
374
+ slots: Partial<DataGridSlots<TFeatures>>
321
375
  sortAscendingIcon: string
322
376
  sortDescendingIcon: string
323
377
  sortNeutralIcon: string
@@ -327,6 +381,7 @@ export interface DataGridOptions {
327
381
  toolbar: boolean | DataGridToolbarOptions
328
382
  toolbarColumnsIcon: string
329
383
  toolbarExportIcon: string
384
+ toolbarPrintIcon: string
330
385
  toolbarRedoIcon: string
331
386
  toolbarUndoIcon: string
332
387
  unpinIcon: string
package/src/ui/body.ts CHANGED
@@ -5,19 +5,24 @@
5
5
  * --------------------------------------------------------------------------
6
6
  */
7
7
 
8
- import type { Row } from '@tanstack/table-core'
9
8
  import type { GridVirtualizerInstance } from '../engine/virtualizer'
10
- import type { DataGridColumn, DataGridItem, DataGridLabels } from '../types'
9
+ import type { DataGridColumn, DataGridLabels, DataGridRow } from '../types'
11
10
  import { applyPinnedCell, type GridPinning } from './pinning'
12
11
 
13
12
  const CLASS_NAME_EMPTY = 'data-grid-empty'
14
13
  const FOCUSABLE_SELECTOR = 'a[href], button, input, select, textarea, [tabindex]'
15
14
  const CLASS_NAME_ROW_ODD = 'data-grid-row-odd'
16
15
  const CLASS_NAME_ROW_SELECTED = 'table-active'
16
+ const CLASS_NAME_CELL_SELECTED = 'data-grid-cell-selected'
17
+ const CLASS_NAME_HANDLE_CELL = 'data-grid-row-handle-cell'
18
+ const EDGE_CLASS_NAMES = ['top', 'right', 'bottom', 'left'] as const
17
19
  const CLASS_NAME_SELECT_CELL = 'data-grid-select-cell'
18
20
  const CLASS_NAME_SPACER = 'data-grid-spacer'
19
21
 
20
22
  export interface GridBodyContext {
23
+ // Hands every rendered row to the virtualizer for measurement instead of
24
+ // assuming a fixed rowHeight.
25
+ autoRowHeight: boolean
21
26
  // With cellNavigation on, rows/cells carry grid roles, every cell (and the
22
27
  // interactive content inside it) drops out of the tab order, and cells are
23
28
  // addressable by data-coreui-column - the roving tabindex pass in the grid
@@ -35,13 +40,16 @@ export interface GridBodyContext {
35
40
  // Absolute index of the page's first row (pagination mode), else 0 - keeps
36
41
  // aria-rowindex absolute like aria-rowcount.
37
42
  getRowIndexOffset: () => number
38
- getRows: () => Array<Row<DataGridItem>>
43
+ getRows: () => DataGridRow[]
39
44
  // Visible leaf columns in table order (pinning + columnOrder + visibility).
40
45
  getVisibleColumns: () => DataGridColumn[]
41
46
  getVirtualizer: () => GridVirtualizerInstance | null
42
47
  renderEmpty: () => Node | string
43
48
  // Guards every string injected via innerHTML (render results, empty state).
44
49
  sanitizeContent: (html: string) => string
50
+ cellSelection: boolean
51
+ rowOrder: boolean
52
+ rowHandleIcon: string
45
53
  selection: boolean
46
54
  }
47
55
 
@@ -76,7 +84,10 @@ export const createBody = (context: GridBodyContext): GridBody => {
76
84
  // rebuilt (not recycled) when the visible column set or order changes.
77
85
  let currentColumns: DataGridColumn[] = context.getVisibleColumns()
78
86
  let layoutSignature = currentColumns.map(column => column.key).join('|')
79
- let cellCount = currentColumns.length + (context.selection ? 1 : 0)
87
+ // The handle column and the select column both sit before the data columns;
88
+ // every cell index in this module is offset by however many are on.
89
+ const leadingCount = (context.rowOrder ? 1 : 0) + (context.selection ? 1 : 0)
90
+ let cellCount = currentColumns.length + leadingCount
80
91
 
81
92
  const buildRowCells = (rowElement: HTMLTableRowElement): void => {
82
93
  rowElement.replaceChildren()
@@ -85,7 +96,24 @@ export const createBody = (context: GridBodyContext): GridBody => {
85
96
  rowElement.setAttribute('role', 'row')
86
97
  }
87
98
 
88
- const offset = context.selection ? 1 : 0
99
+ const offset = leadingCount
100
+
101
+ if (context.rowOrder) {
102
+ const cellElement = document.createElement('td')
103
+ cellElement.classList.add(CLASS_NAME_HANDLE_CELL)
104
+ const handle = document.createElement('span')
105
+ handle.classList.add('data-grid-row-handle')
106
+ handle.dataset.coreuiRowHandle = ''
107
+ handle.setAttribute('aria-hidden', 'true')
108
+ handle.innerHTML = context.rowHandleIcon
109
+ cellElement.append(handle)
110
+ if (context.cellNavigation) {
111
+ cellElement.setAttribute('role', 'gridcell')
112
+ cellElement.setAttribute('aria-colindex', '1')
113
+ }
114
+
115
+ rowElement.append(cellElement)
116
+ }
89
117
 
90
118
  if (context.selection) {
91
119
  const cellElement = document.createElement('td')
@@ -97,7 +125,7 @@ export const createBody = (context: GridBodyContext): GridBody => {
97
125
  input.dataset.coreuiRowSelect = ''
98
126
  if (context.cellNavigation) {
99
127
  cellElement.setAttribute('role', 'gridcell')
100
- cellElement.setAttribute('aria-colindex', '1')
128
+ cellElement.setAttribute('aria-colindex', String(context.rowOrder ? 2 : 1))
101
129
  input.tabIndex = -1
102
130
  }
103
131
 
@@ -126,8 +154,8 @@ export const createBody = (context: GridBodyContext): GridBody => {
126
154
  return rowElement
127
155
  }
128
156
 
129
- const updateRowCells = (rowElement: HTMLTableRowElement, row: Row<DataGridItem>, index: number): void => {
130
- const offset = context.selection ? 1 : 0
157
+ const updateRowCells = (rowElement: HTMLTableRowElement, row: DataGridRow, index: number): void => {
158
+ const offset = leadingCount
131
159
  const editingCell = context.getEditingCell()
132
160
 
133
161
  for (const [cellIndex, column] of currentColumns.entries()) {
@@ -159,14 +187,15 @@ export const createBody = (context: GridBodyContext): GridBody => {
159
187
  }
160
188
  }
161
189
 
162
- const updateRowElement = (rowElement: HTMLTableRowElement, row: Row<DataGridItem>, index: number): void => {
190
+ const updateRowElement = (rowElement: HTMLTableRowElement, row: DataGridRow, index: number): void => {
163
191
  if (rowElement.dataset.coreuiLayout !== layoutSignature) {
164
192
  buildRowCells(rowElement)
165
193
  delete rowElement.dataset.coreuiRow
166
194
  }
167
195
 
168
196
  if (context.selection) {
169
- const input = rowElement.firstElementChild!.firstElementChild as HTMLInputElement
197
+ const selectCell = rowElement.children[leadingCount - 1] as HTMLTableCellElement
198
+ const input = selectCell.firstElementChild as HTMLInputElement
170
199
  const selected = row.getIsSelected()
171
200
  input.checked = selected
172
201
  rowElement.classList.toggle(CLASS_NAME_ROW_SELECTED, selected)
@@ -183,15 +212,33 @@ export const createBody = (context: GridBodyContext): GridBody => {
183
212
  rowElement.dataset.coreuiRow = rowKey
184
213
  }
185
214
 
215
+ // Selection is repainted on every pass, unlike cell content: it changes
216
+ // without the row's data version or id changing.
217
+ if (context.cellSelection) {
218
+ const cells = new Map(row.getAllCells().map(cell => [cell.column.id, cell]))
219
+ for (const [cellIndex, column] of currentColumns.entries()) {
220
+ const cellElement = rowElement.children[cellIndex + leadingCount] as HTMLTableCellElement
221
+ const cell = cells.get(column.key)
222
+ const selected = cell?.getIsSelected() ?? false
223
+ cellElement.classList.toggle(CLASS_NAME_CELL_SELECTED, selected)
224
+ const edges = selected && cell ? cell.getSelectionEdges() : null
225
+ for (const edge of EDGE_CLASS_NAMES) {
226
+ cellElement.classList.toggle(`${CLASS_NAME_CELL_SELECTED}-${edge}`, Boolean(edges?.[edge]))
227
+ }
228
+ }
229
+ }
230
+
186
231
  rowElement.dataset.coreuiIndex = String(index)
187
232
  rowElement.setAttribute('aria-rowindex', String(context.headerRowCount + context.getRowIndexOffset() + index + 1))
188
233
  rowElement.classList.toggle(CLASS_NAME_ROW_ODD, index % 2 === 1)
189
234
 
190
235
  if (currentPinning) {
191
236
  const pinning = currentPinning
192
- const offset = context.selection ? 1 : 0
193
- if (context.selection) {
194
- applyPinnedCell(rowElement.children[0] as HTMLTableCellElement, pinning.select)
237
+ const offset = leadingCount
238
+ // Leading cells share the select column's pin — they travel together at
239
+ // the start edge.
240
+ for (let cellIndex = 0; cellIndex < leadingCount; cellIndex++) {
241
+ applyPinnedCell(rowElement.children[cellIndex] as HTMLTableCellElement, pinning.select)
195
242
  }
196
243
 
197
244
  for (const [cellIndex, column] of currentColumns.entries()) {
@@ -244,7 +291,7 @@ export const createBody = (context: GridBodyContext): GridBody => {
244
291
  element.replaceChildren(emptyElement)
245
292
  }
246
293
 
247
- const renderAll = (rows: Array<Row<DataGridItem>>) => {
294
+ const renderAll = (rows: DataGridRow[]) => {
248
295
  if (emptyElement) {
249
296
  emptyElement.remove()
250
297
  }
@@ -266,7 +313,7 @@ export const createBody = (context: GridBodyContext): GridBody => {
266
313
  }
267
314
  }
268
315
 
269
- const renderWindow = (rows: Array<Row<DataGridItem>>, virtualizer: GridVirtualizerInstance) => {
316
+ const renderWindow = (rows: DataGridRow[], virtualizer: GridVirtualizerInstance) => {
270
317
  if (!spacersAttached) {
271
318
  element.replaceChildren(topSpacer, bottomSpacer)
272
319
  spacersAttached = true
@@ -296,7 +343,16 @@ export const createBody = (context: GridBodyContext): GridBody => {
296
343
  }
297
344
 
298
345
  for (const [index, virtualItem] of virtualItems.entries()) {
299
- updateRowElement(rowElements[index], rows[virtualItem.index], virtualItem.index)
346
+ const rowElement = rowElements[index]
347
+ updateRowElement(rowElement, rows[virtualItem.index], virtualItem.index)
348
+
349
+ if (context.autoRowHeight) {
350
+ // The virtualizer resolves a measurement to its row through this
351
+ // attribute, so it has to be current BEFORE the element is handed
352
+ // over - recycled elements arrive carrying the previous index.
353
+ rowElement.dataset.index = String(virtualItem.index)
354
+ virtualizer.measureElement(rowElement)
355
+ }
300
356
  }
301
357
  }
302
358
 
@@ -325,7 +381,7 @@ export const createBody = (context: GridBodyContext): GridBody => {
325
381
  if (signature !== layoutSignature) {
326
382
  currentColumns = columns
327
383
  layoutSignature = signature
328
- cellCount = currentColumns.length + (context.selection ? 1 : 0);
384
+ cellCount = currentColumns.length + leadingCount;
329
385
  (topSpacer.firstElementChild as HTMLTableCellElement).colSpan = Math.max(cellCount, 1);
330
386
  (bottomSpacer.firstElementChild as HTMLTableCellElement).colSpan = Math.max(cellCount, 1)
331
387
  }
package/src/ui/header.ts CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { interpolate } from '../labels'
9
9
  import type {
10
- DataGridColumn, DataGridItem, DataGridLabels, DataGridSlot, SortingState, Table
10
+ DataGridColumn, DataGridLabels, DataGridSlot, DataGridTable, SortingState
11
11
  } from '../types'
12
12
  import { applyPinnedCell, type GridPinning } from './pinning'
13
13
 
@@ -17,6 +17,7 @@ const CLASS_NAME_HEADER_LABEL = 'data-grid-header-label'
17
17
  const CLASS_NAME_FILTER_ROW = 'data-grid-filter-row'
18
18
  const CLASS_NAME_RESIZER = 'data-grid-resizer'
19
19
  const CLASS_NAME_RESIZER_ACTIVE = 'data-grid-resizer-active'
20
+ const CLASS_NAME_HANDLE_CELL = 'data-grid-row-handle-cell'
20
21
  const CLASS_NAME_SELECT_CELL = 'data-grid-select-cell'
21
22
  const CLASS_NAME_SORTABLE = 'data-grid-sortable'
22
23
  const CLASS_NAME_SORT_BUTTON = 'data-grid-sort-button'
@@ -44,9 +45,10 @@ export interface GridHeaderContext {
44
45
  isSortable: (column: DataGridColumn) => boolean
45
46
  labels: DataGridLabels
46
47
  resizable: boolean
48
+ rowOrder: boolean
47
49
  selection: boolean
48
50
  selectAll: boolean
49
- table: Table<DataGridItem>
51
+ table: DataGridTable
50
52
  }
51
53
 
52
54
  export interface GridHeaderState {
@@ -113,6 +115,15 @@ const createSelectAllCell = (context: GridHeaderContext): { cell: HTMLTableCellE
113
115
  return { cell, input }
114
116
  }
115
117
 
118
+ // The handle column has no header of its own; the cell only reserves the
119
+ // width so the body's grips line up under it.
120
+ const createHandleHeaderCell = (): HTMLTableCellElement => {
121
+ const cell = document.createElement('th')
122
+ cell.classList.add(CLASS_NAME_HANDLE_CELL)
123
+ cell.setAttribute('scope', 'col')
124
+ return cell
125
+ }
126
+
116
127
  const createResizer = (): HTMLElement => {
117
128
  const resizer = document.createElement('span')
118
129
  resizer.classList.add(CLASS_NAME_RESIZER)
@@ -129,20 +140,30 @@ const createLabelRow = (
129
140
  ): {
130
141
  row: HTMLTableRowElement
131
142
  selectAllInput: HTMLInputElement | null
132
- selectCell: HTMLTableCellElement | null
143
+ leadingCells: HTMLTableCellElement[]
133
144
  } => {
134
145
  const row = document.createElement('tr')
135
146
  let selectAllInput: HTMLInputElement | null = null
136
- let selectCell: HTMLTableCellElement | null = null
147
+ const leadingCells: HTMLTableCellElement[] = []
137
148
 
138
149
  if (context.cellNavigation) {
139
150
  row.setAttribute('role', 'row')
140
151
  }
141
152
 
153
+ if (context.rowOrder) {
154
+ const cell = createHandleHeaderCell()
155
+ if (context.cellNavigation) {
156
+ cell.setAttribute('role', 'columnheader')
157
+ }
158
+
159
+ leadingCells.push(cell)
160
+ row.append(cell)
161
+ }
162
+
142
163
  if (context.selection) {
143
164
  const { cell, input } = createSelectAllCell(context)
144
165
  selectAllInput = input
145
- selectCell = cell
166
+ leadingCells.push(cell)
146
167
  if (context.cellNavigation) {
147
168
  cell.setAttribute('role', 'columnheader')
148
169
  if (input) {
@@ -240,25 +261,28 @@ const createLabelRow = (
240
261
  row.append(cell)
241
262
  }
242
263
 
243
- return { row, selectAllInput, selectCell }
264
+ return { row, selectAllInput, leadingCells }
244
265
  }
245
266
 
246
267
  const createFilterRow = (
247
268
  context: GridHeaderContext,
248
269
  filterCells: Map<string, HTMLTableCellElement>,
249
270
  filterSlots: DataGridSlot[]
250
- ): { row: HTMLTableRowElement; selectCell: HTMLTableCellElement | null } => {
271
+ ): { row: HTMLTableRowElement; leadingCells: HTMLTableCellElement[] } => {
251
272
  const row = document.createElement('tr')
252
273
  row.classList.add(CLASS_NAME_FILTER_ROW)
253
274
  if (context.cellNavigation) {
254
275
  row.setAttribute('role', 'row')
255
276
  }
256
277
 
257
- let selectCell: HTMLTableCellElement | null = null
278
+ const leadingCells: HTMLTableCellElement[] = []
258
279
 
259
- if (context.selection) {
260
- selectCell = document.createElement('th')
261
- row.append(selectCell)
280
+ for (const leading of [context.rowOrder, context.selection]) {
281
+ if (leading) {
282
+ const cell = document.createElement('th')
283
+ leadingCells.push(cell)
284
+ row.append(cell)
285
+ }
262
286
  }
263
287
 
264
288
  for (const column of context.columns) {
@@ -274,7 +298,7 @@ const createFilterRow = (
274
298
  row.append(cell)
275
299
  }
276
300
 
277
- return { row, selectCell }
301
+ return { row, leadingCells }
278
302
  }
279
303
 
280
304
  export const createHeader = (context: GridHeaderContext): GridHeader => {
@@ -288,14 +312,14 @@ export const createHeader = (context: GridHeaderContext): GridHeader => {
288
312
  const filterSlots: DataGridSlot[] = []
289
313
  const movableColumns = new Map(context.columns.map(column => [column.key, column.movable !== false]))
290
314
 
291
- const { row, selectAllInput, selectCell } = createLabelRow(context, cells, resizers, sortIcons)
315
+ const { row, selectAllInput, leadingCells } = createLabelRow(context, cells, resizers, sortIcons)
292
316
  element.append(row)
293
317
 
294
318
  let filterRowElement: HTMLTableRowElement | null = null
295
- let filterSelectCell: HTMLTableCellElement | null = null
319
+ let filterLeadingCells: HTMLTableCellElement[] = []
296
320
  if (context.filterRow) {
297
321
  const filterRow = createFilterRow(context, filterCells, filterSlots)
298
- filterSelectCell = filterRow.selectCell
322
+ filterLeadingCells = filterRow.leadingCells
299
323
  filterRowElement = filterRow.row
300
324
  element.append(filterRow.row)
301
325
  }
@@ -309,9 +333,9 @@ export const createHeader = (context: GridHeaderContext): GridHeader => {
309
333
  const reorderRow = (
310
334
  rowElement: HTMLTableRowElement,
311
335
  cellMap: Map<string, HTMLTableCellElement>,
312
- leading: HTMLTableCellElement | null
336
+ leading: HTMLTableCellElement[]
313
337
  ) => {
314
- const ordered: HTMLTableCellElement[] = leading ? [leading] : []
338
+ const ordered: HTMLTableCellElement[] = [...leading]
315
339
  for (const id of order) {
316
340
  const cell = cellMap.get(id)
317
341
  if (cell) {
@@ -333,9 +357,9 @@ export const createHeader = (context: GridHeaderContext): GridHeader => {
333
357
  }
334
358
  }
335
359
 
336
- reorderRow(row, cells, selectCell)
360
+ reorderRow(row, cells, leadingCells)
337
361
  if (filterRowElement) {
338
- reorderRow(filterRowElement, filterCells, filterSelectCell)
362
+ reorderRow(filterRowElement, filterCells, filterLeadingCells)
339
363
  }
340
364
  }
341
365
 
@@ -388,11 +412,14 @@ export const createHeader = (context: GridHeaderContext): GridHeader => {
388
412
 
389
413
  for (const [key, cell] of cells) {
390
414
  const draggable = Boolean(movableColumns.get(key)) && (movablePerRegion.get(regionOf(key)) ?? 0) > 1
391
- cell.draggable = draggable
392
415
  if (draggable) {
393
416
  cell.dataset.coreuiDraggable = ''
417
+ // Without this the browser scrolls (or picks text) instead of
418
+ // handing the pointer stream to the drag controller on touch.
419
+ cell.style.touchAction = 'none'
394
420
  } else {
395
421
  delete cell.dataset.coreuiDraggable
422
+ cell.style.touchAction = ''
396
423
  }
397
424
  }
398
425
  }
@@ -414,12 +441,10 @@ export const createHeader = (context: GridHeaderContext): GridHeader => {
414
441
  applyPinnedCell(cell, state.pinning.columns[key] ?? null)
415
442
  }
416
443
 
417
- if (selectCell) {
418
- applyPinnedCell(selectCell, state.pinning.select)
419
- }
420
-
421
- if (filterSelectCell) {
422
- applyPinnedCell(filterSelectCell, state.pinning.select)
444
+ // Every leading cell (the handle column, the select column) shares
445
+ // the select column's pin — they travel together at the start edge.
446
+ for (const cell of [...leadingCells, ...filterLeadingCells]) {
447
+ applyPinnedCell(cell, state.pinning.select)
423
448
  }
424
449
  }
425
450
 
package/src/ui/loading.ts CHANGED
@@ -14,9 +14,9 @@ export interface GridLoading {
14
14
  setVisible: (visible: boolean) => void
15
15
  }
16
16
 
17
- export const createLoading = (labels: DataGridLabels): GridLoading => {
17
+ export const createLoading = (labels: DataGridLabels, className = CLASS_NAME_LOADING): GridLoading => {
18
18
  const element = document.createElement('div')
19
- element.classList.add(CLASS_NAME_LOADING)
19
+ element.classList.add(className)
20
20
  element.hidden = true
21
21
 
22
22
  const spinner = document.createElement('div')