@snack-uikit/table 0.8.8 → 0.9.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 (34) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +1 -4
  3. package/dist/components/Table/Table.js +27 -1
  4. package/dist/components/TablePagination/TablePagination.js +1 -1
  5. package/dist/helperComponents/Cells/Cell.d.ts +10 -1
  6. package/dist/helperComponents/Cells/Cell.js +5 -4
  7. package/dist/helperComponents/Cells/HeaderCell/HeaderCell.js +14 -1
  8. package/dist/helperComponents/Cells/HeaderCell/ResizeHandle.d.ts +8 -0
  9. package/dist/helperComponents/Cells/HeaderCell/ResizeHandle.js +29 -0
  10. package/dist/helperComponents/Cells/HeaderCell/styles.module.css +65 -3
  11. package/dist/helperComponents/Cells/RowActionsCell/RowActionsCell.js +2 -1
  12. package/dist/helperComponents/Cells/SelectionCell/SelectionCell.js +1 -0
  13. package/dist/helperComponents/Cells/StatusCell/StatusCell.d.ts +4 -1
  14. package/dist/helperComponents/Cells/StatusCell/StatusCell.js +2 -1
  15. package/dist/helperComponents/Rows/HeaderRow.js +1 -1
  16. package/dist/helperComponents/Rows/Row.d.ts +1 -2
  17. package/dist/helperComponents/Rows/Row.js +4 -2
  18. package/dist/helperComponents/hooks.d.ts +2 -5
  19. package/dist/helperComponents/hooks.js +11 -18
  20. package/dist/types.d.ts +1 -1
  21. package/package.json +3 -3
  22. package/src/components/Table/Table.tsx +35 -1
  23. package/src/components/TablePagination/TablePagination.tsx +2 -0
  24. package/src/helperComponents/Cells/Cell.tsx +16 -6
  25. package/src/helperComponents/Cells/HeaderCell/HeaderCell.tsx +38 -16
  26. package/src/helperComponents/Cells/HeaderCell/ResizeHandle.tsx +63 -0
  27. package/src/helperComponents/Cells/HeaderCell/styles.module.scss +81 -2
  28. package/src/helperComponents/Cells/RowActionsCell/RowActionsCell.tsx +2 -1
  29. package/src/helperComponents/Cells/SelectionCell/SelectionCell.tsx +1 -0
  30. package/src/helperComponents/Cells/StatusCell/StatusCell.tsx +5 -0
  31. package/src/helperComponents/Rows/HeaderRow.tsx +1 -1
  32. package/src/helperComponents/Rows/Row.tsx +3 -3
  33. package/src/helperComponents/hooks.ts +16 -21
  34. package/src/types.ts +0 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,28 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 0.9.0 (2024-01-16)
7
+
8
+
9
+ ### Features
10
+
11
+ * **FF-4093:** table columns resizing ([8cae511](https://github.com/cloud-ru-tech/snack-uikit/commit/8cae5116f73f079ce3087099253390f2e79034fe))
12
+
13
+
14
+
15
+
16
+
17
+ ## 0.8.9 (2023-12-29)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * **FF-3960:** change child components size ([2ce2369](https://github.com/cloud-ru-tech/snack-uikit/commit/2ce2369d6f8c05f8688d61f8c53df4956196b52c))
23
+
24
+
25
+
26
+
27
+
6
28
  ## 0.8.8 (2023-12-28)
7
29
 
8
30
  ### Only dependencies have been changed
package/README.md CHANGED
@@ -6,11 +6,7 @@
6
6
  [Changelog](./CHANGELOG.md)
7
7
 
8
8
  ## TODO:
9
- - resize columns
10
- - export data
11
- - loading state
12
9
  - multiple row selection with Shift key pressed
13
- - try to make the table independent of height
14
10
 
15
11
 
16
12
  ## Примечание
@@ -139,6 +135,7 @@ const columnDefinitions: ColumnDefinition<TableData>[] = [
139
135
  | renderDescription | `(cellValue: string) => string` | - | Функция для отрисовки текста, если не передана, то будет отрисован только индикатор статуса |
140
136
  | size | `number` | - | Размер ячейки |
141
137
  | header | `ColumnDefTemplate<HeaderContext<TData, unknown>> & (string \| ((ctx: HeaderContext<TData, unknown>) => string))` | - | Заголовок колонки |
138
+ | enableResizing | `boolean` | - | Включение/выключение ресайза колонки |
142
139
  ## Table.getRowActionsColumnDef
143
140
  Вспомогательная функция для создания ячейки с дополнительными действиями у строки
144
141
  ### Props
@@ -70,6 +70,8 @@ export function Table(_a) {
70
70
  pageCount,
71
71
  defaultColumn: {
72
72
  enableSorting: false,
73
+ enableResizing: false,
74
+ minSize: 40,
73
75
  },
74
76
  globalFilterFn: fuzzyFilter,
75
77
  onGlobalFilterChange,
@@ -78,6 +80,7 @@ export function Table(_a) {
78
80
  enableMultiRowSelection: rowSelectionProp === null || rowSelectionProp === void 0 ? void 0 : rowSelectionProp.multiRow,
79
81
  enableFilters: true,
80
82
  getFilteredRowModel: getFilteredRowModel(),
83
+ enableColumnResizing: true,
81
84
  enableSorting: true,
82
85
  manualSorting: false,
83
86
  enableMultiSort: false,
@@ -111,10 +114,33 @@ export function Table(_a) {
111
114
  return;
112
115
  }
113
116
  }, [loading, rowSelectionProp === null || rowSelectionProp === void 0 ? void 0 : rowSelectionProp.multiRow, table]);
117
+ const columnSizeVars = useMemo(() => {
118
+ const originalColumnDefs = table._getColumnDefs();
119
+ const headers = table.getFlatHeaders();
120
+ const colSizes = {};
121
+ for (let i = 0; i < headers.length; i++) {
122
+ const header = headers[i];
123
+ const originalColDef = originalColumnDefs.find(col => getColumnId(header) === col.id);
124
+ const originalColumnDefSize = originalColDef === null || originalColDef === void 0 ? void 0 : originalColDef.size;
125
+ const initSize = originalColumnDefSize ? `${originalColumnDefSize}px` : '100%';
126
+ let size = initSize;
127
+ if (header.column.getCanResize()) {
128
+ const currentSize = header.getSize();
129
+ const colDefSize = header.column.columnDef.size;
130
+ size = currentSize === colDefSize ? initSize : `${currentSize}px`;
131
+ }
132
+ colSizes[`--table-column-${header.id}-size`] = size;
133
+ colSizes[`--table-column-${header.id}-flex`] = size === '100%' ? 'unset' : '0';
134
+ }
135
+ return colSizes;
136
+ /* effect must be called only on columnSizingInfo.isResizingColumn changes
137
+ to reduce unnecessary recalculations */
138
+ // eslint-disable-next-line react-hooks/exhaustive-deps
139
+ }, [table.getState().columnSizingInfo.isResizingColumn]);
114
140
  const tableRows = table.getRowModel().rows;
115
141
  const loadingTableRows = loadingTable.getRowModel().rows;
116
142
  const tablePagination = table.getState().pagination;
117
- return (_jsx(_Fragment, { children: _jsxs("div", Object.assign({ style: { '--page-size': !suppressPagination ? tablePagination === null || tablePagination === void 0 ? void 0 : tablePagination.pageSize : pageSize }, className: cn(styles.wrapper, className) }, extractSupportProps(rest), { children: [!suppressToolbar && (_jsxs("div", { className: styles.header, children: [_jsx(Toolbar, { value: globalFilter, onChange: onGlobalFilterChange, checked: table.getIsAllPageRowsSelected(), indeterminate: table.getIsSomePageRowsSelected(), className: styles.toolbar, onRefresh: onRefresh ? handleOnRefresh : undefined, onDelete: enableSelection && onDelete ? handleOnDelete : undefined, onCheck: enableSelection ? handleOnCheck : undefined, outline: outline, loading: search === null || search === void 0 ? void 0 : search.loading, placeholder: (search === null || search === void 0 ? void 0 : search.placeholder) || 'Search', selectionMode: (rowSelectionProp === null || rowSelectionProp === void 0 ? void 0 : rowSelectionProp.multiRow) ? 'multiple' : 'single', actions: exportFileName ? (_jsx(ExportButton, { fileName: exportFileName, columnDefinitions: columnDefinitions, data: data })) : undefined, moreActions: moreActions }), columnFiltersProp && _jsxs("div", { className: styles.filtersWrapper, children: [" ", columnFiltersProp, " "] })] })), _jsx("div", { className: styles.scrollWrapper, "data-outline": outline || undefined, children: _jsx(Scroll, { size: 's', className: styles.table, children: _jsx("div", { className: styles.tableContent, children: _jsx(TableContext.Provider, { value: { table }, children: loading ? (_jsxs(SkeletonContextProvider, { loading: true, children: [_jsx(HeaderRow, {}), loadingTableRows.map(row => (_jsx(BodyRow, { row: row }, row.id)))] })) : (_jsxs(_Fragment, { children: [tableRows.length ? _jsx(HeaderRow, {}) : null, tableRows.map(row => (_jsx(BodyRow, { row: row, onRowClick: onRowClick }, row.id))), !tableRows.length && globalFilter && _jsx(TableEmptyState, Object.assign({}, noResultsState)), !tableRows.length && !globalFilter && _jsx(TableEmptyState, Object.assign({}, noDataState))] })) }) }) }) }), !suppressPagination && (_jsx(TablePagination, { table: table, options: paginationProp === null || paginationProp === void 0 ? void 0 : paginationProp.options, optionsLabel: paginationProp === null || paginationProp === void 0 ? void 0 : paginationProp.optionsLabel, pageCount: pageCount }))] })) }));
143
+ return (_jsx(_Fragment, { children: _jsxs("div", Object.assign({ style: { '--page-size': !suppressPagination ? tablePagination === null || tablePagination === void 0 ? void 0 : tablePagination.pageSize : pageSize }, className: cn(styles.wrapper, className) }, extractSupportProps(rest), { children: [!suppressToolbar && (_jsxs("div", { className: styles.header, children: [_jsx(Toolbar, { value: globalFilter, onChange: onGlobalFilterChange, checked: table.getIsAllPageRowsSelected(), indeterminate: table.getIsSomePageRowsSelected(), className: styles.toolbar, onRefresh: onRefresh ? handleOnRefresh : undefined, onDelete: enableSelection && onDelete ? handleOnDelete : undefined, onCheck: enableSelection ? handleOnCheck : undefined, outline: outline, loading: search === null || search === void 0 ? void 0 : search.loading, placeholder: (search === null || search === void 0 ? void 0 : search.placeholder) || 'Search', selectionMode: (rowSelectionProp === null || rowSelectionProp === void 0 ? void 0 : rowSelectionProp.multiRow) ? 'multiple' : 'single', actions: exportFileName ? (_jsx(ExportButton, { fileName: exportFileName, columnDefinitions: columnDefinitions, data: data })) : undefined, moreActions: moreActions }), columnFiltersProp && _jsxs("div", { className: styles.filtersWrapper, children: [" ", columnFiltersProp, " "] })] })), _jsx("div", { className: styles.scrollWrapper, "data-outline": outline || undefined, children: _jsx(Scroll, { size: 's', className: styles.table, children: _jsx("div", { className: styles.tableContent, style: columnSizeVars, children: _jsx(TableContext.Provider, { value: { table }, children: loading ? (_jsxs(SkeletonContextProvider, { loading: true, children: [_jsx(HeaderRow, {}), loadingTableRows.map(row => (_jsx(BodyRow, { row: row }, row.id)))] })) : (_jsxs(_Fragment, { children: [tableRows.length ? _jsx(HeaderRow, {}) : null, tableRows.map(row => (_jsx(BodyRow, { row: row, onRowClick: onRowClick }, row.id))), !tableRows.length && globalFilter && _jsx(TableEmptyState, Object.assign({}, noResultsState)), !tableRows.length && !globalFilter && _jsx(TableEmptyState, Object.assign({}, noDataState))] })) }) }) }) }), !suppressPagination && (_jsx(TablePagination, { table: table, options: paginationProp === null || paginationProp === void 0 ? void 0 : paginationProp.options, optionsLabel: paginationProp === null || paginationProp === void 0 ? void 0 : paginationProp.optionsLabel, pageCount: pageCount }))] })) }));
118
144
  }
119
145
  Table.getStatusColumnDef = getStatusColumnDef;
120
146
  Table.statusAppearances = STATUS_APPEARANCE;
@@ -15,5 +15,5 @@ export function TablePagination({ table, options: optionsProp, optionsLabel = 'R
15
15
  if (table.getPageCount() <= 1 && !options) {
16
16
  return null;
17
17
  }
18
- return (_jsxs("div", { className: styles.footer, children: [table.getPageCount() > 1 && (_jsx(Pagination, { total: table.getPageCount(), page: tablePaginationState.pageIndex + 1, onChange: handlePaginationOnChange, className: styles.pagination })), options && table.getRowModel().rows.length >= Number(options[0].value) && (_jsx(ChipChoice.Single, { value: String(tablePaginationState.pageSize), onChange: handleRowsVolumeOnChange, placement: 'top-end', options: options, label: optionsLabel, widthStrategy: 'auto', showClearButton: false }))] }));
18
+ return (_jsxs("div", { className: styles.footer, children: [table.getPageCount() > 1 && (_jsx(Pagination, { total: table.getPageCount(), page: tablePaginationState.pageIndex + 1, onChange: handlePaginationOnChange, size: 'xs', className: styles.pagination })), options && table.getRowModel().rows.length >= Number(options[0].value) && (_jsx(ChipChoice.Single, { value: String(tablePaginationState.pageSize), onChange: handleRowsVolumeOnChange, placement: 'top-end', options: options, label: optionsLabel, widthStrategy: 'auto', showClearButton: false, size: 'xs' }))] }));
19
19
  }
@@ -3,7 +3,16 @@ import { DataAttributes } from '../types';
3
3
  export type CellProps = {
4
4
  children: ReactNode;
5
5
  onClick?: MouseEventHandler;
6
+ onMouseUp?: MouseEventHandler;
6
7
  className?: string;
7
8
  style?: CSSProperties;
9
+ role?: 'cell' | 'columnheader';
8
10
  } & DataAttributes;
9
- export declare function Cell({ onClick, className, style, children, ...attributes }: CellProps): import("react/jsx-runtime").JSX.Element;
11
+ export declare const Cell: import("react").ForwardRefExoticComponent<{
12
+ children: ReactNode;
13
+ onClick?: MouseEventHandler | undefined;
14
+ onMouseUp?: MouseEventHandler | undefined;
15
+ className?: string | undefined;
16
+ style?: CSSProperties | undefined;
17
+ role?: "cell" | "columnheader" | undefined;
18
+ } & DataAttributes & import("react").RefAttributes<HTMLDivElement>>;
@@ -11,10 +11,11 @@ var __rest = (this && this.__rest) || function (s, e) {
11
11
  };
12
12
  import { jsx as _jsx } from "react/jsx-runtime";
13
13
  import cn from 'classnames';
14
+ import { forwardRef } from 'react';
14
15
  import styles from './styles.module.css';
15
- export function Cell(_a) {
16
- var { onClick, className, style, children } = _a, attributes = __rest(_a, ["onClick", "className", "style", "children"]);
16
+ export const Cell = forwardRef((_a, ref) => {
17
+ var { onClick, onMouseUp, className, style, children, role = 'cell' } = _a, attributes = __rest(_a, ["onClick", "onMouseUp", "className", "style", "children", "role"]);
17
18
  return (
18
19
  // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
19
- _jsx("div", Object.assign({ role: 'cell', onClick: onClick, className: cn(styles.tableCell, className), style: style }, attributes, { children: children })));
20
- }
20
+ _jsx("div", Object.assign({ role: role, onClick: onClick, onMouseUp: onMouseUp, className: cn(styles.tableCell, className), style: style, ref: ref }, attributes, { children: children })));
21
+ });
@@ -1,17 +1,30 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { flexRender } from '@tanstack/react-table';
3
3
  import cn from 'classnames';
4
+ import { useRef } from 'react';
4
5
  import { TruncateString } from '@snack-uikit/truncate-string';
5
6
  import { TEST_IDS } from '../../../constants';
6
7
  import { useCellSizes } from '../../hooks';
7
8
  import { Cell } from '../Cell';
8
9
  import { getSortingIcon } from './helpers';
10
+ import { ResizeHandle } from './ResizeHandle';
9
11
  import styles from './styles.module.css';
10
12
  export function HeaderCell({ header, className }) {
13
+ const cellRef = useRef(null);
11
14
  const isSortable = header.column.getCanSort();
15
+ const isResizable = header.column.getCanResize();
16
+ const isResizing = isResizable && header.column.getIsResizing();
12
17
  const sortDirection = isSortable && (header.column.getIsSorted() || undefined);
13
18
  const sortIcon = getSortingIcon(sortDirection);
19
+ const columnSizingInfo = header.getContext().table.getState().columnSizingInfo;
20
+ const isSomeColumnResizing = columnSizingInfo.isResizingColumn;
14
21
  const columnDef = header.column.columnDef;
15
22
  const style = useCellSizes(header);
16
- return (_jsxs(Cell, { style: style, onClick: header.column.getToggleSortingHandler(), "data-sortable": isSortable || undefined, "data-no-padding": columnDef.noHeaderCellPadding || undefined, "data-no-offset": columnDef.noHeaderCellBorderOffset || undefined, "data-test-id": TEST_IDS.headerCell, className: cn(styles.tableHeaderCell, className, columnDef.headerClassName), children: [columnDef.header && (_jsx("div", { className: styles.tableHeaderCellName, children: _jsx(TruncateString, { text: flexRender(columnDef.header, header.getContext()) }) })), Boolean(sortIcon) && (_jsx("div", { className: styles.tableHeaderSortIcon, "data-sort-direction": sortDirection, "data-test-id": TEST_IDS.headerSortIndicator, children: sortIcon }))] }));
23
+ const sortingHandler = (e) => {
24
+ var _a;
25
+ if (isSomeColumnResizing)
26
+ return;
27
+ return (_a = header.column.getToggleSortingHandler()) === null || _a === void 0 ? void 0 : _a(e);
28
+ };
29
+ return (_jsxs(Cell, { style: style, onMouseUp: sortingHandler, "data-sortable": isSortable || undefined, "data-no-padding": columnDef.noHeaderCellPadding || undefined, "data-no-offset": columnDef.noHeaderCellBorderOffset || undefined, "data-test-id": TEST_IDS.headerCell, "data-resizing": isResizing || undefined, role: 'columnheader', className: cn(styles.tableHeaderCell, className, columnDef.headerClassName), ref: cellRef, children: [_jsxs("div", { className: styles.tableHeaderCellMain, children: [columnDef.header && (_jsx("div", { className: styles.tableHeaderCellName, children: _jsx(TruncateString, { text: flexRender(columnDef.header, header.getContext()) }) })), Boolean(sortIcon) && (_jsx("div", { className: styles.tableHeaderIcon, "data-sort-direction": sortDirection, "data-test-id": TEST_IDS.headerSortIndicator, children: sortIcon }))] }), Boolean(isResizable) && _jsx(ResizeHandle, { header: header, cellRef: cellRef })] }));
17
30
  }
@@ -0,0 +1,8 @@
1
+ import { Header } from '@tanstack/react-table';
2
+ import { RefObject } from 'react';
3
+ type ResizeHandleProps<TData> = {
4
+ header: Header<TData, unknown>;
5
+ cellRef: RefObject<HTMLDivElement>;
6
+ };
7
+ export declare function ResizeHandle<TData>({ header, cellRef }: ResizeHandleProps<TData>): import("react/jsx-runtime").JSX.Element;
8
+ export {};
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import cn from 'classnames';
3
+ import styles from './styles.module.css';
4
+ function getResizeIndicatorOffset({ header, cellRef }) {
5
+ var _a;
6
+ const columnSizingInfo = header.getContext().table.getState().columnSizingInfo;
7
+ const { minSize, maxSize } = header.column.columnDef;
8
+ const { startSize, deltaOffset } = columnSizingInfo;
9
+ let offset = 0;
10
+ if (startSize !== null && deltaOffset !== null) {
11
+ const divElementSize = ((_a = cellRef.current) === null || _a === void 0 ? void 0 : _a.offsetWidth) || 0;
12
+ const initSize = Math.max(startSize, divElementSize);
13
+ const limit = deltaOffset < 0 ? minSize : maxSize;
14
+ let deltaLimit = 0;
15
+ if (limit !== undefined && deltaOffset !== 0) {
16
+ deltaLimit = deltaOffset < 0 ? -(initSize - limit) : limit - initSize;
17
+ offset = deltaOffset < 0 ? Math.max(deltaOffset, deltaLimit) : Math.min(Math.abs(deltaOffset), deltaLimit);
18
+ }
19
+ }
20
+ return offset;
21
+ }
22
+ export function ResizeHandle({ header, cellRef }) {
23
+ const isResizing = header.column.getIsResizing();
24
+ const resizeHandler = header.getResizeHandler();
25
+ const offset = isResizing ? getResizeIndicatorOffset({ header, cellRef }) : 0;
26
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: cn(styles.tableHeaderIcon, styles.tableHeaderResizeHandle), "data-resizing": isResizing || undefined, onMouseDown: resizeHandler, onTouchStart: resizeHandler }), isResizing && (_jsx("div", { className: styles.tableHeaderResizeIndicator, style: {
27
+ transform: `translateX(${offset}px)`,
28
+ } }))] }));
29
+ }
@@ -1,8 +1,34 @@
1
+ .tableHeaderResizeHandle{
2
+ cursor:ew-resize;
3
+ position:absolute;
4
+ z-index:1;
5
+ top:0;
6
+ right:0;
7
+ transform:translateX(50%);
8
+ width:var(--dimension-1m, 8px);
9
+ height:100%;
10
+ opacity:0;
11
+ }
12
+ .tableHeaderResizeHandle::after{
13
+ content:"";
14
+ position:absolute;
15
+ top:0;
16
+ left:50%;
17
+ transform:translateX(-50%);
18
+ width:var(--border-width-table, 1px);
19
+ height:100%;
20
+ background-color:var(--sys-neutral-decor-hovered, #d2d2d2);
21
+ }
22
+ .tableHeaderResizeHandle[data-resizing]{
23
+ opacity:0;
24
+ }
25
+
1
26
  .tableHeaderCell{
2
27
  padding:var(--space-table-cell-padding, 8px);
3
28
  position:relative;
4
29
  display:flex;
5
30
  align-items:center;
31
+ justify-content:space-between;
6
32
  box-sizing:border-box;
7
33
  width:100%;
8
34
  background-color:inherit;
@@ -15,9 +41,12 @@
15
41
  left:50%;
16
42
  transform:translateX(-50%);
17
43
  width:calc(100% - var(--space-table-head-separator-padding, 8px) * 2);
18
- height:1px;
44
+ height:var(--border-width-table, 1px);
19
45
  background-color:var(--sys-neutral-decor-default, #dedede);
20
46
  }
47
+ .tableHeaderCell:hover .tableHeaderResizeHandle:not([data-resizing]){
48
+ opacity:1;
49
+ }
21
50
  .tableHeaderCell[data-sortable]{
22
51
  cursor:pointer;
23
52
  }
@@ -32,6 +61,19 @@
32
61
  .tableHeaderCell[data-no-padding]::after{
33
62
  width:100%;
34
63
  }
64
+ .tableHeaderCell[data-resizing]{
65
+ -webkit-user-select:none;
66
+ -moz-user-select:none;
67
+ user-select:none;
68
+ }
69
+
70
+ .tableHeaderCellMain{
71
+ overflow:auto;
72
+ display:flex;
73
+ align-items:center;
74
+ box-sizing:border-box;
75
+ min-width:0;
76
+ }
35
77
 
36
78
  .tableHeaderCellName{
37
79
  height:var(--size-table-head-name-layout, 24px);
@@ -42,12 +84,32 @@
42
84
  color:var(--sys-neutral-text-light, #898989);
43
85
  }
44
86
 
45
- .tableHeaderSortIcon{
87
+ .tableHeaderIcon{
46
88
  display:flex;
47
89
  box-sizing:border-box;
48
90
  color:var(--sys-neutral-text-light, #898989);
49
91
  }
50
- .tableHeaderSortIcon svg{
92
+ .tableHeaderIcon svg{
51
93
  width:var(--dimension-2m, 16px) !important;
52
94
  height:var(--dimension-2m, 16px) !important;
95
+ }
96
+
97
+ .tableHeaderResizeIndicator{
98
+ cursor:col-resize;
99
+ position:absolute;
100
+ z-index:2;
101
+ top:0;
102
+ right:0;
103
+ width:1px;
104
+ height:100%;
105
+ background-color:var(--sys-neutral-decor-activated, #b8b8b8);
106
+ }
107
+ .tableHeaderResizeIndicator::after{
108
+ content:"";
109
+ position:absolute;
110
+ top:0;
111
+ left:0;
112
+ transform:translateX(-50%);
113
+ width:calc(100% + var(--dimension-4m, 32px));
114
+ height:100%;
53
115
  }
@@ -20,7 +20,7 @@ function RowActionsCell({ row, actions }) {
20
20
  const visibleActions = useMemo(() => actions.filter(item => !(item === null || item === void 0 ? void 0 : item.hidden)), [actions]);
21
21
  return (
22
22
  // eslint-disable-next-line jsx-a11y/no-static-element-interactions
23
- _jsx("div", { onClick: stopPropagationClick, className: styles.rowActionsCellWrap, "data-open": droplistOpened || undefined, children: !disabled && Boolean(visibleActions.length) && (_jsx(Droplist, { open: droplistOpened, onOpenChange: setDroplistOpen, placement: 'bottom-end', firstElementRefCallback: firstElementRefCallback, onFocusLeave: handleDroplistFocusLeave, triggerElement: _jsx("span", { children: _jsx(ButtonFunction, { icon: _jsx(MoreSVG, { size: 24 }), "data-test-id": TEST_IDS.rowActions.droplistTrigger, onKeyDown: handleTriggerKeyDown, ref: triggerElementRef }) }), triggerClassName: styles.rowActionsCellTrigger, size: 's', "data-test-id": TEST_IDS.rowActions.droplist, children: actions.map(item => (_createElement(Droplist.ItemSingle, Object.assign({}, item, { key: `${row.id}-${item.id || item.option}`, onClick: e => handleDroplistItemClick(e, handleItemClick(item)), "data-test-id": TEST_IDS.rowActions.option, onKeyDown: handleDroplistItemKeyDown })))) })) }));
23
+ _jsx("div", { onClick: stopPropagationClick, className: styles.rowActionsCellWrap, "data-open": droplistOpened || undefined, children: !disabled && Boolean(visibleActions.length) && (_jsx(Droplist, { open: droplistOpened, onOpenChange: setDroplistOpen, placement: 'bottom-end', firstElementRefCallback: firstElementRefCallback, onFocusLeave: handleDroplistFocusLeave, triggerElement: _jsx("span", { children: _jsx(ButtonFunction, { icon: _jsx(MoreSVG, { size: 24 }), "data-test-id": TEST_IDS.rowActions.droplistTrigger, onKeyDown: handleTriggerKeyDown, ref: triggerElementRef }) }), triggerClassName: styles.rowActionsCellTrigger, size: 'm', "data-test-id": TEST_IDS.rowActions.droplist, children: actions.map(item => (_createElement(Droplist.ItemSingle, Object.assign({}, item, { key: `${row.id}-${item.id || item.option}`, onClick: e => handleDroplistItemClick(e, handleItemClick(item)), "data-test-id": TEST_IDS.rowActions.option, onKeyDown: handleDroplistItemKeyDown })))) })) }));
24
24
  }
25
25
  /** Вспомогательная функция для создания ячейки с дополнительными действиями у строки */
26
26
  export function getRowActionsColumnDef({ actionsGenerator, pinned, }) {
@@ -33,6 +33,7 @@ export function getRowActionsColumnDef({ actionsGenerator, pinned, }) {
33
33
  },
34
34
  noBodyCellPadding: true,
35
35
  cellClassName: styles.rowActionsCell,
36
+ enableResizing: false,
36
37
  cell: cell => _jsx(RowActionsCell, { row: cell.row, actions: actionsGenerator(cell) }),
37
38
  };
38
39
  }
@@ -30,6 +30,7 @@ export function getSelectionCellColumnDef() {
30
30
  noBodyCellPadding: true,
31
31
  size: 40,
32
32
  headerClassName: styles.selectionCellHeader,
33
+ enableResizing: false,
33
34
  cell: ({ row, table }) => {
34
35
  const disabled = !row.getCanSelect();
35
36
  if (disabled)
@@ -15,6 +15,7 @@ type StatusColumnDef = BaseStatusColumnDef & {
15
15
  renderDescription?: never;
16
16
  size?: never;
17
17
  header?: never;
18
+ enableResizing?: never;
18
19
  };
19
20
  type StatusColumnDefWithDescription<TData> = BaseStatusColumnDef & {
20
21
  /** Функция для отрисовки текста, если не передана, то будет отрисован только индикатор статуса */
@@ -23,7 +24,9 @@ type StatusColumnDefWithDescription<TData> = BaseStatusColumnDef & {
23
24
  size: number;
24
25
  /** Заголовок колонки */
25
26
  header?: ColumnDefinition<TData>['header'];
27
+ /** Включение/выключение ресайза колонки */
28
+ enableResizing?: boolean;
26
29
  };
27
30
  export type StatusColumnDefinitionProps<TData> = StatusColumnDef | StatusColumnDefWithDescription<TData>;
28
31
  /** Вспомогательная функция для создания ячейки со статусом */
29
- export declare function getStatusColumnDef<TData>({ header, accessorKey, mapStatusToAppearance, renderDescription, size, enableSorting, }: StatusColumnDefinitionProps<TData>): ColumnDefinition<TData>;
32
+ export declare function getStatusColumnDef<TData>({ header, accessorKey, mapStatusToAppearance, renderDescription, size, enableSorting, enableResizing, }: StatusColumnDefinitionProps<TData>): ColumnDefinition<TData>;
@@ -10,7 +10,7 @@ function StatusCell({ appearance, label }) {
10
10
  return (_jsxs("div", { className: styles.statusCell, "data-no-label": !label || undefined, children: [_jsx("div", { "data-appearance": isLoading ? undefined : appearance, className: styles.statusCellIndicator, "data-loading": isLoading || undefined, "data-test-id": TEST_IDS.statusIndicator }), label && (_jsx("div", { className: styles.statusCellLabel, "data-test-id": TEST_IDS.statusLabel, children: _jsx(Typography.LightLabelS, { children: _jsx(TruncateString, { text: label }) }) }))] }));
11
11
  }
12
12
  /** Вспомогательная функция для создания ячейки со статусом */
13
- export function getStatusColumnDef({ header, accessorKey, mapStatusToAppearance, renderDescription, size, enableSorting, }) {
13
+ export function getStatusColumnDef({ header, accessorKey, mapStatusToAppearance, renderDescription, size, enableSorting, enableResizing, }) {
14
14
  const hasDescription = Boolean(renderDescription);
15
15
  return {
16
16
  id: 'snack_predefined_statusColumn',
@@ -26,6 +26,7 @@ export function getStatusColumnDef({ header, accessorKey, mapStatusToAppearance,
26
26
  accessorKey,
27
27
  enableSorting,
28
28
  header: hasDescription ? header : undefined,
29
+ enableResizing: enableResizing !== null && enableResizing !== void 0 ? enableResizing : hasDescription,
29
30
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
31
  accessorFn: (row) => renderDescription && Object.hasOwn(row, accessorKey)
31
32
  ? renderDescription(row[accessorKey])
@@ -7,5 +7,5 @@ import { Row } from './Row';
7
7
  import styles from './styles.module.css';
8
8
  export function HeaderRow() {
9
9
  const { leftPinned, unpinned, rightPinned } = useHeaderGroups();
10
- return (_jsxs(Row, { className: styles.tableHeader, "data-test-id": TEST_IDS.headerRow, role: 'rowheader', children: [leftPinned && (_jsx(PinnedCells, { position: COLUMN_PIN_POSITION.Left, children: leftPinned.map(headerGroup => headerGroup.headers.map(header => header.isPlaceholder ? null : _jsx(HeaderCell, { header: header }, header.id))) })), unpinned.map(headerGroup => headerGroup.headers.map(header => _jsx(HeaderCell, { header: header }, header.id))), rightPinned && (_jsx(PinnedCells, { position: COLUMN_PIN_POSITION.Right, children: rightPinned.map(headerGroup => headerGroup.headers.map(header => header.isPlaceholder ? null : _jsx(HeaderCell, { header: header }, header.id))) }))] }));
10
+ return (_jsxs(Row, { className: styles.tableHeader, "data-test-id": TEST_IDS.headerRow, children: [leftPinned && (_jsx(PinnedCells, { position: COLUMN_PIN_POSITION.Left, children: leftPinned.map(headerGroup => headerGroup.headers.map(header => header.isPlaceholder ? null : _jsx(HeaderCell, { header: header }, header.id))) })), unpinned.map(headerGroup => headerGroup.headers.map(header => _jsx(HeaderCell, { header: header }, header.id))), rightPinned && (_jsx(PinnedCells, { position: COLUMN_PIN_POSITION.Right, children: rightPinned.map(headerGroup => headerGroup.headers.map(header => header.isPlaceholder ? null : _jsx(HeaderCell, { header: header }, header.id))) }))] }));
11
11
  }
@@ -4,7 +4,6 @@ type RowProps = {
4
4
  children: ReactNode;
5
5
  onClick?(e: MouseEvent<HTMLDivElement>): void;
6
6
  className?: string;
7
- role?: 'rowheader' | 'row';
8
7
  } & DataAttributes;
9
- export declare function Row({ onClick, role, children, className, ...attributes }: RowProps): import("react/jsx-runtime").JSX.Element;
8
+ export declare function Row({ onClick, children, className, ...attributes }: RowProps): import("react/jsx-runtime").JSX.Element;
10
9
  export {};
@@ -13,6 +13,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
13
13
  import cn from 'classnames';
14
14
  import styles from './styles.module.css';
15
15
  export function Row(_a) {
16
- var { onClick, role = 'row', children, className } = _a, attributes = __rest(_a, ["onClick", "role", "children", "className"]);
17
- return (_jsx("div", Object.assign({ onClick: onClick, className: cn(styles.tableRow, className), role: role }, attributes, { children: children })));
16
+ var { onClick, children, className } = _a, attributes = __rest(_a, ["onClick", "children", "className"]);
17
+ return (
18
+ // eslint-disable-next-line jsx-a11y/interactive-supports-focus
19
+ _jsx("div", Object.assign({ onClick: onClick, className: cn(styles.tableRow, className), role: 'row' }, attributes, { children: children })));
18
20
  }
@@ -18,11 +18,8 @@ export declare function useRowCells<TData>(row: Row<TData>): {
18
18
  unpinned: Cell<TData, unknown>[];
19
19
  };
20
20
  export declare function useCellSizes<TData>(element: Cell<TData, unknown> | Header<TData, unknown>): {
21
- width: number;
22
- minWidth?: undefined;
23
- maxWidth?: undefined;
24
- } | {
25
21
  minWidth: number | undefined;
26
- width: number | undefined;
22
+ width: string;
27
23
  maxWidth: number | undefined;
24
+ flexShrink: string;
28
25
  };
@@ -1,6 +1,5 @@
1
1
  import { useMemo } from 'react';
2
2
  import { useTableContext } from './contexts';
3
- import { getColumnId } from './helpers';
4
3
  function hasHeaders(groups) {
5
4
  return groups.some(group => group.headers.length);
6
5
  }
@@ -47,21 +46,15 @@ export function useRowCells(row) {
47
46
  }, [row, pinEnabled, columnDefs]);
48
47
  }
49
48
  export function useCellSizes(element) {
50
- return useMemo(() => {
51
- const column = element.column;
52
- if (column.getIsPinned()) {
53
- return {
54
- width: column.getSize(),
55
- };
56
- }
57
- const originalColumnDef = element
58
- .getContext()
59
- .table._getColumnDefs()
60
- .find(col => getColumnId(col) === column.id);
61
- return {
62
- minWidth: (originalColumnDef === null || originalColumnDef === void 0 ? void 0 : originalColumnDef.size) || (originalColumnDef === null || originalColumnDef === void 0 ? void 0 : originalColumnDef.minSize) || column.columnDef.minSize,
63
- width: originalColumnDef === null || originalColumnDef === void 0 ? void 0 : originalColumnDef.size,
64
- maxWidth: (originalColumnDef === null || originalColumnDef === void 0 ? void 0 : originalColumnDef.maxSize) || column.columnDef.maxSize,
65
- };
66
- }, [element]);
49
+ const column = element.column;
50
+ const minWidth = column.columnDef.minSize;
51
+ const maxWidth = column.columnDef.maxSize;
52
+ const width = `var(--table-column-${column.id}-size)`;
53
+ const flexShrink = `var(--table-column-${column.id}-flex)`;
54
+ return useMemo(() => ({
55
+ minWidth,
56
+ width,
57
+ maxWidth,
58
+ flexShrink,
59
+ }), [flexShrink, maxWidth, minWidth, width]);
67
60
  }
package/dist/types.d.ts CHANGED
@@ -6,7 +6,7 @@ import { COLUMN_ALIGN, COLUMN_PIN_POSITION } from './constants';
6
6
  import { Except } from './helperComponents';
7
7
  type ColumnAlign = ValueOf<typeof COLUMN_ALIGN>;
8
8
  type ColumnPinPosition = ValueOf<typeof COLUMN_PIN_POSITION>;
9
- type BaseColumnDefinition<TData> = Except<ColumnDef<TData>, 'footer' | 'enablePinning' | 'enableGrouping' | 'enableResizing' | 'enableColumnFilter' | 'filterFn' | 'enableGlobalFilter' | 'enableMultiSort' | 'enableHiding'> & {
9
+ type BaseColumnDefinition<TData> = Except<ColumnDef<TData>, 'footer' | 'enablePinning' | 'enableGrouping' | 'enableColumnFilter' | 'filterFn' | 'enableGlobalFilter' | 'enableMultiSort' | 'enableHiding'> & {
10
10
  /** Заголовок колонки */
11
11
  header?: string | ((ctx: HeaderContext<TData, unknown>) => string);
12
12
  /** Позиционирование контента ячейки в теле таблицы */
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "access": "public"
5
5
  },
6
6
  "title": "Table",
7
- "version": "0.8.8",
7
+ "version": "0.9.0",
8
8
  "sideEffects": [
9
9
  "*.css",
10
10
  "*.woff",
@@ -46,10 +46,10 @@
46
46
  "@snack-uikit/typography": "0.6.1",
47
47
  "@snack-uikit/utils": "3.2.0",
48
48
  "@tanstack/match-sorter-utils": "8.8.4",
49
- "@tanstack/react-table": "8.10.7",
49
+ "@tanstack/react-table": "8.11.6",
50
50
  "classnames": "2.3.2",
51
51
  "uncontrollable": "8.0.0",
52
52
  "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.1/xlsx-0.20.1.tgz"
53
53
  },
54
- "gitHead": "57c597a3eebaf6528c0c61b6b27dd244753de1cf"
54
+ "gitHead": "b8ffb981d1933d3798b28e05095f25538768ffd4"
55
55
  }
@@ -243,6 +243,8 @@ export function Table<TData extends object>({
243
243
 
244
244
  defaultColumn: {
245
245
  enableSorting: false,
246
+ enableResizing: false,
247
+ minSize: 40,
246
248
  },
247
249
 
248
250
  globalFilterFn: fuzzyFilter,
@@ -255,6 +257,8 @@ export function Table<TData extends object>({
255
257
  enableFilters: true,
256
258
  getFilteredRowModel: getFilteredRowModel(),
257
259
 
260
+ enableColumnResizing: true,
261
+
258
262
  enableSorting: true,
259
263
  manualSorting: false,
260
264
  enableMultiSort: false,
@@ -296,6 +300,36 @@ export function Table<TData extends object>({
296
300
  }
297
301
  }, [loading, rowSelectionProp?.multiRow, table]);
298
302
 
303
+ const columnSizeVars = useMemo(() => {
304
+ const originalColumnDefs = table._getColumnDefs();
305
+ const headers = table.getFlatHeaders();
306
+ const colSizes: { [key: string]: string } = {};
307
+
308
+ for (let i = 0; i < headers.length; i++) {
309
+ const header = headers[i];
310
+ const originalColDef = originalColumnDefs.find(col => getColumnId(header) === col.id);
311
+ const originalColumnDefSize = originalColDef?.size;
312
+ const initSize = originalColumnDefSize ? `${originalColumnDefSize}px` : '100%';
313
+
314
+ let size = initSize;
315
+
316
+ if (header.column.getCanResize()) {
317
+ const currentSize = header.getSize();
318
+ const colDefSize = header.column.columnDef.size;
319
+
320
+ size = currentSize === colDefSize ? initSize : `${currentSize}px`;
321
+ }
322
+
323
+ colSizes[`--table-column-${header.id}-size`] = size;
324
+ colSizes[`--table-column-${header.id}-flex`] = size === '100%' ? 'unset' : '0';
325
+ }
326
+
327
+ return colSizes;
328
+ /* effect must be called only on columnSizingInfo.isResizingColumn changes
329
+ to reduce unnecessary recalculations */
330
+ // eslint-disable-next-line react-hooks/exhaustive-deps
331
+ }, [table.getState().columnSizingInfo.isResizingColumn]);
332
+
299
333
  const tableRows = table.getRowModel().rows;
300
334
  const loadingTableRows = loadingTable.getRowModel().rows;
301
335
  const tablePagination = table.getState().pagination;
@@ -336,7 +370,7 @@ export function Table<TData extends object>({
336
370
 
337
371
  <div className={styles.scrollWrapper} data-outline={outline || undefined}>
338
372
  <Scroll size='s' className={styles.table}>
339
- <div className={styles.tableContent}>
373
+ <div className={styles.tableContent} style={columnSizeVars}>
340
374
  <TableContext.Provider value={{ table }}>
341
375
  {loading ? (
342
376
  <SkeletonContextProvider loading>
@@ -50,6 +50,7 @@ export function TablePagination<TData>({
50
50
  total={table.getPageCount()}
51
51
  page={tablePaginationState.pageIndex + 1}
52
52
  onChange={handlePaginationOnChange}
53
+ size='xs'
53
54
  className={styles.pagination}
54
55
  />
55
56
  )}
@@ -63,6 +64,7 @@ export function TablePagination<TData>({
63
64
  label={optionsLabel}
64
65
  widthStrategy='auto'
65
66
  showClearButton={false}
67
+ size='xs'
66
68
  />
67
69
  )}
68
70
  </div>
@@ -1,5 +1,5 @@
1
1
  import cn from 'classnames';
2
- import { CSSProperties, MouseEventHandler, ReactNode } from 'react';
2
+ import { CSSProperties, forwardRef, MouseEventHandler, ReactNode } from 'react';
3
3
 
4
4
  import { DataAttributes } from '../types';
5
5
  import styles from './styles.module.scss';
@@ -7,15 +7,25 @@ import styles from './styles.module.scss';
7
7
  export type CellProps = {
8
8
  children: ReactNode;
9
9
  onClick?: MouseEventHandler;
10
+ onMouseUp?: MouseEventHandler;
10
11
  className?: string;
11
12
  style?: CSSProperties;
13
+ role?: 'cell' | 'columnheader';
12
14
  } & DataAttributes;
13
15
 
14
- export function Cell({ onClick, className, style, children, ...attributes }: CellProps) {
15
- return (
16
+ export const Cell = forwardRef<HTMLDivElement, CellProps>(
17
+ ({ onClick, onMouseUp, className, style, children, role = 'cell', ...attributes }, ref) => (
16
18
  // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
17
- <div role='cell' onClick={onClick} className={cn(styles.tableCell, className)} style={style} {...attributes}>
19
+ <div
20
+ role={role}
21
+ onClick={onClick}
22
+ onMouseUp={onMouseUp}
23
+ className={cn(styles.tableCell, className)}
24
+ style={style}
25
+ ref={ref}
26
+ {...attributes}
27
+ >
18
28
  {children}
19
29
  </div>
20
- );
21
- }
30
+ ),
31
+ );
@@ -1,5 +1,6 @@
1
1
  import { flexRender, Header } from '@tanstack/react-table';
2
2
  import cn from 'classnames';
3
+ import { MouseEvent, useRef } from 'react';
3
4
 
4
5
  import { TruncateString } from '@snack-uikit/truncate-string';
5
6
 
@@ -8,6 +9,7 @@ import { ColumnDefinition } from '../../../types';
8
9
  import { useCellSizes } from '../../hooks';
9
10
  import { Cell, CellProps } from '../Cell';
10
11
  import { getSortingIcon } from './helpers';
12
+ import { ResizeHandle } from './ResizeHandle';
11
13
  import styles from './styles.module.scss';
12
14
 
13
15
  type HeaderCellProps<TData> = Omit<CellProps, 'align' | 'children' | 'onClick' | 'style'> & {
@@ -15,39 +17,59 @@ type HeaderCellProps<TData> = Omit<CellProps, 'align' | 'children' | 'onClick' |
15
17
  };
16
18
 
17
19
  export function HeaderCell<TData>({ header, className }: HeaderCellProps<TData>) {
20
+ const cellRef = useRef<HTMLDivElement>(null);
18
21
  const isSortable = header.column.getCanSort();
22
+ const isResizable = header.column.getCanResize();
23
+ const isResizing = isResizable && header.column.getIsResizing();
24
+
19
25
  const sortDirection = isSortable && (header.column.getIsSorted() || undefined);
20
26
  const sortIcon = getSortingIcon(sortDirection);
21
27
 
28
+ const columnSizingInfo = header.getContext().table.getState().columnSizingInfo;
29
+ const isSomeColumnResizing = columnSizingInfo.isResizingColumn;
30
+
22
31
  const columnDef: ColumnDefinition<TData> = header.column.columnDef;
23
32
 
24
33
  const style = useCellSizes(header);
25
34
 
35
+ const sortingHandler = (e: MouseEvent<HTMLDivElement>) => {
36
+ if (isSomeColumnResizing) return;
37
+
38
+ return header.column.getToggleSortingHandler()?.(e);
39
+ };
40
+
26
41
  return (
27
42
  <Cell
28
43
  style={style}
29
- onClick={header.column.getToggleSortingHandler()}
44
+ onMouseUp={sortingHandler}
30
45
  data-sortable={isSortable || undefined}
31
46
  data-no-padding={columnDef.noHeaderCellPadding || undefined}
32
47
  data-no-offset={columnDef.noHeaderCellBorderOffset || undefined}
33
48
  data-test-id={TEST_IDS.headerCell}
49
+ data-resizing={isResizing || undefined}
50
+ role='columnheader'
34
51
  className={cn(styles.tableHeaderCell, className, columnDef.headerClassName)}
52
+ ref={cellRef}
35
53
  >
36
- {columnDef.header && (
37
- <div className={styles.tableHeaderCellName}>
38
- <TruncateString text={flexRender(columnDef.header, header.getContext()) as string} />
39
- </div>
40
- )}
41
-
42
- {Boolean(sortIcon) && (
43
- <div
44
- className={styles.tableHeaderSortIcon}
45
- data-sort-direction={sortDirection}
46
- data-test-id={TEST_IDS.headerSortIndicator}
47
- >
48
- {sortIcon}
49
- </div>
50
- )}
54
+ <div className={styles.tableHeaderCellMain}>
55
+ {columnDef.header && (
56
+ <div className={styles.tableHeaderCellName}>
57
+ <TruncateString text={flexRender(columnDef.header, header.getContext()) as string} />
58
+ </div>
59
+ )}
60
+
61
+ {Boolean(sortIcon) && (
62
+ <div
63
+ className={styles.tableHeaderIcon}
64
+ data-sort-direction={sortDirection}
65
+ data-test-id={TEST_IDS.headerSortIndicator}
66
+ >
67
+ {sortIcon}
68
+ </div>
69
+ )}
70
+ </div>
71
+
72
+ {Boolean(isResizable) && <ResizeHandle header={header} cellRef={cellRef} />}
51
73
  </Cell>
52
74
  );
53
75
  }
@@ -0,0 +1,63 @@
1
+ import { Header } from '@tanstack/react-table';
2
+ import cn from 'classnames';
3
+ import { RefObject } from 'react';
4
+
5
+ import styles from './styles.module.scss';
6
+
7
+ type ResizeHandleProps<TData> = {
8
+ header: Header<TData, unknown>;
9
+ cellRef: RefObject<HTMLDivElement>;
10
+ };
11
+
12
+ function getResizeIndicatorOffset<TData>({ header, cellRef }: ResizeHandleProps<TData>) {
13
+ const columnSizingInfo = header.getContext().table.getState().columnSizingInfo;
14
+
15
+ const { minSize, maxSize } = header.column.columnDef;
16
+ const { startSize, deltaOffset } = columnSizingInfo;
17
+
18
+ let offset = 0;
19
+
20
+ if (startSize !== null && deltaOffset !== null) {
21
+ const divElementSize = cellRef.current?.offsetWidth || 0;
22
+ const initSize = Math.max(startSize, divElementSize);
23
+
24
+ const limit = deltaOffset < 0 ? minSize : maxSize;
25
+ let deltaLimit = 0;
26
+
27
+ if (limit !== undefined && deltaOffset !== 0) {
28
+ deltaLimit = deltaOffset < 0 ? -(initSize - limit) : limit - initSize;
29
+
30
+ offset = deltaOffset < 0 ? Math.max(deltaOffset, deltaLimit) : Math.min(Math.abs(deltaOffset), deltaLimit);
31
+ }
32
+ }
33
+
34
+ return offset;
35
+ }
36
+
37
+ export function ResizeHandle<TData>({ header, cellRef }: ResizeHandleProps<TData>) {
38
+ const isResizing = header.column.getIsResizing();
39
+ const resizeHandler = header.getResizeHandler();
40
+
41
+ const offset = isResizing ? getResizeIndicatorOffset({ header, cellRef }) : 0;
42
+
43
+ return (
44
+ <>
45
+ {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
46
+ <div
47
+ className={cn(styles.tableHeaderIcon, styles.tableHeaderResizeHandle)}
48
+ data-resizing={isResizing || undefined}
49
+ onMouseDown={resizeHandler}
50
+ onTouchStart={resizeHandler}
51
+ />
52
+
53
+ {isResizing && (
54
+ <div
55
+ className={styles.tableHeaderResizeIndicator}
56
+ style={{
57
+ transform: `translateX(${offset}px)`,
58
+ }}
59
+ />
60
+ )}
61
+ </>
62
+ );
63
+ }
@@ -1,6 +1,39 @@
1
1
  @import '@snack-uikit/figma-tokens/build/scss/components/styles-tokens-table';
2
2
  @import '@snack-uikit/figma-tokens/build/scss/components/styles-tokens-element';
3
3
 
4
+ .tableHeaderResizeHandle {
5
+ cursor: ew-resize;
6
+
7
+ position: absolute;
8
+ z-index: 1;
9
+ top: 0;
10
+ right: 0;
11
+ transform: translateX(50%);
12
+
13
+ width: $dimension-1m;
14
+ height: 100%;
15
+
16
+ opacity: 0;
17
+
18
+ &::after {
19
+ content: '';
20
+
21
+ position: absolute;
22
+ top: 0;
23
+ left: 50%;
24
+ transform: translateX(-50%);
25
+
26
+ width: $border-width-table;
27
+ height: 100%;
28
+
29
+ background-color: $sys-neutral-decor-hovered;
30
+ }
31
+
32
+ &[data-resizing] {
33
+ opacity: 0;
34
+ }
35
+ }
36
+
4
37
  .tableHeaderCell {
5
38
  @include composite-var($table-head-column);
6
39
 
@@ -8,6 +41,7 @@
8
41
 
9
42
  display: flex;
10
43
  align-items: center;
44
+ justify-content: space-between;
11
45
 
12
46
  box-sizing: border-box;
13
47
  width: 100%;
@@ -24,11 +58,17 @@
24
58
  transform: translateX(-50%);
25
59
 
26
60
  width: calc(100% - $space-table-head-separator-padding * 2);
27
- height: 1px;
61
+ height: $border-width-table;
28
62
 
29
63
  background-color: $sys-neutral-decor-default;
30
64
  }
31
65
 
66
+ &:hover {
67
+ .tableHeaderResizeHandle:not([data-resizing]) {
68
+ opacity: 1;
69
+ }
70
+ }
71
+
32
72
  &[data-sortable] {
33
73
  cursor: pointer;
34
74
  }
@@ -48,6 +88,19 @@
48
88
  width: 100%;
49
89
  }
50
90
  }
91
+
92
+ &[data-resizing] {
93
+ user-select: none;
94
+ }
95
+ }
96
+
97
+ .tableHeaderCellMain {
98
+ overflow: auto;
99
+ display: flex;
100
+ align-items: center;
101
+
102
+ box-sizing: border-box;
103
+ min-width: 0;
51
104
  }
52
105
 
53
106
  .tableHeaderCellName {
@@ -62,7 +115,7 @@
62
115
  color: simple-var($sys-neutral-text-light);
63
116
  }
64
117
 
65
- .tableHeaderSortIcon {
118
+ .tableHeaderIcon {
66
119
  display: flex;
67
120
  box-sizing: border-box;
68
121
  color: simple-var($sys-neutral-text-light);
@@ -72,3 +125,29 @@
72
125
  height: simple-var($icon-xs) !important; /* stylelint-disable-line declaration-no-important */
73
126
  }
74
127
  }
128
+
129
+ .tableHeaderResizeIndicator {
130
+ cursor: col-resize;
131
+
132
+ position: absolute;
133
+ z-index: 2;
134
+ top: 0;
135
+ right: 0;
136
+
137
+ width: 1px;
138
+ height: 100%;
139
+
140
+ background-color: $sys-neutral-decor-activated;
141
+
142
+ &::after {
143
+ content: '';
144
+
145
+ position: absolute;
146
+ top: 0;
147
+ left: 0;
148
+ transform: translateX(-50%);
149
+
150
+ width: calc(100% + $dimension-4m);
151
+ height: 100%;
152
+ }
153
+ }
@@ -75,7 +75,7 @@ function RowActionsCell<TData>({ row, actions }: RowActionsCellProps<TData>) {
75
75
  </span>
76
76
  }
77
77
  triggerClassName={styles.rowActionsCellTrigger}
78
- size='s'
78
+ size='m'
79
79
  data-test-id={TEST_IDS.rowActions.droplist}
80
80
  >
81
81
  {actions.map(item => (
@@ -116,6 +116,7 @@ export function getRowActionsColumnDef<TData>({
116
116
  },
117
117
  noBodyCellPadding: true,
118
118
  cellClassName: styles.rowActionsCell,
119
+ enableResizing: false,
119
120
  cell: cell => <RowActionsCell row={cell.row} actions={actionsGenerator(cell)} />,
120
121
  };
121
122
  }
@@ -34,6 +34,7 @@ export function getSelectionCellColumnDef<TData>(): ColumnDefinition<TData> {
34
34
  noBodyCellPadding: true,
35
35
  size: 40,
36
36
  headerClassName: styles.selectionCellHeader,
37
+ enableResizing: false,
37
38
  cell: ({ row, table }) => {
38
39
  const disabled = !row.getCanSelect();
39
40
 
@@ -29,6 +29,7 @@ type StatusColumnDef = BaseStatusColumnDef & {
29
29
  renderDescription?: never;
30
30
  size?: never;
31
31
  header?: never;
32
+ enableResizing?: never;
32
33
  };
33
34
 
34
35
  type StatusColumnDefWithDescription<TData> = BaseStatusColumnDef & {
@@ -38,6 +39,8 @@ type StatusColumnDefWithDescription<TData> = BaseStatusColumnDef & {
38
39
  size: number;
39
40
  /** Заголовок колонки */
40
41
  header?: ColumnDefinition<TData>['header'];
42
+ /** Включение/выключение ресайза колонки */
43
+ enableResizing?: boolean;
41
44
  };
42
45
 
43
46
  export type StatusColumnDefinitionProps<TData> = StatusColumnDef | StatusColumnDefWithDescription<TData>;
@@ -73,6 +76,7 @@ export function getStatusColumnDef<TData>({
73
76
  renderDescription,
74
77
  size,
75
78
  enableSorting,
79
+ enableResizing,
76
80
  }: StatusColumnDefinitionProps<TData>): ColumnDefinition<TData> {
77
81
  const hasDescription = Boolean(renderDescription);
78
82
 
@@ -90,6 +94,7 @@ export function getStatusColumnDef<TData>({
90
94
  accessorKey,
91
95
  enableSorting,
92
96
  header: hasDescription ? header : undefined,
97
+ enableResizing: enableResizing ?? hasDescription,
93
98
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
94
99
  accessorFn: (row: any) =>
95
100
  renderDescription && Object.hasOwn(row as object, accessorKey)
@@ -9,7 +9,7 @@ export function HeaderRow() {
9
9
  const { leftPinned, unpinned, rightPinned } = useHeaderGroups();
10
10
 
11
11
  return (
12
- <Row className={styles.tableHeader} data-test-id={TEST_IDS.headerRow} role='rowheader'>
12
+ <Row className={styles.tableHeader} data-test-id={TEST_IDS.headerRow}>
13
13
  {leftPinned && (
14
14
  <PinnedCells position={COLUMN_PIN_POSITION.Left}>
15
15
  {leftPinned.map(headerGroup =>
@@ -8,12 +8,12 @@ type RowProps = {
8
8
  children: ReactNode;
9
9
  onClick?(e: MouseEvent<HTMLDivElement>): void;
10
10
  className?: string;
11
- role?: 'rowheader' | 'row';
12
11
  } & DataAttributes;
13
12
 
14
- export function Row({ onClick, role = 'row', children, className, ...attributes }: RowProps) {
13
+ export function Row({ onClick, children, className, ...attributes }: RowProps) {
15
14
  return (
16
- <div onClick={onClick} className={cn(styles.tableRow, className)} role={role} {...attributes}>
15
+ // eslint-disable-next-line jsx-a11y/interactive-supports-focus
16
+ <div onClick={onClick} className={cn(styles.tableRow, className)} role='row' {...attributes}>
17
17
  {children}
18
18
  </div>
19
19
  );
@@ -2,7 +2,6 @@ import { Cell, Header, HeaderGroup, Row } from '@tanstack/react-table';
2
2
  import { useMemo } from 'react';
3
3
 
4
4
  import { useTableContext } from './contexts';
5
- import { getColumnId } from './helpers';
6
5
 
7
6
  function hasHeaders<TData>(groups: HeaderGroup<TData>[]) {
8
7
  return groups.some(group => group.headers.length);
@@ -61,24 +60,20 @@ export function useRowCells<TData>(row: Row<TData>) {
61
60
  }
62
61
 
63
62
  export function useCellSizes<TData>(element: Cell<TData, unknown> | Header<TData, unknown>) {
64
- return useMemo(() => {
65
- const column = element.column;
66
-
67
- if (column.getIsPinned()) {
68
- return {
69
- width: column.getSize(),
70
- };
71
- }
72
-
73
- const originalColumnDef = element
74
- .getContext()
75
- .table._getColumnDefs()
76
- .find(col => getColumnId(col) === column.id);
77
-
78
- return {
79
- minWidth: originalColumnDef?.size || originalColumnDef?.minSize || column.columnDef.minSize,
80
- width: originalColumnDef?.size,
81
- maxWidth: originalColumnDef?.maxSize || column.columnDef.maxSize,
82
- };
83
- }, [element]);
63
+ const column = element.column;
64
+
65
+ const minWidth = column.columnDef.minSize;
66
+ const maxWidth = column.columnDef.maxSize;
67
+ const width = `var(--table-column-${column.id}-size)`;
68
+ const flexShrink = `var(--table-column-${column.id}-flex)`;
69
+
70
+ return useMemo(
71
+ () => ({
72
+ minWidth,
73
+ width,
74
+ maxWidth,
75
+ flexShrink,
76
+ }),
77
+ [flexShrink, maxWidth, minWidth, width],
78
+ );
84
79
  }
package/src/types.ts CHANGED
@@ -25,7 +25,6 @@ type BaseColumnDefinition<TData> = Except<
25
25
  | 'footer'
26
26
  | 'enablePinning'
27
27
  | 'enableGrouping'
28
- | 'enableResizing'
29
28
  | 'enableColumnFilter'
30
29
  | 'filterFn'
31
30
  | 'enableGlobalFilter'