@snack-uikit/table 0.29.0 → 0.30.1-preview-9a03a1c9.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 (66) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +2 -0
  3. package/dist/cjs/components/Table/Table.d.ts +1 -1
  4. package/dist/cjs/components/Table/Table.js +141 -104
  5. package/dist/cjs/components/Table/hooks/index.d.ts +1 -0
  6. package/dist/cjs/components/Table/hooks/index.js +2 -1
  7. package/dist/cjs/components/Table/hooks/useColumnOrderByDrag.d.ts +9 -0
  8. package/dist/cjs/components/Table/hooks/useColumnOrderByDrag.js +76 -0
  9. package/dist/cjs/components/Table/utils.js +6 -3
  10. package/dist/cjs/components/types.d.ts +2 -0
  11. package/dist/cjs/constants.d.ts +1 -0
  12. package/dist/cjs/constants.js +3 -2
  13. package/dist/cjs/helperComponents/Cells/BodyCell/BodyCell.d.ts +2 -1
  14. package/dist/cjs/helperComponents/Cells/BodyCell/BodyCell.js +13 -3
  15. package/dist/cjs/helperComponents/Cells/HeaderCell/DragHandle.d.ts +6 -0
  16. package/dist/cjs/helperComponents/Cells/HeaderCell/DragHandle.js +27 -0
  17. package/dist/cjs/helperComponents/Cells/HeaderCell/HeaderCell.d.ts +2 -1
  18. package/dist/cjs/helperComponents/Cells/HeaderCell/HeaderCell.js +22 -2
  19. package/dist/cjs/helperComponents/Cells/HeaderCell/styles.module.css +11 -0
  20. package/dist/cjs/helperComponents/Rows/BodyRow.d.ts +4 -1
  21. package/dist/cjs/helperComponents/Rows/BodyRow.js +30 -12
  22. package/dist/cjs/helperComponents/Rows/HeaderRow.d.ts +7 -1
  23. package/dist/cjs/helperComponents/Rows/HeaderRow.js +33 -13
  24. package/dist/cjs/helperComponents/hooks.d.ts +22 -13
  25. package/dist/cjs/helperComponents/hooks.js +63 -15
  26. package/dist/cjs/types.d.ts +5 -0
  27. package/dist/esm/components/Table/Table.d.ts +1 -1
  28. package/dist/esm/components/Table/Table.js +34 -25
  29. package/dist/esm/components/Table/hooks/index.d.ts +1 -0
  30. package/dist/esm/components/Table/hooks/index.js +1 -0
  31. package/dist/esm/components/Table/hooks/useColumnOrderByDrag.d.ts +9 -0
  32. package/dist/esm/components/Table/hooks/useColumnOrderByDrag.js +66 -0
  33. package/dist/esm/components/Table/utils.js +6 -3
  34. package/dist/esm/components/types.d.ts +2 -0
  35. package/dist/esm/constants.d.ts +1 -0
  36. package/dist/esm/constants.js +1 -0
  37. package/dist/esm/helperComponents/Cells/BodyCell/BodyCell.d.ts +2 -1
  38. package/dist/esm/helperComponents/Cells/BodyCell/BodyCell.js +7 -3
  39. package/dist/esm/helperComponents/Cells/HeaderCell/DragHandle.d.ts +6 -0
  40. package/dist/esm/helperComponents/Cells/HeaderCell/DragHandle.js +6 -0
  41. package/dist/esm/helperComponents/Cells/HeaderCell/HeaderCell.d.ts +2 -1
  42. package/dist/esm/helperComponents/Cells/HeaderCell/HeaderCell.js +13 -4
  43. package/dist/esm/helperComponents/Cells/HeaderCell/styles.module.css +11 -0
  44. package/dist/esm/helperComponents/Rows/BodyRow.d.ts +4 -1
  45. package/dist/esm/helperComponents/Rows/BodyRow.js +4 -3
  46. package/dist/esm/helperComponents/Rows/HeaderRow.d.ts +7 -1
  47. package/dist/esm/helperComponents/Rows/HeaderRow.js +4 -3
  48. package/dist/esm/helperComponents/hooks.d.ts +22 -13
  49. package/dist/esm/helperComponents/hooks.js +58 -15
  50. package/dist/esm/types.d.ts +5 -0
  51. package/package.json +6 -2
  52. package/src/components/Table/Table.tsx +151 -117
  53. package/src/components/Table/hooks/index.ts +1 -0
  54. package/src/components/Table/hooks/useColumnOrderByDrag.ts +100 -0
  55. package/src/components/Table/utils.ts +8 -3
  56. package/src/components/types.ts +3 -0
  57. package/src/constants.tsx +2 -0
  58. package/src/helperComponents/Cells/BodyCell/BodyCell.tsx +9 -2
  59. package/src/helperComponents/Cells/HeaderCell/DragHandle.tsx +18 -0
  60. package/src/helperComponents/Cells/HeaderCell/HeaderCell.tsx +19 -4
  61. package/src/helperComponents/Cells/HeaderCell/styles.module.scss +15 -4
  62. package/src/helperComponents/Rows/BodyRow.tsx +36 -9
  63. package/src/helperComponents/Rows/HeaderRow.tsx +51 -18
  64. package/src/helperComponents/Rows/styles.module.scss +2 -2
  65. package/src/helperComponents/hooks.ts +78 -15
  66. package/src/types.ts +6 -0
@@ -1,3 +1,5 @@
1
+ import { closestCenter, DndContext } from '@dnd-kit/core';
2
+ import { restrictToHorizontalAxis } from '@dnd-kit/modifiers';
1
3
  import {
2
4
  CellContext,
3
5
  ColumnPinningState,
@@ -43,7 +45,7 @@ import { getTreeColumnDef } from '../../helperComponents/Cells/TreeCell';
43
45
  import { ColumnDefinition } from '../../types';
44
46
  import { fuzzyFilter } from '../../utils';
45
47
  import { TableProps } from '../types';
46
- import { useLoadingTable, useStateControl } from './hooks';
48
+ import { useColumnOrderByDrag, useLoadingTable, useStateControl } from './hooks';
47
49
  import { usePageReset } from './hooks/usePageReset';
48
50
  import styles from './styles.module.scss';
49
51
  import {
@@ -97,6 +99,7 @@ export function Table<TData extends object, TFilters extends FiltersState = Reco
97
99
  savedState,
98
100
  expanding,
99
101
  bulkActions: bulkActionsProp,
102
+ enableColumnsOrderSortByDrag,
100
103
  ...rest
101
104
  }: TableProps<TData, TFilters>) {
102
105
  const { state: globalFilter, onStateChange: onGlobalFilterChange } = useStateControl<string>(search, '');
@@ -130,6 +133,9 @@ export function Table<TData extends object, TFilters extends FiltersState = Reco
130
133
  return cols;
131
134
  }, [columnDefinitions, enableSelection, enableSelectPinned, expanding]);
132
135
 
136
+ const { columnOrder, setColumnOrder, groupedColumnOrderState, sensors, handleDragEnd } =
137
+ useColumnOrderByDrag(tableColumns);
138
+
133
139
  const columnPinning = useMemo(() => {
134
140
  const pinningState: Required<ColumnPinningState> = { left: [], right: [] };
135
141
  for (const col of tableColumns) {
@@ -160,6 +166,7 @@ export function Table<TData extends object, TFilters extends FiltersState = Reco
160
166
  columns: tableColumns,
161
167
  state: {
162
168
  columnPinning,
169
+ columnOrder,
163
170
  globalFilter,
164
171
  rowSelection,
165
172
  sorting,
@@ -173,6 +180,7 @@ export function Table<TData extends object, TFilters extends FiltersState = Reco
173
180
  minSize: 40,
174
181
  cell: (cell: CellContext<TData, unknown>) => <TruncateString text={String(cell.getValue())} maxLines={1} />,
175
182
  },
183
+ onColumnOrderChange: setColumnOrder,
176
184
 
177
185
  manualSorting,
178
186
  manualPagination,
@@ -256,9 +264,11 @@ export function Table<TData extends object, TFilters extends FiltersState = Reco
256
264
  const columnSizeVarsRef = useRef<Record<string, string>>();
257
265
  const headers = table.getFlatHeaders();
258
266
 
259
- const columnSizeVars = useMemo(() => {
267
+ const columnSizes = useMemo(() => {
260
268
  const originalColumnDefs = table._getColumnDefs();
261
- const colSizes: Record<string, string> = {};
269
+ const vars: Record<string, string> = {};
270
+ const realSizes: Record<string, number> = {};
271
+ const resizedColumnIndex = headers.findIndex(({ column }) => column.getIsResizing());
262
272
 
263
273
  for (let i = 0; i < headers.length; i++) {
264
274
  const header = headers[i];
@@ -267,8 +277,8 @@ export function Table<TData extends object, TFilters extends FiltersState = Reco
267
277
  const originalColDef = originalColumnDefs.find(col => getColumnId(header) === col.id);
268
278
 
269
279
  if (header.id === 'snack_predefined_statusColumn' && !originalColDef?.header && !originalColDef?.enableSorting) {
270
- colSizes[sizeKey] = 'var(--size-table-cell-status-indicator-horizontal)';
271
- colSizes[flexKey] = '100%';
280
+ vars[sizeKey] = 'var(--size-table-cell-status-indicator-horizontal)';
281
+ vars[flexKey] = '100%';
272
282
  } else {
273
283
  const originalColumnDefSize = originalColDef?.size;
274
284
  let initSize = originalColumnDefSize ? `${originalColumnDefSize}px` : '100%';
@@ -289,12 +299,9 @@ export function Table<TData extends object, TFilters extends FiltersState = Reco
289
299
  const currentSize = header.getSize();
290
300
  const colDefSize = header.column.columnDef.size;
291
301
 
292
- size = currentSize === colDefSize ? initSize : `${currentSize}px`;
293
-
294
- if (prevSize === '100%' && currentSize !== colDefSize) {
295
- const realSize = getCurrentlyConfiguredHeaderWidth(header.id);
296
- table.setColumnSizing(old => ({ ...old, [header.id]: realSize }));
297
-
302
+ if (currentSize !== colDefSize || (i < resizedColumnIndex && prevSize === '100%')) {
303
+ const realSize = prevSize === '100%' ? getCurrentlyConfiguredHeaderWidth(header.id) : currentSize;
304
+ realSizes[header.id] = realSize;
298
305
  size = `${realSize}px`;
299
306
  }
300
307
  }
@@ -303,12 +310,12 @@ export function Table<TData extends object, TFilters extends FiltersState = Reco
303
310
  saveStateToLocalStorage({ id: savedState.id, columnId: header.id, size });
304
311
  }
305
312
 
306
- colSizes[sizeKey] = size;
307
- colSizes[flexKey] = size === '100%' ? 'unset' : '0';
313
+ vars[sizeKey] = size;
314
+ vars[flexKey] = size === '100%' ? 'unset' : '0';
308
315
  }
309
316
  }
310
317
 
311
- return colSizes;
318
+ return { vars, realSizes };
312
319
  /*
313
320
  effect must be called only on columnSizingInfo.isResizingColumn changes
314
321
  to reduce unnecessary recalculations
@@ -321,8 +328,12 @@ export function Table<TData extends object, TFilters extends FiltersState = Reco
321
328
  }, [table.getState().columnSizingInfo.isResizingColumn, headers, table.getTotalSize()]);
322
329
 
323
330
  useEffect(() => {
324
- columnSizeVarsRef.current = columnSizeVars;
325
- }, [columnSizeVars]);
331
+ if (Object.keys(columnSizes.realSizes).length) {
332
+ table.setColumnSizing(old => ({ ...old, ...columnSizes.realSizes }));
333
+ }
334
+
335
+ columnSizeVarsRef.current = columnSizes.vars;
336
+ }, [columnSizes, table]);
326
337
 
327
338
  const tableRows = table.getRowModel().rows;
328
339
  const tableCenterRows = table.getCenterRows();
@@ -359,110 +370,133 @@ export function Table<TData extends object, TFilters extends FiltersState = Reco
359
370
  const showToolbar = !suppressToolbar;
360
371
 
361
372
  return (
362
- <CellAutoResizeContext.Provider value={{ updateCellMap }}>
363
- <div
364
- style={{
365
- '--page-size': cssPageSize,
366
- }}
367
- className={cn(styles.wrapper, className)}
368
- {...extractSupportProps(rest)}
369
- >
370
- {showToolbar && (
371
- <div className={styles.header}>
372
- <Toolbar
373
- search={
374
- suppressSearch
375
- ? undefined
376
- : {
377
- value: globalFilter,
378
- onChange: onGlobalFilterChange,
379
- loading: search?.loading,
380
- placeholder: search?.placeholder || t('searchPlaceholder'),
381
- }
382
- }
383
- className={styles.toolbar}
384
- onRefresh={onRefresh ? handleOnRefresh : undefined}
385
- bulkActions={bulkActions}
386
- selectionMode={rowSelectionProp?.multiRow ? 'multiple' : 'single'}
387
- checked={table.getIsAllPageRowsSelected()}
388
- indeterminate={table.getIsSomePageRowsSelected()}
389
- onCheck={enableSelection ? handleOnToolbarCheck : undefined}
390
- outline={outline}
391
- after={
392
- toolbarAfter || exportSettings ? (
393
- <>
394
- {toolbarAfter}
395
- {exportSettings && (
396
- <ExportButton
397
- settings={exportSettings}
398
- columnDefinitions={columnDefinitions}
399
- data={data}
400
- topRows={filteredTopRows}
401
- centerRows={centerRows}
373
+ <DndContext
374
+ collisionDetection={closestCenter}
375
+ modifiers={[restrictToHorizontalAxis]}
376
+ onDragEnd={handleDragEnd}
377
+ sensors={sensors}
378
+ >
379
+ <CellAutoResizeContext.Provider value={{ updateCellMap }}>
380
+ <div
381
+ style={{
382
+ '--page-size': cssPageSize,
383
+ }}
384
+ className={cn(styles.wrapper, className)}
385
+ {...extractSupportProps(rest)}
386
+ >
387
+ {showToolbar && (
388
+ <div className={styles.header}>
389
+ <Toolbar
390
+ search={
391
+ suppressSearch
392
+ ? undefined
393
+ : {
394
+ value: globalFilter,
395
+ onChange: onGlobalFilterChange,
396
+ loading: search?.loading,
397
+ placeholder: search?.placeholder || t('searchPlaceholder'),
398
+ }
399
+ }
400
+ className={styles.toolbar}
401
+ onRefresh={onRefresh ? handleOnRefresh : undefined}
402
+ bulkActions={bulkActions}
403
+ selectionMode={rowSelectionProp?.multiRow ? 'multiple' : 'single'}
404
+ checked={table.getIsAllPageRowsSelected()}
405
+ indeterminate={table.getIsSomePageRowsSelected()}
406
+ onCheck={enableSelection ? handleOnToolbarCheck : undefined}
407
+ outline={outline}
408
+ after={
409
+ toolbarAfter || exportSettings ? (
410
+ <>
411
+ {toolbarAfter}
412
+ {exportSettings && (
413
+ <ExportButton
414
+ settings={exportSettings}
415
+ columnDefinitions={columnDefinitions}
416
+ data={data}
417
+ topRows={filteredTopRows}
418
+ centerRows={centerRows}
419
+ />
420
+ )}
421
+ </>
422
+ ) : undefined
423
+ }
424
+ moreActions={moreActions}
425
+ filterRow={columnFilters}
426
+ data-test-id={TEST_IDS.toolbar}
427
+ />
428
+ </div>
429
+ )}
430
+
431
+ <div className={styles.scrollWrapper} data-outline={outline || undefined}>
432
+ <Scroll size='s' className={styles.table} ref={scrollContainerRef}>
433
+ <div className={styles.tableContent} style={columnSizes.vars}>
434
+ <TableContext.Provider value={{ table }}>
435
+ {loading ? (
436
+ <SkeletonContextProvider loading>
437
+ <HeaderRow groupedColumnOrderState={groupedColumnOrderState} />
438
+ {loadingTableRows.map(row => (
439
+ <BodyRow key={row.id} row={row} groupedColumnOrderState={groupedColumnOrderState} />
440
+ ))}
441
+ </SkeletonContextProvider>
442
+ ) : (
443
+ <>
444
+ {centerRows.length || filteredTopRows.length ? (
445
+ <HeaderRow
446
+ groupedColumnOrderState={groupedColumnOrderState}
447
+ enableColumnsOrderSortByDrag={enableColumnsOrderSortByDrag}
448
+ />
449
+ ) : null}
450
+
451
+ {filteredTopRows.length ? (
452
+ <div className={styles.topRowWrapper}>
453
+ {filteredTopRows.map(row => (
454
+ <BodyRow
455
+ key={row.id}
456
+ row={row}
457
+ onRowClick={onRowClick}
458
+ groupedColumnOrderState={groupedColumnOrderState}
459
+ />
460
+ ))}
461
+ </div>
462
+ ) : null}
463
+
464
+ {centerRows.map(row => (
465
+ <BodyRow
466
+ key={row.id}
467
+ row={row}
468
+ onRowClick={onRowClick}
469
+ groupedColumnOrderState={groupedColumnOrderState}
470
+ enableColumnsOrderSortByDrag={enableColumnsOrderSortByDrag}
471
+ />
472
+ ))}
473
+
474
+ <TableEmptyState
475
+ emptyStates={emptyStates}
476
+ dataError={dataError}
477
+ dataFiltered={dataFiltered || Boolean(table.getState().globalFilter)}
478
+ tableRowsLength={tableRows.length + filteredTopRows.length}
402
479
  />
403
- )}
404
- </>
405
- ) : undefined
406
- }
407
- moreActions={moreActions}
408
- filterRow={columnFilters}
409
- data-test-id={TEST_IDS.toolbar}
410
- />
480
+ </>
481
+ )}
482
+ </TableContext.Provider>
483
+ </div>
484
+ <div className={styles.scrollStub} ref={scrollRef as RefObject<HTMLDivElement>} />
485
+ </Scroll>
411
486
  </div>
412
- )}
413
-
414
- <div className={styles.scrollWrapper} data-outline={outline || undefined}>
415
- <Scroll size='s' className={styles.table} ref={scrollContainerRef}>
416
- <div className={styles.tableContent} style={columnSizeVars}>
417
- <TableContext.Provider value={{ table }}>
418
- {loading ? (
419
- <SkeletonContextProvider loading>
420
- <HeaderRow />
421
- {loadingTableRows.map(row => (
422
- <BodyRow key={row.id} row={row} />
423
- ))}
424
- </SkeletonContextProvider>
425
- ) : (
426
- <>
427
- {centerRows.length || filteredTopRows.length ? <HeaderRow /> : null}
428
-
429
- {filteredTopRows.length ? (
430
- <div className={styles.topRowWrapper}>
431
- {filteredTopRows.map(row => (
432
- <BodyRow key={row.id} row={row} onRowClick={onRowClick} />
433
- ))}
434
- </div>
435
- ) : null}
436
-
437
- {centerRows.map(row => (
438
- <BodyRow key={row.id} row={row} onRowClick={onRowClick} />
439
- ))}
440
-
441
- <TableEmptyState
442
- emptyStates={emptyStates}
443
- dataError={dataError}
444
- dataFiltered={dataFiltered || Boolean(table.getState().globalFilter)}
445
- tableRowsLength={tableRows.length + filteredTopRows.length}
446
- />
447
- </>
448
- )}
449
- </TableContext.Provider>
450
- </div>
451
- <div className={styles.scrollStub} ref={scrollRef as RefObject<HTMLDivElement>} />
452
- </Scroll>
453
- </div>
454
487
 
455
- {!suppressPagination && (
456
- <TablePagination
457
- table={table}
458
- options={paginationProp?.options}
459
- optionsLabel={paginationProp?.optionsLabel}
460
- pageCount={pageCount}
461
- optionsRender={paginationProp?.optionsRender}
462
- />
463
- )}
464
- </div>
465
- </CellAutoResizeContext.Provider>
488
+ {!suppressPagination && (
489
+ <TablePagination
490
+ table={table}
491
+ options={paginationProp?.options}
492
+ optionsLabel={paginationProp?.optionsLabel}
493
+ pageCount={pageCount}
494
+ optionsRender={paginationProp?.optionsRender}
495
+ />
496
+ )}
497
+ </div>
498
+ </CellAutoResizeContext.Provider>
499
+ </DndContext>
466
500
  );
467
501
  }
468
502
 
@@ -1,2 +1,3 @@
1
1
  export * from './useLoadingTable';
2
2
  export * from './useStateControl';
3
+ export * from './useColumnOrderByDrag';
@@ -0,0 +1,100 @@
1
+ import { DragEndEvent, KeyboardSensor, MouseSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core';
2
+ import { arrayMove } from '@dnd-kit/sortable';
3
+ import { useCallback, useMemo, useState } from 'react';
4
+
5
+ import { UNDRAGGABLE_COLUMNS } from '../../../constants';
6
+ import { ColumnDefinition, GroupedColumnOrderState } from '../../../types';
7
+
8
+ type PinnedColumnsMap = Record<string, keyof GroupedColumnOrderState>;
9
+
10
+ function preparePinnedColumnsMap<TData extends object>(tableColumns: ColumnDefinition<TData>[]): PinnedColumnsMap {
11
+ return tableColumns.reduce((accMap, columnDefinition) => {
12
+ switch (columnDefinition.pinned) {
13
+ case 'left':
14
+ accMap[columnDefinition.id] = 'leftPinned';
15
+ break;
16
+
17
+ case 'right':
18
+ accMap[columnDefinition.id] = 'rightPinned';
19
+ break;
20
+
21
+ default:
22
+ }
23
+
24
+ return accMap;
25
+ }, {} as PinnedColumnsMap);
26
+ }
27
+
28
+ function prepareInitialState<TData extends object>(tableColumns: ColumnDefinition<TData>[]) {
29
+ return tableColumns.map(c => c.id as string).filter(id => !UNDRAGGABLE_COLUMNS.includes(id));
30
+ }
31
+
32
+ function prepareGroupedColumnOrderState(
33
+ columnOrder: string[],
34
+ pinnedColumnsMap: PinnedColumnsMap,
35
+ ): GroupedColumnOrderState {
36
+ return columnOrder.reduce(
37
+ (accState: GroupedColumnOrderState, columnId) => {
38
+ if (UNDRAGGABLE_COLUMNS.includes(columnId)) {
39
+ return accState;
40
+ }
41
+
42
+ const groupName = pinnedColumnsMap[columnId] ?? 'unpinned';
43
+
44
+ accState[groupName].push(columnId);
45
+
46
+ return accState;
47
+ },
48
+ {
49
+ leftPinned: [],
50
+ unpinned: [],
51
+ rightPinned: [],
52
+ },
53
+ );
54
+ }
55
+
56
+ export function useColumnOrderByDrag<TData extends object>(tableColumns: ColumnDefinition<TData>[]) {
57
+ const [columnOrder, setColumnOrder] = useState<string[]>(() => prepareInitialState(tableColumns));
58
+
59
+ const pinnedColumnsMap = useMemo(() => preparePinnedColumnsMap(tableColumns), [tableColumns]);
60
+
61
+ const groupedColumnOrderState = useMemo(
62
+ () => prepareGroupedColumnOrderState(columnOrder, pinnedColumnsMap),
63
+ [columnOrder, pinnedColumnsMap],
64
+ );
65
+
66
+ const handleDragEnd = useCallback(
67
+ ({ active, over }: DragEndEvent) => {
68
+ if (active && over && active.id !== over.id) {
69
+ if (UNDRAGGABLE_COLUMNS.includes(over.id.toString())) {
70
+ return;
71
+ }
72
+
73
+ const activeGroup = pinnedColumnsMap[active.id.toString()];
74
+ const overGroup = pinnedColumnsMap[over.id.toString()];
75
+ if (activeGroup !== overGroup) {
76
+ return;
77
+ }
78
+
79
+ setColumnOrder(columnOrder => {
80
+ const oldIndex = columnOrder.indexOf(active.id.toString());
81
+ const newIndex = columnOrder.indexOf(over.id.toString());
82
+ return arrayMove(columnOrder, oldIndex, newIndex);
83
+ });
84
+ }
85
+ },
86
+ [pinnedColumnsMap],
87
+ );
88
+
89
+ const sensors = useSensors(useSensor(MouseSensor, {}), useSensor(TouchSensor, {}), useSensor(KeyboardSensor, {}));
90
+
91
+ return {
92
+ columnOrder,
93
+ setColumnOrder,
94
+
95
+ groupedColumnOrderState,
96
+
97
+ handleDragEnd,
98
+ sensors,
99
+ };
100
+ }
@@ -7,10 +7,15 @@ export function getCurrentlyConfiguredHeaderWidth(id: string): number {
7
7
  '[data-test-id="table__header-cell-resize-handle-moving-part"]',
8
8
  );
9
9
 
10
- if (cell && resizeHandler) {
10
+ if (cell) {
11
11
  const { width } = cell.getBoundingClientRect();
12
- const offset = parseInt(resizeHandler.style.getPropertyValue('--offset'));
13
- return width + offset;
12
+
13
+ if (resizeHandler) {
14
+ const offset = parseInt(resizeHandler.style.getPropertyValue('--offset'));
15
+ return width + offset;
16
+ }
17
+
18
+ return width;
14
19
  }
15
20
  }
16
21
 
@@ -45,6 +45,9 @@ export type TableProps<
45
45
  onChange?(state: SortingState): void;
46
46
  };
47
47
 
48
+ /** Включение сортировки порядка столбцов вручную перетаскиванием */
49
+ enableColumnsOrderSortByDrag?: boolean;
50
+
48
51
  /** Параметр отвечает за общие настройки раскрывающихся строк*/
49
52
  expanding?: {
50
53
  /** Метод отвечает за получение дочерних строк*/
package/src/constants.tsx CHANGED
@@ -39,3 +39,5 @@ export const SORT_FN = {
39
39
  } as const;
40
40
 
41
41
  export const DEFAULT_PAGE_SIZE = 10;
42
+
43
+ export const UNDRAGGABLE_COLUMNS = ['snack_predefined_statusColumn', 'selectionCell', 'rowActions'];
@@ -1,3 +1,4 @@
1
+ import { useSortable } from '@dnd-kit/sortable';
1
2
  import { Cell as TableCell, flexRender } from '@tanstack/react-table';
2
3
  import cn from 'classnames';
3
4
 
@@ -9,16 +10,22 @@ import styles from './styles.module.scss';
9
10
 
10
11
  type BodyCellProps<TData> = Omit<CellProps, 'style' | 'children'> & {
11
12
  cell: TableCell<TData, unknown>;
13
+ isDraggable?: boolean;
12
14
  };
13
15
 
14
- export function BodyCell<TData>({ cell, className, ...props }: BodyCellProps<TData>) {
16
+ export function BodyCell<TData>({ cell, className, isDraggable, ...props }: BodyCellProps<TData>) {
15
17
  const columnDef: ColumnDefinition<TData> = cell.column.columnDef;
16
18
 
17
- const style = useCellSizes(cell);
19
+ const style = useCellSizes(cell, { isDraggable });
20
+
21
+ const { setNodeRef } = useSortable({
22
+ id: cell.column.id,
23
+ });
18
24
 
19
25
  return (
20
26
  <Cell
21
27
  {...props}
28
+ ref={setNodeRef}
22
29
  style={style}
23
30
  className={cn(styles.tableBodyCell, className, columnDef.cellClassName)}
24
31
  data-align={columnDef.align}
@@ -0,0 +1,18 @@
1
+ import { useSortable } from '@dnd-kit/sortable';
2
+ import { CSSProperties } from 'react';
3
+
4
+ import { KebabSVG } from '@snack-uikit/icons';
5
+
6
+ import styles from './styles.module.scss';
7
+
8
+ export type DragHandleProps = Pick<ReturnType<typeof useSortable>, 'attributes' | 'listeners'> & {
9
+ style?: CSSProperties;
10
+ };
11
+
12
+ export function DragHandle({ attributes, listeners }: DragHandleProps) {
13
+ return (
14
+ <div className={styles.dragHandle} {...attributes} {...listeners}>
15
+ <KebabSVG className={styles.dragIcon} />
16
+ </div>
17
+ );
18
+ }
@@ -1,13 +1,15 @@
1
+ import { useSortable } from '@dnd-kit/sortable';
1
2
  import { flexRender, Header } from '@tanstack/react-table';
2
3
  import cn from 'classnames';
3
4
  import { MouseEvent, useRef } from 'react';
4
5
 
5
6
  import { TruncateString } from '@snack-uikit/truncate-string';
6
7
 
7
- import { TEST_IDS } from '../../../constants';
8
+ import { TEST_IDS, UNDRAGGABLE_COLUMNS } from '../../../constants';
8
9
  import { ColumnDefinition, ColumnPinPosition } from '../../../types';
9
10
  import { useCellSizes } from '../../hooks';
10
11
  import { Cell, CellProps } from '../Cell';
12
+ import { DragHandle } from './DragHandle';
11
13
  import { getSortingIcon } from './helpers';
12
14
  import { ResizeHandle } from './ResizeHandle';
13
15
  import styles from './styles.module.scss';
@@ -15,9 +17,10 @@ import styles from './styles.module.scss';
15
17
  type HeaderCellProps<TData> = Omit<CellProps, 'align' | 'children' | 'onClick' | 'style'> & {
16
18
  header: Header<TData, unknown>;
17
19
  pinPosition?: ColumnPinPosition;
20
+ isDraggable?: boolean;
18
21
  };
19
22
 
20
- export function HeaderCell<TData>({ header, pinPosition, className }: HeaderCellProps<TData>) {
23
+ export function HeaderCell<TData>({ header, pinPosition, isDraggable, className }: HeaderCellProps<TData>) {
21
24
  const cellRef = useRef<HTMLDivElement>(null);
22
25
  const isSortable = header.column.getCanSort();
23
26
  const isResizable = header.column.getCanResize();
@@ -31,7 +34,11 @@ export function HeaderCell<TData>({ header, pinPosition, className }: HeaderCell
31
34
 
32
35
  const columnDef: ColumnDefinition<TData> = header.column.columnDef;
33
36
 
34
- const style = useCellSizes(header);
37
+ const style = useCellSizes(header, { isDraggable });
38
+
39
+ const { attributes, listeners, setNodeRef } = useSortable({
40
+ id: header.column.id,
41
+ });
35
42
 
36
43
  const sortingHandler = (e: MouseEvent<HTMLDivElement>) => {
37
44
  if (isSomeColumnResizing) return;
@@ -39,11 +46,14 @@ export function HeaderCell<TData>({ header, pinPosition, className }: HeaderCell
39
46
  return header.column.getToggleSortingHandler()?.(e);
40
47
  };
41
48
 
49
+ const isAvailableForDrag = !UNDRAGGABLE_COLUMNS.includes(header.column.id);
50
+
42
51
  return (
43
52
  <Cell
44
53
  style={style}
45
54
  onClick={sortingHandler}
46
55
  data-sortable={isSortable || undefined}
56
+ data-draggable={isDraggable || undefined}
47
57
  data-no-padding={columnDef.noHeaderCellPadding || undefined}
48
58
  data-no-offset={columnDef.noHeaderCellBorderOffset || undefined}
49
59
  data-test-id={TEST_IDS.headerCell}
@@ -53,7 +63,10 @@ export function HeaderCell<TData>({ header, pinPosition, className }: HeaderCell
53
63
  data-pin-position={pinPosition || undefined}
54
64
  role='columnheader'
55
65
  className={cn(styles.tableHeaderCell, className, columnDef.headerClassName)}
56
- ref={cellRef}
66
+ ref={element => {
67
+ setNodeRef(element);
68
+ return cellRef;
69
+ }}
57
70
  >
58
71
  <div className={styles.tableHeaderCellMain}>
59
72
  {columnDef.header && (
@@ -73,6 +86,8 @@ export function HeaderCell<TData>({ header, pinPosition, className }: HeaderCell
73
86
  )}
74
87
  </div>
75
88
 
89
+ {isAvailableForDrag && Boolean(isDraggable) && <DragHandle attributes={attributes} listeners={listeners} />}
90
+
76
91
  {Boolean(isResizable) && <ResizeHandle header={header} cellRef={cellRef} />}
77
92
  </Cell>
78
93
  );
@@ -58,7 +58,7 @@
58
58
  left: 0;
59
59
  transform: translateX(-50%);
60
60
 
61
- width: calc(100% + styles-tokens-table.$dimension-4m);
61
+ width: calc(100% + #{styles-tokens-table.$dimension-4m});
62
62
  height: 100%;
63
63
  }
64
64
  }
@@ -86,7 +86,7 @@
86
86
  left: 50%;
87
87
  transform: translateX(-50%);
88
88
 
89
- width: calc(100% - styles-tokens-table.$space-table-head-separator-padding * 2);
89
+ width: calc(100% - #{styles-tokens-table.$space-table-head-separator-padding} * 2);
90
90
  height: styles-tokens-table.$border-width-table;
91
91
 
92
92
  background-color: styles-tokens-table.$sys-neutral-decor-default;
@@ -106,7 +106,7 @@
106
106
  &::after {
107
107
  left: 0;
108
108
  transform: none;
109
- width: calc(100% - styles-tokens-table.$space-table-head-separator-padding);
109
+ width: calc(100% - #{styles-tokens-table.$space-table-head-separator-padding});
110
110
  }
111
111
  }
112
112
 
@@ -136,7 +136,7 @@
136
136
  }
137
137
 
138
138
  .tableHeaderResizeIndicator {
139
- right: calc(styles-tokens-table.$dimension-1m / 2);
139
+ right: calc(#{styles-tokens-table.$dimension-1m} / 2);
140
140
  }
141
141
  }
142
142
  }
@@ -173,3 +173,14 @@
173
173
  height: styles-tokens-table.simple-var(styles-tokens-element.$icon-xs) !important; /* stylelint-disable-line declaration-no-important */
174
174
  }
175
175
  }
176
+
177
+ .dragHandle {
178
+ cursor: pointer;
179
+ display: flex;
180
+ flex-direction: row;
181
+ gap: 2px;
182
+ }
183
+
184
+ .dragIcon {
185
+ color: styles-tokens-table.simple-var(styles-tokens-table.$sys-neutral-text-light);
186
+ }