@gravity-ui/react-data-table 2.0.0 → 2.1.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.
package/README.md CHANGED
@@ -25,6 +25,7 @@ of the sort list. A simple Click sorts the table by this column only. Sorting or
25
25
  | theme | `String` | | 'yandex-cloud' | The name of the theme. To use the built-in table theme, use the `'legacy'` value. For compatibility with @gravity-ui/uikit colors, use `'yandex-cloud'` (default) |
26
26
  | onSort | `Function` | | | Sort listener `([{columnId: <string>, order: <DataTable.ASCENDING `&#124;` DataTable.DESCENDING>}]) => void`. Use it to save the sorting state and pass it to the `initialSortOrder` on page refresh. |
27
27
  | sortOrder | `Object` | | | Sorting in the format `{columnId: <string>, order: <DataTable.ASCENDING `&#124;` DataTable.DESCENDING>}` or a list of such objects. |
28
+ | onResize | `Function` | | | Resize listener `(columnId: string, newWidth: number) => void`. Use it to save new column width. |
28
29
  | visibleRowIndex | `Function` | | | A method `(row, index) => number` that returns the row index shown in the column enabled by the `displayIndices` flag. Use it to specify a correct row index when applying external sorting |
29
30
  | onRowClick | `Function` | | | Click listener for a row `(row, index, event) => void` |
30
31
  | nullBeforeNumbers | `Boolean` | | | A flag that sets the position of `null`/`undefined` values when sorting. If true, `null`/`undefined` values are shown at the top in the case of ascending order and at the bottom in the case of descending order. If `false`/`undefined`, they are shown at the bottom |
@@ -54,6 +55,7 @@ of the sort list. A simple Click sorts the table by this column only. Sorting or
54
55
  | externalSort | `Boolean` | `false` | Disable data sorting on the component side. [Read more](#externalSort) |
55
56
  | disableSortReset | `Boolean` | `false` | Disable sort reset (enables the `none -> default <-> reversed default` sort scheme) |
56
57
  | defaultOrder | `DataTable.ASCENDING DataTable.DESCENDING` | `DataTable.ASCENDING` | Default sort order in the table |
58
+ | defaultResizeable | `Boolean` | `false` | Value that is used to determine whether column is resizeable, if no explicit value for column is set |
57
59
  | highlightRows | `Boolean` | | Highlighting table rows on hover |
58
60
  | stripedRows | `Boolean` | | Whether to color even/odd rows |
59
61
  | headerMod | `multiline` `pre` | | Enable a multi-line (the `multiline` value) or pre-formatted header (the `pre` value) |
@@ -83,26 +85,29 @@ The cell will show `row.column_name` as it is.
83
85
 
84
86
  The `name` field is the only mandatory field. The remaining fields are described below:
85
87
 
86
- | Property | Type | Description |
87
- | :------------ | :------------------------------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
88
- | name | `string` | Column name and default value for the `header` and `accessor` |
89
- | header | `string` or `node` | Column header |
90
- | headerTitle | `string` | Header title |
91
- | className | `string` | Column class |
92
- | width         | `string \| number` | Column width |
93
- | align | `DataTable.LEFT DataTable.CENTER DataTable.RIGHT` | Text alignment in a cell |
94
- | accessor | `func` | Callback `(row) => (value)`. Use it if you want to display deeply "hidden" values in the column. For example: `(row) => (row.child1.child2[0].value)` |
95
- | title | `string` or `func:string` | Callback `(row) => (title)`. Lets you set a fixed title (in the case of `string`) or a dynamic title for column cells. Returns a string. |
96
- | render | `func:node` | Callback `({value, row, index}) => (node)`. Lets you render a cell in an arbitrary way. As an input, it gets a value, a whole row, or a row index. Returns a React node. |
97
- | customStyle | `func` | Callback `({row, index, header, name}) => (styleObject)`. Lets you set a custom style for the cell. In the callback, you pass a string with data, an index, a column name, and a flag whether the cell is a header. At the output, you expect an object with styles following the React syntax. |
98
- | onClick | `func` | Callback `(row, column) => ()`. The onClick listener on the cell. |
99
- | sortable | `Boolean` | Whether the column is sortable. |
100
- | defaultOrder | `DataTable.ASCENDING DataTable.DESCENDING` | A default sorting order in the column. |
101
- | sortAccessor | `func` | Callback `(row) => (value)`. Use it if you want to sort by a value other than `value`, or if `value` is non-scalar. |
102
- | sortAscending | `func` | Callback `({row, index, span?}, {row, index, span?}) => (sortValue)`. Use if you want to customize sorting completely. |
103
- | sub | `array` | Nested columns |
104
- | group | `Boolean` | Groups the same values into one cell, allowing you to create complex tables. [More details](#group) |
105
- | autogroup | `Boolean` | Enables ascending sorting across all columns with group, resulting in autogrouping. Defaults to `true` |
88
+ | Property | Type | Description |
89
+ | :------------- | :------------------------------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
90
+ | name | `string` | Column name and default value for the `header` and `accessor` |
91
+ | header | `string` or `node` | Column header |
92
+ | headerTitle | `string` | Header title |
93
+ | className | `string` | Column class |
94
+ | width         | `string \| number` | Column width |
95
+ | resizeable     | `boolean` | Determines whether column width can be changed. Applied only if `onResize` function is passed. |
96
+ | resizeMinWidth | `number` | Min column width for resize. |
97
+ | resizeMaxWidth | `number` | Max column width for resize. |
98
+ | align | `DataTable.LEFT DataTable.CENTER DataTable.RIGHT` | Text alignment in a cell |
99
+ | accessor | `func` | Callback `(row) => (value)`. Use it if you want to display deeply "hidden" values in the column. For example: `(row) => (row.child1.child2[0].value)` |
100
+ | title | `string` or `func:string` | Callback `(row) => (title)`. Lets you set a fixed title (in the case of `string`) or a dynamic title for column cells. Returns a string. |
101
+ | render | `func:node` | Callback `({value, row, index}) => (node)`. Lets you render a cell in an arbitrary way. As an input, it gets a value, a whole row, or a row index. Returns a React node. |
102
+ | customStyle | `func` | Callback `({row, index, header, name}) => (styleObject)`. Lets you set a custom style for the cell. In the callback, you pass a string with data, an index, a column name, and a flag whether the cell is a header. At the output, you expect an object with styles following the React syntax. |
103
+ | onClick | `func` | Callback `(row, column) => ()`. The onClick listener on the cell. |
104
+ | sortable | `Boolean` | Whether the column is sortable. |
105
+ | defaultOrder | `DataTable.ASCENDING DataTable.DESCENDING` | A default sorting order in the column. |
106
+ | sortAccessor | `func` | Callback `(row) => (value)`. Use it if you want to sort by a value other than `value`, or if `value` is non-scalar. |
107
+ | sortAscending | `func` | Callback `({row, index, span?}, {row, index, span?}) => (sortValue)`. Use if you want to customize sorting completely. |
108
+ | sub | `array` | Nested columns |
109
+ | group | `Boolean` | Groups the same values into one cell, allowing you to create complex tables. [More details](#group) |
110
+ | autogroup | `Boolean` | Enables ascending sorting across all columns with group, resulting in autogrouping. Defaults to `true` |
106
111
 
107
112
  ### group
108
113
 
@@ -29,6 +29,7 @@ interface TableHeadProps<T> {
29
29
  headColumns: HeadColumns<T>;
30
30
  displayIndices?: boolean;
31
31
  onSort?: TableProps<T>['onSort'];
32
+ onResize?: TableProps<T>['onResize'];
32
33
  onColumnsUpdated?: (widths: number[]) => void;
33
34
  renderedDataRows?: React.ReactNode;
34
35
  onDataRowsHeightChange?: StickyHeadProps<T>['onDataRowsHeightChange'];
@@ -36,7 +37,7 @@ interface TableHeadProps<T> {
36
37
  declare class TableHead<T> extends React.Component<TableHeadProps<T>> {
37
38
  _dataRowsRef: HTMLTableSectionElement | null;
38
39
  dataRowsHeightObserver?: HeightObserver;
39
- renderedColumns: HTMLTableHeaderCellElement[];
40
+ renderedColumns: HTMLTableCellElement[];
40
41
  componentDidMount(): void;
41
42
  componentDidUpdate(): void;
42
43
  componentWillUnmount(): void;
@@ -48,6 +49,7 @@ declare class TableHead<T> extends React.Component<TableHeadProps<T>> {
48
49
  renderHeadLevel: (row: HeadColumnsItem<T>, rowIndex: number) => JSX.Element;
49
50
  dataRowsRef: (ref: HTMLTableSectionElement) => void;
50
51
  _getColumnRef: (index: number) => (node: HTMLTableHeaderCellElement) => void;
52
+ _getRenderedColumn: (index?: number) => HTMLTableCellElement | undefined;
51
53
  }
52
54
  interface StickyHeadProps<T> {
53
55
  dataColumns: DataColumns<T>;
@@ -55,6 +57,7 @@ interface StickyHeadProps<T> {
55
57
  mode: HeadPositionInner;
56
58
  displayIndices?: Settings['displayIndices'];
57
59
  onSort?: TableProps<T>['onSort'];
60
+ onResize?: TableProps<T>['onResize'];
58
61
  top: number;
59
62
  renderedDataRows: React.ReactNode;
60
63
  onDataRowsHeightChange: (height: number) => void;
@@ -129,6 +132,7 @@ interface TableProps<T> {
129
132
  rowKey: (row: T, index: number) => string | number;
130
133
  startIndex: DataTableProps<T>['startIndex'];
131
134
  onSort: DataTableView<T>['onSort'];
135
+ onResize: DataTableProps<T>['onResize'];
132
136
  renderEmptyRow: unknown;
133
137
  nullBeforeNumbers?: boolean;
134
138
  getColSpansOfRow?: (p: Omit<TableRowProps<T>, 'getColSpansOfRow' | 'onClick' | 'className'>) => {
@@ -214,6 +218,7 @@ declare class DataTableView<T> extends React.Component<DataTableProps<T>, DataTa
214
218
  sortable: boolean;
215
219
  externalSort: boolean;
216
220
  defaultOrder: OrderType;
221
+ defaultResizeable: boolean;
217
222
  };
218
223
  rowKey: (row: any, index: number) => any;
219
224
  initialSortOrder: {};
@@ -243,6 +248,7 @@ declare class DataTableView<T> extends React.Component<DataTableProps<T>, DataTa
243
248
  externalSort: boolean;
244
249
  disableSortReset?: boolean | undefined;
245
250
  defaultOrder: OrderType;
251
+ defaultResizeable: boolean;
246
252
  highlightRows?: boolean | undefined;
247
253
  stripedRows?: boolean | undefined;
248
254
  headerMod?: "pre" | "multiline" | undefined;
@@ -270,6 +276,7 @@ declare class DataTableView<T> extends React.Component<DataTableProps<T>, DataTa
270
276
  externalSort: boolean;
271
277
  disableSortReset?: boolean | undefined;
272
278
  defaultOrder: OrderType;
279
+ defaultResizeable: boolean;
273
280
  highlightRows?: boolean | undefined;
274
281
  stripedRows?: boolean | undefined;
275
282
  headerMod?: "pre" | "multiline" | undefined;
@@ -299,6 +306,7 @@ declare class DataTableView<T> extends React.Component<DataTableProps<T>, DataTa
299
306
  externalSort: boolean;
300
307
  disableSortReset?: boolean | undefined;
301
308
  defaultOrder: OrderType;
309
+ defaultResizeable: boolean;
302
310
  highlightRows?: boolean | undefined;
303
311
  stripedRows?: boolean | undefined;
304
312
  headerMod?: "pre" | "multiline" | undefined;
@@ -328,6 +336,7 @@ declare class DataTableView<T> extends React.Component<DataTableProps<T>, DataTa
328
336
  externalSort: boolean;
329
337
  disableSortReset?: boolean | undefined;
330
338
  defaultOrder: OrderType;
339
+ defaultResizeable: boolean;
331
340
  highlightRows?: boolean | undefined;
332
341
  stripedRows?: boolean | undefined;
333
342
  headerMod?: "pre" | "multiline" | undefined;
@@ -340,6 +349,7 @@ declare class DataTableView<T> extends React.Component<DataTableProps<T>, DataTa
340
349
  _tableRef: (node: Table<T>) => void;
341
350
  renderMemoizedCell: ({ column, value, row, index, footer, headerData }: CellProps<T>) => JSX.Element;
342
351
  getColumn: (column: Column<T>, columnIndex: number) => {
352
+ resizeable: boolean;
343
353
  sortable: boolean;
344
354
  _className: string;
345
355
  _getValue: (row: T) => any;
@@ -382,6 +392,8 @@ declare class DataTableView<T> extends React.Component<DataTableProps<T>, DataTa
382
392
  sortAccessor?: string | ((row: T) => any) | undefined;
383
393
  sortAscending?: import("./types").Comparator<T> | undefined;
384
394
  sub?: Column<T>[] | undefined;
395
+ resizeMinWidth?: number | undefined;
396
+ resizeMaxWidth?: number | undefined;
385
397
  index: number;
386
398
  columnIndex: number;
387
399
  dataColumn: boolean;
@@ -2,11 +2,11 @@ import { __rest } from "tslib";
2
2
  import * as React from 'react';
3
3
  import ReactList from 'react-list';
4
4
  import './DataTable.scss';
5
+ import { ResizeHandler } from './ResizeHandler';
5
6
  import { ASCENDING, CENTER, DESCENDING, FIXED, INDEX_COLUMN, LEFT, MOVING, RIGHT } from './constants';
6
7
  import { positionStickySupported } from './featureSupport';
7
8
  import { HeightObserver } from './height-observer';
8
- import { cn, externalToInternalSortOrder, getSortOrder, getSortedData, internalToExternalSortOrder, } from './util';
9
- const b = cn('data-table');
9
+ import { b, externalToInternalSortOrder, getSortOrder, getSortedData, internalToExternalSortOrder, } from './util';
10
10
  const ICON_ASC = (React.createElement("svg", { className: b('icon'), viewBox: "0 0 10 6", width: "10", height: "6" },
11
11
  React.createElement("path", { fill: "currentColor", d: "M0 5h10l-5 -5z" })));
12
12
  const ICON_DESC = (React.createElement("svg", { className: b('icon'), viewBox: "0 0 10 6", width: "10", height: "6" },
@@ -66,14 +66,19 @@ class TableRow extends React.PureComponent {
66
66
  }
67
67
  }
68
68
  const value = column._getValue(row);
69
- return (React.createElement("td", { key: columnIndex, className: column._className, title: column._getTitle(row), style: column.customStyle({
70
- row,
71
- index,
72
- name: column.name,
73
- header: false,
74
- footer,
75
- headerData,
76
- }), colSpan: colSpans ? colSpans[column.name] : undefined, rowSpan: rowSpan, onClick: column._getOnClick({ row, index, footer, headerData }) }, column._renderValue({ value, row, index, footer, headerData })));
69
+ let style = column.customStyle({
70
+ row,
71
+ index,
72
+ name: column.name,
73
+ header: false,
74
+ footer,
75
+ headerData,
76
+ });
77
+ // Fixed cell width for resizeable columns for proper content wrap
78
+ if (column.resizeable) {
79
+ style = Object.assign(Object.assign({}, style), { width: column.width, maxWidth: column.width });
80
+ }
81
+ return (React.createElement("td", { key: columnIndex, className: column._className, title: column._getTitle(row), style: style, colSpan: colSpans ? colSpans[column.name] : undefined, rowSpan: rowSpan, onClick: column._getOnClick({ row, index, footer, headerData }) }, column._renderValue({ value, row, index, footer, headerData })));
77
82
  })));
78
83
  }
79
84
  }
@@ -86,13 +91,21 @@ class TableHead extends React.Component {
86
91
  this._dataRowsRef = null;
87
92
  this.renderedColumns = [];
88
93
  this.renderHeadCell = (headCell) => {
94
+ var _a;
95
+ const { onResize } = this.props;
89
96
  const { column, rowSpan, colSpan } = headCell;
90
- const { sortable = false, header = column.name, className, index, columnIndex, align, } = column;
97
+ const { sortable = false, header = column.name, className, index, columnIndex, align, name, width, resizeable, resizeMinWidth, resizeMaxWidth, } = column;
91
98
  const { headerTitle = (typeof header === 'string' && header) || undefined } = column;
92
- return (React.createElement("th", { ref: column.dataColumn ? this._getColumnRef(columnIndex) : null, className: b('th', { sortable, align }, className), key: column.name, title: headerTitle, "data-index": index, colSpan: colSpan, rowSpan: rowSpan, style: column.customStyle && column.customStyle({ header: true, name: column.name }), onClick: this._getOnSortClick(column) },
99
+ let style = (_a = column.customStyle) === null || _a === void 0 ? void 0 : _a.call(column, { header: true, name });
100
+ // Fixed cell width for resizeable columns for proper content wrap
101
+ if (resizeable) {
102
+ style = Object.assign(Object.assign({}, style), { width, maxWidth: width });
103
+ }
104
+ return (React.createElement("th", { ref: column.dataColumn ? this._getColumnRef(columnIndex) : null, className: b('th', { sortable, align }, className), key: name, title: headerTitle, "data-index": index, colSpan: colSpan, rowSpan: rowSpan, style: style, onClick: this._getOnSortClick(column) },
93
105
  React.createElement("div", { className: b('head-cell') },
94
106
  header,
95
- React.createElement(ColumnSortIcon, Object.assign({}, column)))));
107
+ React.createElement(ColumnSortIcon, Object.assign({}, column))),
108
+ resizeable && (React.createElement(ResizeHandler, { getColumn: this._getRenderedColumn, columnIndex: columnIndex, onResize: onResize, columnId: name, minWidth: resizeMinWidth, maxWidth: resizeMaxWidth }))));
96
109
  };
97
110
  this.renderHeadLevel = (row, rowIndex) => {
98
111
  return (React.createElement("tr", { key: rowIndex, className: b('head-row') }, row.map(this.renderHeadCell)));
@@ -109,6 +122,12 @@ class TableHead extends React.Component {
109
122
  this.renderedColumns[index] = node;
110
123
  };
111
124
  };
125
+ this._getRenderedColumn = (index) => {
126
+ if (index) {
127
+ return this.renderedColumns[index];
128
+ }
129
+ return undefined;
130
+ };
112
131
  }
113
132
  componentDidMount() {
114
133
  this._calculateColumnsWidth();
@@ -445,19 +464,19 @@ class Table extends React.PureComponent {
445
464
  }
446
465
  }
447
466
  renderHead() {
448
- const { columns, onSort } = this.props;
467
+ const { columns, onSort, onResize } = this.props;
449
468
  const { displayIndices } = this.props.settings;
450
469
  const rows = this.renderHeaderRows();
451
- return (React.createElement(TableHead, Object.assign({ ref: this._refHead }, columns, { displayIndices: Boolean(displayIndices), onSort: onSort, onColumnsUpdated: this._onColumnsUpdated, renderedDataRows: rows })));
470
+ return (React.createElement(TableHead, Object.assign({ ref: this._refHead }, columns, { displayIndices: Boolean(displayIndices), onSort: onSort, onResize: onResize, onColumnsUpdated: this._onColumnsUpdated, renderedDataRows: rows })));
452
471
  }
453
472
  renderStickyHead() {
454
- const { columns, onSort } = this.props;
473
+ const { columns, onSort, onResize } = this.props;
455
474
  const { displayIndices, stickyTop, stickyHead } = this.props.settings;
456
475
  const top = stickyTop === 'auto' && this._body && this._body.parentNode
457
476
  ? this._body.parentNode.offsetTop
458
477
  : Number(stickyTop) || 0;
459
478
  const rows = this.renderHeaderRows();
460
- return (React.createElement(StickyHead, Object.assign({ mode: stickyHead, top: top, ref: this._refStickyHead }, columns, { displayIndices: displayIndices, onSort: onSort, renderedDataRows: rows, onDataRowsHeightChange: this.onMovingHeaderDataRowsHeightChange })));
479
+ return (React.createElement(StickyHead, Object.assign({ mode: stickyHead, top: top, ref: this._refStickyHead }, columns, { displayIndices: displayIndices, onSort: onSort, onResize: onResize, renderedDataRows: rows, onDataRowsHeightChange: this.onMovingHeaderDataRowsHeightChange })));
461
480
  }
462
481
  renderStickyFooter() {
463
482
  const { columns } = this.props;
@@ -517,6 +536,8 @@ class DataTableView extends React.Component {
517
536
  headerData }));
518
537
  };
519
538
  this.getColumn = (column, columnIndex) => {
539
+ var _a;
540
+ const { onResize } = this.props;
520
541
  const { settings } = this.state;
521
542
  const { defaultOrder } = settings;
522
543
  const { sortOrder = {}, sortColumns, indexColumn } = this.state;
@@ -525,6 +546,7 @@ class DataTableView extends React.Component {
525
546
  const { name, accessor = column.name, align, sortable = settings.sortable, group, autogroup = true, } = column;
526
547
  const { sortAccessor, onClick } = column;
527
548
  const _className = b('td', { align }, column.className);
549
+ const resizeable = ((_a = column.resizeable) !== null && _a !== void 0 ? _a : settings.defaultResizeable) && Boolean(onResize);
528
550
  const _getValue = typeof accessor === 'function'
529
551
  ? (row) => accessor(row)
530
552
  : (row) => {
@@ -543,7 +565,7 @@ class DataTableView extends React.Component {
543
565
  const _getOnClick = typeof onClick === 'function'
544
566
  ? (data) => (event) => onClick(data, column, event)
545
567
  : () => undefined;
546
- return Object.assign(Object.assign({ index: columnIndex - indexAdjustment, columnIndex, dataColumn: true, defaultOrder }, column), { sortable: sortable && isSortEnabled, _className,
568
+ return Object.assign(Object.assign({ index: columnIndex - indexAdjustment, columnIndex, dataColumn: true, defaultOrder }, column), { resizeable, sortable: sortable && isSortEnabled, _className,
547
569
  _getValue,
548
570
  _getTitle,
549
571
  _getSortValue,
@@ -606,6 +628,7 @@ class DataTableView extends React.Component {
606
628
  return startIndex + index;
607
629
  },
608
630
  sortable: false,
631
+ resizeable: false,
609
632
  width: 20 + Math.ceil(Math.log10(lastIndex)) * 10,
610
633
  };
611
634
  }
@@ -615,7 +638,7 @@ class DataTableView extends React.Component {
615
638
  ? Object.assign({}, externalToInternalSortOrder(nextProps.sortOrder, nextProps.settings)) : undefined));
616
639
  }
617
640
  render() {
618
- const { getColSpansOfRow, headerData, data, footerData, columns, startIndex, emptyDataMessage, rowClassName, rowKey, onRowClick, theme, renderEmptyRow, nullBeforeNumbers, } = this.props;
641
+ const { getColSpansOfRow, headerData, data, footerData, columns, startIndex, emptyDataMessage, rowClassName, rowKey, onRowClick, onResize, theme, renderEmptyRow, nullBeforeNumbers, } = this.props;
619
642
  const { settings, sortOrder, sortColumns } = this.state;
620
643
  const { highlightRows = false, stripedRows = false, headerMod = false } = settings;
621
644
  const tableClassName = b({
@@ -634,7 +657,7 @@ class DataTableView extends React.Component {
634
657
  }, {
635
658
  nullBeforeNumbers,
636
659
  externalSort: settings === null || settings === void 0 ? void 0 : settings.externalSort,
637
- }), footerData: footerData, onSort: this.onSort }));
660
+ }), footerData: footerData, onSort: this.onSort, onResize: onResize }));
638
661
  }
639
662
  getComplexColumns(columns) {
640
663
  const headColumns = [];
@@ -689,6 +712,7 @@ DataTableView.defaultProps = {
689
712
  sortable: true,
690
713
  externalSort: false,
691
714
  defaultOrder: ASCENDING,
715
+ defaultResizeable: false,
692
716
  },
693
717
  rowKey: (row, index) => Object.prototype.hasOwnProperty.call(row, 'id') ? row.id : index,
694
718
  initialSortOrder: {},
@@ -67,6 +67,7 @@ $cell-border: 1px solid var(--data-table-border-color);
67
67
  }
68
68
 
69
69
  &__th {
70
+ box-sizing: border-box;
70
71
  font-weight: 500;
71
72
  cursor: default;
72
73
  padding: var(--data-table-header-vertical-padding) var(--data-table-cell-horizontal-padding);
@@ -98,6 +99,7 @@ $cell-border: 1px solid var(--data-table-border-color);
98
99
  }
99
100
 
100
101
  &__td {
102
+ box-sizing: border-box;
101
103
  padding: var(--data-table-cell-vertical-padding) var(--data-table-cell-horizontal-padding);
102
104
  border: $cell-border;
103
105
  vertical-align: var(--data-table-cell-align);
@@ -267,4 +269,22 @@ $cell-border: 1px solid var(--data-table-border-color);
267
269
  --data-table-color-hover-area: #ffeba0;
268
270
  --data-table-color-footer-area: var(--data-table-color-base);
269
271
  }
272
+
273
+ &__resize-handler {
274
+ visibility: hidden;
275
+ position: absolute;
276
+ right: 0;
277
+ top: 0;
278
+ cursor: col-resize;
279
+ width: 6px;
280
+ height: 100%;
281
+ background-color: var(--g-color-base-generic);
282
+
283
+ &_resizing {
284
+ visibility: visible;
285
+ }
286
+ }
287
+ &__th:hover > &__resize-handler {
288
+ visibility: visible;
289
+ }
270
290
  }
@@ -0,0 +1,11 @@
1
+ /// <reference types="react" />
2
+ interface ResizeHandlerProps {
3
+ getColumn: (index?: number) => HTMLTableCellElement | undefined;
4
+ columnIndex?: number;
5
+ columnId: string;
6
+ maxWidth?: number;
7
+ minWidth?: number;
8
+ onResize?: (columnId: string, width: number) => void;
9
+ }
10
+ export declare function ResizeHandler({ getColumn, columnIndex, columnId, minWidth, maxWidth, onResize, }: ResizeHandlerProps): JSX.Element;
11
+ export {};
@@ -0,0 +1,61 @@
1
+ import React from 'react';
2
+ import { b, calculateColumnWidth, rafThrottle } from './util';
3
+ export function ResizeHandler({ getColumn, columnIndex, columnId, minWidth, maxWidth, onResize, }) {
4
+ const elementRef = React.useRef(null);
5
+ const [resizing, setResizing] = React.useState(false);
6
+ React.useEffect(() => {
7
+ const element = elementRef.current;
8
+ if (!element) {
9
+ return undefined;
10
+ }
11
+ let mouseXPosition;
12
+ let initialColumnWidth;
13
+ let currentColumnWidth;
14
+ const onMouseMove = rafThrottle((e) => {
15
+ restrictMouseEvent(e);
16
+ if (typeof mouseXPosition !== 'number' || typeof initialColumnWidth !== 'number') {
17
+ return;
18
+ }
19
+ const xDiff = e.clientX - mouseXPosition;
20
+ const newWidth = calculateColumnWidth(initialColumnWidth + xDiff, minWidth, maxWidth);
21
+ if (newWidth === currentColumnWidth) {
22
+ return;
23
+ }
24
+ currentColumnWidth = newWidth;
25
+ onResize === null || onResize === void 0 ? void 0 : onResize(columnId, currentColumnWidth);
26
+ });
27
+ const onMouseUp = (e) => {
28
+ restrictMouseEvent(e);
29
+ if (currentColumnWidth !== undefined) {
30
+ onResize === null || onResize === void 0 ? void 0 : onResize(columnId, currentColumnWidth);
31
+ }
32
+ setResizing(false);
33
+ mouseXPosition = undefined;
34
+ document.removeEventListener('mousemove', onMouseMove);
35
+ document.removeEventListener('mouseup', onMouseUp);
36
+ };
37
+ const onMouseDown = (e) => {
38
+ var _a;
39
+ initialColumnWidth = (_a = getColumn(columnIndex)) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect().width;
40
+ restrictMouseEvent(e);
41
+ mouseXPosition = e.clientX;
42
+ setResizing(true);
43
+ document.addEventListener('mousemove', onMouseMove);
44
+ document.addEventListener('mouseup', onMouseUp);
45
+ };
46
+ element.addEventListener('mousedown', onMouseDown);
47
+ return () => {
48
+ element.removeEventListener('mousedown', onMouseDown);
49
+ document.removeEventListener('mousemove', onMouseMove);
50
+ document.removeEventListener('mouseup', onMouseUp);
51
+ };
52
+ }, [columnId, onResize, minWidth, maxWidth, getColumn, columnIndex]);
53
+ return (React.createElement("span", { ref: elementRef, className: b('resize-handler', { resizing }),
54
+ // Prevent sort trigger on resize
55
+ onClick: (e) => restrictMouseEvent(e) }));
56
+ }
57
+ // Prevent sort trigger and text selection on resize
58
+ function restrictMouseEvent(e) {
59
+ e.preventDefault();
60
+ e.stopPropagation();
61
+ }
@@ -1,4 +1,5 @@
1
1
  export { INDEX_COLUMN } from './constants';
2
2
  export * from './types';
3
+ export * from './useTableResize';
3
4
  import DataTable from './DataTable';
4
5
  export default DataTable;
@@ -1,4 +1,5 @@
1
1
  export { INDEX_COLUMN } from './constants';
2
2
  export * from './types';
3
+ export * from './useTableResize';
3
4
  import DataTable from './DataTable';
4
5
  export default DataTable;
@@ -24,6 +24,7 @@ export interface DataTableProps<T> {
24
24
  sortOrder?: SortOrder | SortOrder[];
25
25
  theme: THEMES | string;
26
26
  onSort?: (sortOrder: DataTableProps<T>['initialSortOrder']) => void;
27
+ onResize?: HandleResize;
27
28
  onRowClick?: (row: T, index: number, event: React.MouseEvent<HTMLTableRowElement>) => void;
28
29
  onError?: (error: unknown) => void;
29
30
  nullBeforeNumbers?: boolean;
@@ -61,6 +62,9 @@ export interface Column<T> {
61
62
  sub?: Column<T>[];
62
63
  group?: boolean;
63
64
  autogroup?: boolean;
65
+ resizeable?: boolean;
66
+ resizeMinWidth?: number;
67
+ resizeMaxWidth?: number;
64
68
  }
65
69
  export type DynamicInnerRefT = ReactList;
66
70
  export interface Settings {
@@ -84,6 +88,7 @@ export interface Settings {
84
88
  externalSort?: boolean;
85
89
  disableSortReset?: boolean;
86
90
  defaultOrder?: OrderType;
91
+ defaultResizeable?: boolean;
87
92
  highlightRows?: boolean;
88
93
  stripedRows?: boolean;
89
94
  headerMod?: 'multiline' | 'pre';
@@ -112,4 +117,5 @@ export interface SortedDataItem<T> {
112
117
  };
113
118
  }
114
119
  export type Comparator<T> = (l: SortedDataItem<T>, r: SortedDataItem<T>) => number;
120
+ export type HandleResize = (columnId: string, newWidth: number) => void;
115
121
  export {};
@@ -0,0 +1,9 @@
1
+ import type { Column, HandleResize } from './types';
2
+ export type ColumnWidthByName = Record<string, number>;
3
+ export type SaveColumnWidthByName = (data: ColumnWidthByName) => void;
4
+ export type GetSavedColumnWidthByName = () => ColumnWidthByName;
5
+ export declare function updateColumnsWidth<T>(columns: Column<T>[], columnsWidthSetup: ColumnWidthByName, defaultResizeable?: boolean): Column<T>[];
6
+ export declare function useTableResize({ saveSizes, getSizes, }: {
7
+ saveSizes: SaveColumnWidthByName;
8
+ getSizes: GetSavedColumnWidthByName;
9
+ }): [ColumnWidthByName, HandleResize];
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ export function updateColumnsWidth(columns, columnsWidthSetup, defaultResizeable = false) {
3
+ return columns.map((column) => {
4
+ var _a;
5
+ let sub;
6
+ if (column.sub) {
7
+ sub = updateColumnsWidth(column.sub, columnsWidthSetup, defaultResizeable);
8
+ }
9
+ const newWidth = (_a = columnsWidthSetup[column.name]) !== null && _a !== void 0 ? _a : column.width;
10
+ return Object.assign(Object.assign({}, column), { width: newWidth, sub });
11
+ });
12
+ }
13
+ export function useTableResize({ saveSizes, getSizes, }) {
14
+ const [tableColumnsWidthSetup, setTableColumnsWidth] = React.useState(() => {
15
+ return getSizes();
16
+ });
17
+ const handleSetupChange = React.useCallback((columnId, columnWidth) => {
18
+ setTableColumnsWidth((previousSetup) => {
19
+ const setup = Object.assign(Object.assign({}, previousSetup), { [columnId]: columnWidth });
20
+ saveSizes(setup);
21
+ return setup;
22
+ });
23
+ }, [saveSizes]);
24
+ return [tableColumnsWidthSetup, handleSetupChange];
25
+ }
@@ -1,6 +1,7 @@
1
1
  import { ColumnExtended, NameToOrderTypeMap, SortOrderWithSortColumns } from './DataTable';
2
2
  import { OrderType, Settings, SortOrder, SortedDataItem } from './types';
3
3
  export declare const cn: import("@bem-react/classname").ClassNameInitilizer;
4
+ export declare const b: import("@bem-react/classname").ClassNameFormatter;
4
5
  export type SlimColumn = {
5
6
  name: string;
6
7
  defaultOrder?: OrderType;
@@ -32,4 +33,6 @@ export declare function internalToExternalSortOrder(sortOrder: NameToOrderTypeMa
32
33
  columnId: string;
33
34
  order: OrderType;
34
35
  }[];
36
+ export declare function rafThrottle<Fn extends (...args: any[]) => any>(fn: Fn): (...args: Parameters<Fn>) => void;
37
+ export declare function calculateColumnWidth(newWidth: number, minWidth?: number, maxWidth?: number): number;
35
38
  export {};
@@ -2,6 +2,7 @@ import { __rest } from "tslib";
2
2
  import { withNaming } from '@bem-react/classname';
3
3
  import { ASCENDING, DESCENDING } from './constants';
4
4
  export const cn = withNaming({ e: '__', m: '_' });
5
+ export const b = cn('data-table');
5
6
  export function getSortOrder({ name: columnName, defaultOrder: columnDefaultOrder }, { sortOrder = {}, sortColumns = [] }, multiColumnSort = false, { defaultOrder: settingsDefaultOrder, disableSortReset, } = {}) {
6
7
  const defaultOrder = columnDefaultOrder || settingsDefaultOrder;
7
8
  const emptyResult = {
@@ -146,3 +147,24 @@ export function internalToExternalSortOrder(sortOrder) {
146
147
  };
147
148
  });
148
149
  }
150
+ // invoke passed function at most once per animation frame
151
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
152
+ export function rafThrottle(fn) {
153
+ let rafId = null;
154
+ let latestArgs;
155
+ return function throttled(...args) {
156
+ // call throttled function with latest args
157
+ latestArgs = args;
158
+ if (typeof rafId === 'number') {
159
+ return;
160
+ }
161
+ rafId = requestAnimationFrame(() => {
162
+ fn(...latestArgs);
163
+ rafId = null;
164
+ });
165
+ };
166
+ }
167
+ // 40px minWidth so sort icon won't overlap wrapped column title
168
+ export function calculateColumnWidth(newWidth, minWidth = 40, maxWidth = Infinity) {
169
+ return Math.max(minWidth, Math.min(newWidth, maxWidth));
170
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/react-data-table",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "DataTable component",
5
5
  "keywords": [
6
6
  "table"
@@ -51,7 +51,8 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@bem-react/classname": ">=1.6.0",
54
- "react-list": "^0.8.17"
54
+ "react-list": "^0.8.17",
55
+ "tslib": "^2.6.2"
55
56
  },
56
57
  "devDependencies": {
57
58
  "@bem-react/classname": "^1.6.0",
@@ -61,7 +62,7 @@
61
62
  "@gravity-ui/prettier-config": "^1.0.1",
62
63
  "@gravity-ui/stylelint-config": "^2.0.0",
63
64
  "@gravity-ui/tsconfig": "^1.0.0",
64
- "@gravity-ui/uikit": "^5.4.1",
65
+ "@gravity-ui/uikit": "^6.1.1",
65
66
  "@storybook/addon-actions": "^7.3.1",
66
67
  "@storybook/addon-essentials": "^7.3.1",
67
68
  "@storybook/addon-interactions": "^7.3.1",