diff --git a/README.md b/README.md index b353598..a5737a2 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ of the sort list. A simple Click sorts the table by this column only. Sorting or | 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) | | onSort | `Function` | | | Sort listener `([{columnId: , order: }]) => void`. Use it to save the sorting state and pass it to the `initialSortOrder` on page refresh. | | sortOrder | `Object` | | | Sorting in the format `{columnId: , order: }` or a list of such objects. | +| onResize | `Function` | | | Resize listener `(columnId: string, newWidth: number) => void`. Use it to save new column width. | | 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 | | onRowClick | `Function` | | | Click listener for a row `(row, index, event) => void` | | 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 | externalSort | `Boolean` | `false` | Disable data sorting on the component side. [Read more](#externalSort) | | disableSortReset | `Boolean` | `false` | Disable sort reset (enables the `none -> default <-> reversed default` sort scheme) | | defaultOrder | `DataTable.ASCENDING DataTable.DESCENDING` | `DataTable.ASCENDING` | Default sort order in the table | +| defaultResizeable | `Boolean` | `false` | Value that is used to determine whether column is resizeable, if no explicit value for column is set | | highlightRows | `Boolean` | | Highlighting table rows on hover | | stripedRows | `Boolean` | | Whether to color even/odd rows | | 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. The `name` field is the only mandatory field. The remaining fields are described below: -| Property | Type | Description | -| :------------ | :------------------------------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| name | `string` | Column name and default value for the `header` and `accessor` | -| header | `string` or `node` | Column header | -| headerTitle | `string` | Header title | -| className | `string` | Column class | -| width         | `string \| number` | Column width | -| align | `DataTable.LEFT DataTable.CENTER DataTable.RIGHT` | Text alignment in a cell | -| 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)` | -| 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. | -| 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. | -| 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. | -| onClick | `func` | Callback `(row, column) => ()`. The onClick listener on the cell. | -| sortable | `Boolean` | Whether the column is sortable. | -| defaultOrder | `DataTable.ASCENDING DataTable.DESCENDING` | A default sorting order in the column. | -| sortAccessor | `func` | Callback `(row) => (value)`. Use it if you want to sort by a value other than `value`, or if `value` is non-scalar. | -| sortAscending | `func` | Callback `({row, index, span?}, {row, index, span?}) => (sortValue)`. Use if you want to customize sorting completely. | -| sub | `array` | Nested columns | -| group | `Boolean` | Groups the same values into one cell, allowing you to create complex tables. [More details](#group) | -| autogroup | `Boolean` | Enables ascending sorting across all columns with group, resulting in autogrouping. Defaults to `true` | +| Property | Type | Description | +| :------------- | :------------------------------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | `string` | Column name and default value for the `header` and `accessor` | +| header | `string` or `node` | Column header | +| headerTitle | `string` | Header title | +| className | `string` | Column class | +| width         | `string \| number` | Column width | +| resizeable     | `boolean` | Determines whether column width can be changed. Applied only if `onResize` function is passed. | +| resizeMinWidth | `number` | Min column width for resize. | +| resizeMaxWidth | `number` | Max column width for resize. | +| align | `DataTable.LEFT DataTable.CENTER DataTable.RIGHT` | Text alignment in a cell | +| 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)` | +| 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. | +| 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. | +| 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. | +| onClick | `func` | Callback `(row, column) => ()`. The onClick listener on the cell. | +| sortable | `Boolean` | Whether the column is sortable. | +| defaultOrder | `DataTable.ASCENDING DataTable.DESCENDING` | A default sorting order in the column. | +| sortAccessor | `func` | Callback `(row) => (value)`. Use it if you want to sort by a value other than `value`, or if `value` is non-scalar. | +| sortAscending | `func` | Callback `({row, index, span?}, {row, index, span?}) => (sortValue)`. Use if you want to customize sorting completely. | +| sub | `array` | Nested columns | +| group | `Boolean` | Groups the same values into one cell, allowing you to create complex tables. [More details](#group) | +| autogroup | `Boolean` | Enables ascending sorting across all columns with group, resulting in autogrouping. Defaults to `true` | ### group diff --git a/src/.eslintrc b/src/.eslintrc index 06dc00d..6759a32 100644 --- a/src/.eslintrc +++ b/src/.eslintrc @@ -1,3 +1,6 @@ { - "extends": ["@gravity-ui/eslint-config", "@gravity-ui/eslint-config/prettier"] + "extends": ["@gravity-ui/eslint-config", "@gravity-ui/eslint-config/prettier"], + "rules": { + "no-implicit-globals": "off" + } } diff --git a/src/lib/DataTable.scss b/src/lib/DataTable.scss index 3b5fdaa..ef6ec50 100644 --- a/src/lib/DataTable.scss +++ b/src/lib/DataTable.scss @@ -67,6 +67,7 @@ $cell-border: 1px solid var(--data-table-border-color); } &__th { + box-sizing: border-box; font-weight: 500; cursor: default; 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); } &__td { + box-sizing: border-box; padding: var(--data-table-cell-vertical-padding) var(--data-table-cell-horizontal-padding); border: $cell-border; vertical-align: var(--data-table-cell-align); @@ -267,4 +269,22 @@ $cell-border: 1px solid var(--data-table-border-color); --data-table-color-hover-area: #ffeba0; --data-table-color-footer-area: var(--data-table-color-base); } + + &__resize-handler { + visibility: hidden; + position: absolute; + right: 0; + top: 0; + cursor: col-resize; + width: 6px; + height: 100%; + background-color: var(--g-color-base-generic); + + &_resizing { + visibility: visible; + } + } + &__th:hover > &__resize-handler { + visibility: visible; + } } diff --git a/src/lib/DataTable.tsx b/src/lib/DataTable.tsx index 7f0fcd7..02ddf2d 100644 --- a/src/lib/DataTable.tsx +++ b/src/lib/DataTable.tsx @@ -2,21 +2,20 @@ import * as React from 'react'; import ReactList from 'react-list'; import './DataTable.scss'; +import {ResizeHandler} from './ResizeHandler'; import {ASCENDING, CENTER, DESCENDING, FIXED, INDEX_COLUMN, LEFT, MOVING, RIGHT} from './constants'; import {positionStickySupported} from './featureSupport'; import {HeightObserver} from './height-observer'; import {Column, DataTableProps, HeadPosition, OrderType, Settings, SortedDataItem} from './types'; import { SlimColumn, - cn, + b, externalToInternalSortOrder, getSortOrder, getSortedData, internalToExternalSortOrder, } from './util'; -const b = cn('data-table'); - const ICON_ASC = ( @@ -122,19 +121,27 @@ class TableRow extends React.PureComponent> { } const value = column._getValue(row); + + let style = column.customStyle({ + row, + index, + name: column.name, + header: false, + footer, + headerData, + }); + + // Fixed cell width for resizeable columns for proper content wrap + if (column.resizeable) { + style = {...style, width: column.width, maxWidth: column.width}; + } + return ( { displayIndices?: boolean; onSort?: TableProps['onSort']; + onResize?: TableProps['onResize']; onColumnsUpdated?: (widths: number[]) => void; renderedDataRows?: React.ReactNode; @@ -169,7 +177,7 @@ interface TableHeadProps { class TableHead extends React.Component> { _dataRowsRef: HTMLTableSectionElement | null = null; dataRowsHeightObserver?: HeightObserver; - renderedColumns: HTMLTableHeaderCellElement[] = []; + renderedColumns: HTMLTableCellElement[] = []; componentDidMount() { this._calculateColumnsWidth(); @@ -238,6 +246,7 @@ class TableHead extends React.Component> { : undefined; } renderHeadCell = (headCell: HeadCellsType) => { + const {onResize} = this.props; const {column, rowSpan, colSpan} = headCell; const { sortable = false, @@ -246,26 +255,48 @@ class TableHead extends React.Component> { index, columnIndex, align, + name, + width, + resizeable, + resizeMinWidth, + resizeMaxWidth, } = column; const {headerTitle = (typeof header === 'string' && header) || undefined} = column; + let style = column.customStyle?.({header: true, name}); + + // Fixed cell width for resizeable columns for proper content wrap + if (resizeable) { + style = {...style, width, maxWidth: width}; + } + return (
{header} {}
+ {resizeable && ( + + )} ); }; @@ -289,6 +320,12 @@ class TableHead extends React.Component> { this.renderedColumns[index] = node; }; }; + _getRenderedColumn = (index?: number) => { + if (index) { + return this.renderedColumns[index]; + } + return undefined; + }; } interface StickyHeadProps { @@ -298,6 +335,7 @@ interface StickyHeadProps { mode: HeadPositionInner; displayIndices?: Settings['displayIndices']; onSort?: TableProps['onSort']; + onResize?: TableProps['onResize']; top: number; renderedDataRows: React.ReactNode; @@ -534,6 +572,7 @@ interface TableProps { rowKey: (row: T, index: number) => string | number; startIndex: DataTableProps['startIndex']; onSort: DataTableView['onSort']; + onResize: DataTableProps['onResize']; renderEmptyRow: unknown; nullBeforeNumbers?: boolean; getColSpansOfRow?: ( @@ -673,7 +712,7 @@ class Table extends React.PureComponent, TableState> { } renderHead() { - const {columns, onSort} = this.props; + const {columns, onSort, onResize} = this.props; const {displayIndices} = this.props.settings; const rows = this.renderHeaderRows(); return ( @@ -682,13 +721,14 @@ class Table extends React.PureComponent, TableState> { {...columns} displayIndices={Boolean(displayIndices)} onSort={onSort} + onResize={onResize} onColumnsUpdated={this._onColumnsUpdated} renderedDataRows={rows} /> ); } renderStickyHead() { - const {columns, onSort} = this.props; + const {columns, onSort, onResize} = this.props; const {displayIndices, stickyTop, stickyHead} = this.props.settings; const top = stickyTop === 'auto' && this._body && this._body.parentNode @@ -703,6 +743,7 @@ class Table extends React.PureComponent, TableState> { {...columns} displayIndices={displayIndices} onSort={onSort} + onResize={onResize} renderedDataRows={rows} onDataRowsHeightChange={this.onMovingHeaderDataRowsHeightChange} /> @@ -916,6 +957,7 @@ class DataTableView extends React.Component, DataTableViewS sortable: true, externalSort: false, defaultOrder: ASCENDING as OrderType, + defaultResizeable: false, }, rowKey: (row: any, index: number) => Object.prototype.hasOwnProperty.call(row, 'id') ? row.id : index, @@ -971,6 +1013,7 @@ class DataTableView extends React.Component, DataTableViewS return startIndex + index; }, sortable: false, + resizeable: false, width: 20 + Math.ceil(Math.log10(lastIndex)) * 10, }; } @@ -1006,6 +1049,7 @@ class DataTableView extends React.Component, DataTableViewS rowClassName, rowKey, onRowClick, + onResize, theme, renderEmptyRow, nullBeforeNumbers, @@ -1055,6 +1099,7 @@ class DataTableView extends React.Component, DataTableViewS )} footerData={footerData} onSort={this.onSort} + onResize={onResize} /> ); } @@ -1079,6 +1124,7 @@ class DataTableView extends React.Component, DataTableViewS }; getColumn = (column: Column, columnIndex: number) => { + const {onResize} = this.props; const {settings} = this.state; const {defaultOrder} = settings; const {sortOrder = {}, sortColumns, indexColumn} = this.state; @@ -1098,6 +1144,8 @@ class DataTableView extends React.Component, DataTableViewS const {sortAccessor, onClick} = column; const _className = b('td', {align}, column.className); + const resizeable = (column.resizeable ?? settings.defaultResizeable) && Boolean(onResize); + const _getValue = typeof accessor === 'function' ? (row: T) => accessor(row) @@ -1137,6 +1185,7 @@ class DataTableView extends React.Component, DataTableViewS dataColumn: true, defaultOrder, ...column, + resizeable, sortable: sortable && isSortEnabled, _className, _getValue, diff --git a/src/lib/ResizeHandler.tsx b/src/lib/ResizeHandler.tsx new file mode 100644 index 0000000..726e641 --- /dev/null +++ b/src/lib/ResizeHandler.tsx @@ -0,0 +1,109 @@ +import React from 'react'; + +import {b, calculateColumnWidth, rafThrottle} from './util'; + +interface ResizeHandlerProps { + getColumn: (index?: number) => HTMLTableCellElement | undefined; + columnIndex?: number; + columnId: string; + maxWidth?: number; + minWidth?: number; + onResize?: (columnId: string, width: number) => void; +} + +export function ResizeHandler({ + getColumn, + columnIndex, + columnId, + minWidth, + maxWidth, + onResize, +}: ResizeHandlerProps) { + const elementRef = React.useRef(null); + + const [resizing, setResizing] = React.useState(false); + + React.useEffect(() => { + const element = elementRef.current; + + if (!element) { + return undefined; + } + + let mouseXPosition: number | undefined; + let initialColumnWidth: number | undefined; + let currentColumnWidth: number | undefined; + + const onMouseMove = rafThrottle((e: MouseEvent) => { + restrictMouseEvent(e); + + if (typeof mouseXPosition !== 'number' || typeof initialColumnWidth !== 'number') { + return; + } + + const xDiff = e.clientX - mouseXPosition; + + const newWidth = calculateColumnWidth(initialColumnWidth + xDiff, minWidth, maxWidth); + + if (newWidth === currentColumnWidth) { + return; + } + + currentColumnWidth = newWidth; + + onResize?.(columnId, currentColumnWidth); + }); + + const onMouseUp = (e: MouseEvent) => { + restrictMouseEvent(e); + + if (currentColumnWidth !== undefined) { + onResize?.(columnId, currentColumnWidth); + } + + setResizing(false); + mouseXPosition = undefined; + + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseup', onMouseUp); + }; + + const onMouseDown = (e: MouseEvent) => { + initialColumnWidth = getColumn(columnIndex)?.getBoundingClientRect().width; + + restrictMouseEvent(e); + + mouseXPosition = e.clientX; + + setResizing(true); + + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + }; + + element.addEventListener('mousedown', onMouseDown); + + return () => { + element.removeEventListener('mousedown', onMouseDown); + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseup', onMouseUp); + }; + }, [columnId, onResize, minWidth, maxWidth, getColumn, columnIndex]); + + return ( + restrictMouseEvent(e)} + /> + ); +} + +// Prevent sort trigger and text selection on resize +function restrictMouseEvent< + T extends {preventDefault: VoidFunction; stopPropagation: VoidFunction}, +>(e: T) { + e.preventDefault(); + e.stopPropagation(); +} diff --git a/src/lib/index.ts b/src/lib/index.ts index 58a2644..763c39b 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1,6 +1,7 @@ export {INDEX_COLUMN} from './constants'; export * from './types'; +export * from './useTableResize'; import DataTable from './DataTable'; export default DataTable; diff --git a/src/lib/types.ts b/src/lib/types.ts index 2aa7096..9a0942c 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -40,6 +40,8 @@ export interface DataTableProps { theme: THEMES | string; onSort?: (sortOrder: DataTableProps['initialSortOrder']) => void; + onResize?: HandleResize; + onRowClick?: (row: T, index: number, event: React.MouseEvent) => void; onError?: (error: unknown) => void; @@ -82,6 +84,10 @@ export interface Column { sub?: Column[]; group?: boolean; autogroup?: boolean; + + resizeable?: boolean; + resizeMinWidth?: number; + resizeMaxWidth?: number; } export type DynamicInnerRefT = ReactList; @@ -109,6 +115,7 @@ export interface Settings { externalSort?: boolean; disableSortReset?: boolean; defaultOrder?: OrderType; + defaultResizeable?: boolean; highlightRows?: boolean; stripedRows?: boolean; headerMod?: 'multiline' | 'pre'; @@ -142,3 +149,5 @@ export interface SortedDataItem { } export type Comparator = (l: SortedDataItem, r: SortedDataItem) => number; + +export type HandleResize = (columnId: string, newWidth: number) => void; diff --git a/src/lib/useTableResize.ts b/src/lib/useTableResize.ts new file mode 100644 index 0000000..96ae144 --- /dev/null +++ b/src/lib/useTableResize.ts @@ -0,0 +1,53 @@ +import React from 'react'; + +import type {Column, HandleResize} from './types'; + +export type ColumnWidthByName = Record; +export type SaveColumnWidthByName = (data: ColumnWidthByName) => void; +export type GetSavedColumnWidthByName = () => ColumnWidthByName; + +export function updateColumnsWidth( + columns: Column[], + columnsWidthSetup: ColumnWidthByName, + defaultResizeable = false, +): Column[] { + return columns.map((column) => { + let sub: Column[] | undefined; + + if (column.sub) { + sub = updateColumnsWidth(column.sub, columnsWidthSetup, defaultResizeable); + } + + const newWidth = columnsWidthSetup[column.name] ?? column.width; + + return {...column, width: newWidth, sub}; + }); +} + +export function useTableResize({ + saveSizes, + getSizes, +}: { + saveSizes: SaveColumnWidthByName; + getSizes: GetSavedColumnWidthByName; +}): [ColumnWidthByName, HandleResize] { + const [tableColumnsWidthSetup, setTableColumnsWidth] = React.useState(() => { + return getSizes(); + }); + + const handleSetupChange: HandleResize = React.useCallback( + (columnId, columnWidth) => { + setTableColumnsWidth((previousSetup) => { + const setup = { + ...previousSetup, + [columnId]: columnWidth, + }; + saveSizes(setup); + return setup; + }); + }, + [saveSizes], + ); + + return [tableColumnsWidthSetup, handleSetupChange]; +} diff --git a/src/lib/util.ts b/src/lib/util.ts index 7a4fda8..ffd94bf 100644 --- a/src/lib/util.ts +++ b/src/lib/util.ts @@ -5,6 +5,8 @@ import {ASCENDING, DESCENDING} from './constants'; import {Comparator, OrderType, Settings, SortOrder, SortedDataItem} from './types'; export const cn = withNaming({e: '__', m: '_'}); +export const b = cn('data-table'); + export type SlimColumn = {name: string; defaultOrder?: OrderType}; export function getSortOrder( @@ -202,3 +204,29 @@ export function internalToExternalSortOrder(sortOrder: NameToOrderTypeMap) { }; }); } + +// invoke passed function at most once per animation frame +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function rafThrottle any>(fn: Fn) { + let rafId: number | null = null; + let latestArgs: Parameters; + + return function throttled(...args: Parameters) { + // call throttled function with latest args + latestArgs = args; + + if (typeof rafId === 'number') { + return; + } + + rafId = requestAnimationFrame(() => { + fn(...latestArgs); + rafId = null; + }); + }; +} + +// 40px minWidth so sort icon won't overlap wrapped column title +export function calculateColumnWidth(newWidth: number, minWidth = 40, maxWidth = Infinity) { + return Math.max(minWidth, Math.min(newWidth, maxWidth)); +} diff --git a/src/stories/Resizeable/Resizeable.scss b/src/stories/Resizeable/Resizeable.scss new file mode 100644 index 0000000..7db7353 --- /dev/null +++ b/src/stories/Resizeable/Resizeable.scss @@ -0,0 +1,6 @@ +.resizeable-datable-example { + &__table { + display: flex; + width: max-content; + } +} diff --git a/src/stories/Resizeable/Resizeable.tsx b/src/stories/Resizeable/Resizeable.tsx new file mode 100644 index 0000000..4f57054 --- /dev/null +++ b/src/stories/Resizeable/Resizeable.tsx @@ -0,0 +1,137 @@ +import React from 'react'; + +import DataTable from '../../lib/DataTable'; +import {YCLOUD_THEME} from '../../lib/constants'; +import type {Column, DataTableProps} from '../../lib/types'; +import { + GetSavedColumnWidthByName, + SaveColumnWidthByName, + updateColumnsWidth, + useTableResize, +} from '../../lib/useTableResize'; +import {cn} from '../../lib/util'; + +import './Resizeable.scss'; + +const b = cn('resizeable-datable-example'); + +const data = Array(5) + .join('|') + .split('|') + .map((_empty, index) => { + return { + id: index + 1, + number: index + 123456789101112, + col1: index % 23, + col2: index % 13, + string: `some_very_long_line_${index + 1}`, + complex: { + value: index + 5, + }, + bar: Math.pow(index * 20 - 250, 2) / 500, + something1: index + 123456789101112, + something2: index + 123456789101112, + }; + }); + +type RowType = typeof data[number]; + +const columns: Column[] = [ + { + name: 'number', + }, + { + name: 'col1', + }, + { + name: 'col2', + }, + { + name: 'string', + width: 100, + resizeMaxWidth: 200, + }, + { + name: 'complex', + render: ({value}) => JSON.stringify(value, null, 2), + defaultOrder: DataTable.DESCENDING, + sortAscending: (item1, item2) => { + const value1 = item1.row.complex.value; + const value2 = item2.row.complex.value; + return (value1 % 2) - (value2 % 2) || value1 - value2; + }, + width: 150, + resizeMinWidth: 100, + }, + { + name: 'bar', + render: ({value}) => ( +
+ ), + resizeable: false, + }, + { + name: 'multiLevel', + sub: [ + { + name: 'sub1-1', + customStyle: () => ({backgroundColor: 'wheat', fontSize: '1.2em'}), + sub: [ + { + name: 'sub2-1', + resizeable: false, + }, + { + name: 'sub2-2', + sub: [ + { + name: 'sub3-1', + accessor: 'something1', + }, + ], + }, + ], + }, + { + name: 'something2', + }, + ], + }, +]; + +const COLUMNS_WIDTH_LOCAL_STORAGE_KEY = 'resizeableTable'; + +export function ResizeableTable({theme = YCLOUD_THEME, ...props}: DataTableProps) { + const getColumnsWidth: GetSavedColumnWidthByName = React.useCallback(() => { + try { + const rawData = localStorage.getItem(COLUMNS_WIDTH_LOCAL_STORAGE_KEY); + return rawData ? JSON.parse(rawData) : {}; + } catch { + return {}; + } + }, []); + + const saveColumnsWidth: SaveColumnWidthByName = React.useCallback((value) => { + localStorage.setItem(COLUMNS_WIDTH_LOCAL_STORAGE_KEY, JSON.stringify(value)); + }, []); + + const [tableColumnsWidthSetup, setTableColumnsWidth] = useTableResize({ + getSizes: getColumnsWidth, + saveSizes: saveColumnsWidth, + }); + + const updatedColumns = updateColumnsWidth(columns, tableColumnsWidthSetup); + + return ( +
+ +
+ ); +} diff --git a/src/stories/index.stories.tsx b/src/stories/index.stories.tsx index 71bde3f..08a6d90 100644 --- a/src/stories/index.stories.tsx +++ b/src/stories/index.stories.tsx @@ -4,19 +4,14 @@ import React from 'react'; import {FIXED, LEGACY_THEME, MOVING, YCLOUD_THEME} from '../lib/constants'; import ExampleDT100, {defaultProps} from './Example/Example'; +import {ResizeableTable} from './Resizeable/Resizeable'; export default { title: 'React Data Table', component: ExampleDT100, } as Meta; -//const Template = (args: ExampleProps) => ; - -const Template: StoryFn = (args) => ; - -export const Default = Template.bind({}); -Default.args = defaultProps; -Default.argTypes = { +const templateArgTypes = { stickyHeadValues: { options: [MOVING, FIXED], control: {type: 'select'}, @@ -29,3 +24,15 @@ Default.argTypes = { control: {type: 'select'}, }, }; + +const Template: StoryFn = (args) => ; + +export const Default = Template.bind({}); +Default.args = defaultProps; +Default.argTypes = templateArgTypes; + +const ResizeableTemplate: StoryFn = (args) => ; + +export const Resizeable = ResizeableTemplate.bind({}); +Default.args = {theme: YCLOUD_THEME}; +Default.argTypes = templateArgTypes;