From 8b527024430d7619ed83eb2a4a0ff1f4bfbba469 Mon Sep 17 00:00:00 2001 From: Dmitry Artemov Date: Thu, 11 Jul 2024 16:22:23 +0200 Subject: [PATCH] feat(Table): pass table and rowVirtualizer instances instead of options and hoc (#9) --- .storybook/preview.tsx | 3 - README.md | 357 ++++++++++++------ src/components/BaseRow/BaseRow.tsx | 57 ++- src/components/Cell/Cell.tsx | 16 +- .../DragHandle/DragHandle.classname.ts | 2 +- src/components/DraggableRow/DraggableRow.tsx | 20 +- .../DraggableRowMarker.classname.ts | 2 +- src/components/HeaderCell/HeaderCell.tsx | 6 +- src/components/HeaderRow/HeaderRow.tsx | 3 - .../ReorderingProvider/ReorderingProvider.tsx | 19 +- src/components/Row/Row.tsx | 34 -- src/components/Row/index.ts | 1 - .../SortIndicator/SortIndicator.classname.ts | 2 +- src/components/SortableList/SortableList.tsx | 25 +- src/components/SortableList/types.ts | 2 +- .../SortableListDndContext.tsx | 4 +- src/components/Table/Table.classname.ts | 2 +- src/components/Table/Table.scss | 48 ++- src/components/Table/Table.tsx | 327 +++++----------- src/components/TableContext/TableContext.ts | 4 +- .../TableContextProvider.tsx | 9 +- src/components/VirtualRow/VirtualRow.tsx | 59 --- src/components/VirtualRow/index.ts | 1 - .../VirtualizationContainer.classname.ts | 3 - .../VirtualizationContainer.scss | 55 --- .../VirtualizationContainer.tsx | 35 -- .../VirtualizationContainer/index.ts | 1 - .../VirtualizationContext.ts | 13 - src/components/VirtualizationContext/index.ts | 1 - .../VirtualizationProvider.tsx | 61 --- .../VirtualizationProvider/index.ts | 1 - .../WindowVirtualizationProvider.tsx | 53 --- .../WindowVirtualizationProvider/index.ts | 1 - src/components/__stories__/DefaultDemo.tsx | 16 + src/components/__stories__/Grid2.stories.tsx | 215 ----------- .../__stories__/GridDemo.classname.ts | 3 - src/components/__stories__/GridDemo.scss | 10 - src/components/__stories__/GroupingDemo.tsx | 42 +-- src/components/__stories__/GroupingDemo2.tsx | 50 +-- .../__stories__/GroupingWithSelectionDemo.tsx | 65 ++-- .../__stories__/HeaderGroupsDemo.classname.ts | 3 + .../__stories__/HeaderGroupsDemo.scss | 8 + .../__stories__/HeaderGroupsDemo.tsx | 58 +++ src/components/__stories__/ReorderingDemo.tsx | 40 +- .../__stories__/ReorderingTreeDemo.tsx | 53 +-- .../ReorderingWithVirtualizationDemo.tsx | 68 ++-- src/components/__stories__/ResizingDemo.tsx | 18 + src/components/__stories__/SortingDemo.tsx | 24 +- src/components/__stories__/Table.stories.tsx | 75 ++++ src/components/__stories__/TreeDemo.tsx | 33 +- .../__stories__/TreeWithGroupsDemo.tsx | 42 +-- .../__stories__/VirtualizationDemo.tsx | 35 +- .../__stories__/WindowVirtualizationDemo.tsx | 31 +- .../__stories__/WithSelectionDemo.tsx | 28 +- .../__stories__/WithoutHeaderDemo.tsx | 16 + .../__stories__/constants/columns.tsx | 5 +- .../__stories__/constants/grouping.ts | 5 +- src/components/__stories__/constants/tree.tsx | 9 +- .../hooks/useTreeDataReordering.ts | 4 +- src/components/__stories__/utils.ts | 4 +- src/components/index.ts | 6 - src/components/utils/flattenTableData.ts | 35 -- .../utils/getVirtualRangeExtractor.ts | 17 - .../utils/renderDefaultGroupHeader.tsx | 28 -- .../utils/renderDefaultSortIndicator.tsx | 18 - src/constants/defaultSelectionColumn.tsx | 1 + src/demo/DocsDecorator/DocsDecorator.tsx | 2 +- src/hocs/index.ts | 2 - src/hocs/withTableReorder.tsx | 58 +-- src/hocs/withTableVirtualization.tsx | 38 -- src/hocs/withTableWindowVirtualization.tsx | 38 -- src/hooks/index.ts | 6 +- src/hooks/useColumns.ts | 33 -- src/hooks/useExpanded.ts | 43 --- src/hooks/useRowVirtualizer.ts | 11 + src/hooks/useSelection.ts | 40 -- src/hooks/useSortableList.ts | 87 +++-- src/hooks/useTable.ts | 45 +++ src/hooks/useWindowRowVirtualizer.ts | 9 + src/index.ts | 31 +- .../utils/__tests__/toDataAttributes.test.ts | 0 src/{components => }/utils/cn.ts | 0 src/utils/getVirtualRowRangeExtractor.ts | 16 + src/utils/index.ts | 5 + src/utils/renderDefaultGroupHeader.tsx | 23 ++ src/utils/renderDefaultSortIndicator.tsx | 13 + .../utils/toDataAttributes.ts | 10 +- 87 files changed, 1199 insertions(+), 1603 deletions(-) delete mode 100644 src/components/Row/Row.tsx delete mode 100644 src/components/Row/index.ts delete mode 100644 src/components/VirtualRow/VirtualRow.tsx delete mode 100644 src/components/VirtualRow/index.ts delete mode 100644 src/components/VirtualizationContainer/VirtualizationContainer.classname.ts delete mode 100644 src/components/VirtualizationContainer/VirtualizationContainer.scss delete mode 100644 src/components/VirtualizationContainer/VirtualizationContainer.tsx delete mode 100644 src/components/VirtualizationContainer/index.ts delete mode 100644 src/components/VirtualizationContext/VirtualizationContext.ts delete mode 100644 src/components/VirtualizationContext/index.ts delete mode 100644 src/components/VirtualizationProvider/VirtualizationProvider.tsx delete mode 100644 src/components/VirtualizationProvider/index.ts delete mode 100644 src/components/WindowVirtualizationProvider/WindowVirtualizationProvider.tsx delete mode 100644 src/components/WindowVirtualizationProvider/index.ts create mode 100644 src/components/__stories__/DefaultDemo.tsx delete mode 100644 src/components/__stories__/Grid2.stories.tsx delete mode 100644 src/components/__stories__/GridDemo.classname.ts delete mode 100644 src/components/__stories__/GridDemo.scss create mode 100644 src/components/__stories__/HeaderGroupsDemo.classname.ts create mode 100644 src/components/__stories__/HeaderGroupsDemo.scss create mode 100644 src/components/__stories__/HeaderGroupsDemo.tsx create mode 100644 src/components/__stories__/ResizingDemo.tsx create mode 100644 src/components/__stories__/Table.stories.tsx create mode 100644 src/components/__stories__/WithoutHeaderDemo.tsx delete mode 100644 src/components/utils/flattenTableData.ts delete mode 100644 src/components/utils/getVirtualRangeExtractor.ts delete mode 100644 src/components/utils/renderDefaultGroupHeader.tsx delete mode 100644 src/components/utils/renderDefaultSortIndicator.tsx delete mode 100644 src/hocs/withTableVirtualization.tsx delete mode 100644 src/hocs/withTableWindowVirtualization.tsx delete mode 100644 src/hooks/useColumns.ts delete mode 100644 src/hooks/useExpanded.ts create mode 100644 src/hooks/useRowVirtualizer.ts delete mode 100644 src/hooks/useSelection.ts create mode 100644 src/hooks/useTable.ts create mode 100644 src/hooks/useWindowRowVirtualizer.ts rename src/{components => }/utils/__tests__/toDataAttributes.test.ts (100%) rename src/{components => }/utils/cn.ts (100%) create mode 100644 src/utils/getVirtualRowRangeExtractor.ts create mode 100644 src/utils/index.ts create mode 100644 src/utils/renderDefaultGroupHeader.tsx create mode 100644 src/utils/renderDefaultSortIndicator.tsx rename src/{components => }/utils/toDataAttributes.ts (83%) diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 3a25a31..978c4eb 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -66,9 +66,6 @@ const preview: Preview = { }, }, parameters: { - actions: { - argTypesRegex: '^on[A-Z].*', - }, docs: { theme: themes.light, container: DocsDecorator, diff --git a/README.md b/README.md index caa9d4b..effd33d 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,9 @@ npm install --save @gravity-ui/table ## Usage ```tsx -import {Table, TableProps} from '@gravity-ui/table'; +import React from 'react'; +import {Table, useTable} from '@gravity-ui/table'; +import type {ColumnDef} from '@tanstack/react-table'; interface Person { id: string; @@ -17,33 +19,57 @@ interface Person { age: number; } -const data: Person[] = [ - { - id: 'name', - name: 'John', - age: 23, - }, - { - id: 'age', - name: 'Michael', - age: 27, - }, -]; - -const columns: TableProps['columns'] = [ +const columns: ColumnDef[] = [ {accessorKey: 'name', header: 'Name', size: 100}, {accessorKey: 'age', header: 'Age', size: 100}, ]; - person.id} />; +const data: Person[] = [ + {id: 'name', name: 'John', age: 23}, + {id: 'age', name: 'Michael', age: 27}, +]; + +const BasicExample = () => { + const table = useTable({ + columns, + data, + }); + + return
; +}; ``` -### With selection +### Row selection ```tsx -const [selectedIds, setSelectedIds] = React.useState([]); +import {defaultSelectionColumn} from '@gravity-ui/table'; +import type {RowSelectionState} from '@tanstack/react-table'; + +const columns: ColumnDef[] = [ + defaultSelectionColumn as ColumnDef, + // ...other columns +]; -
; +const data: Person[] = [ + /* ... */ +]; + +const RowSelectionExample = () => { + const [rowSelection, setRowSelection] = React.useState({}); + + const table = useTable({ + columns, + data, + enableRowSelection: true, + enableMultiRowSelection: true, + onRowSelectionChange: setRowSelection, + state: { + rowSelection, + }, + }); + + return
; +}; ``` ### Sorting @@ -51,17 +77,53 @@ const [selectedIds, setSelectedIds] = React.useState([]); Learn about the column properties in the react-table [docs](https://tanstack.com/table/v8/docs/guide/sorting) ```tsx -const [sorting, setSorting] = React.useState['sorting']>>([]); +import type {SortingState} from '@tanstack/react-table'; -
; +const columns: ColumnDef[] = [ + /* ... */ +]; + +const data: Person[] = [ + /* ... */ +]; + +const SortingExample = () => { + const [sorting, setSorting] = React.useState([]); + + const table = useTable({ + columns, + data, + enableSorting: true, + getRowId: (item) => item.id, + onSortingChange: setSorting, + state: { + sorting, + }, + }); + + return
; +}; ``` -If you want to sort the elements manually pass `manualSorting` property. +If you want to sort the elements manually pass `manualSorting` property: + +```tsx +const table = useTable({ + // ... + manualSorting: true, +}); +``` ### Grouping ```tsx -type ItemType = PersonGroup | Person; +import type {ExpandedState, Row} from '@tanstack/react-table'; + +interface Person { + id: string; + name: string; + age: number; +} interface PersonGroup { id: string; @@ -69,12 +131,14 @@ interface PersonGroup { items: Person[]; } -const columns: TableProps['columns'] = [ +type Item = PersonGroup | Person; + +const columns: ColumnDef[] = [ {accessorKey: 'name', header: 'Name', size: 200}, {accessorKey: 'age', header: 'Age', size: 100}, ]; -const data: ItemType[] = [ +const data: Item[] = [ { id: 'friends', name: 'Friends', @@ -93,70 +157,78 @@ const data: ItemType[] = [ }, ]; -const getSubRows = (item: ItemType) => ('items' in item ? item.items : undefined); -const getGroupTitle: TableProps['getGroupTitle'] = (row) => row.getValue('name'); -const getRowId = (item: ItemType) => item.id; +const getGroupTitle = (row: Row) => row.getValue('name'); -const [expandedIds, setExpandedIds] = React.useState([]); +const GroupingExample = () => { + const [expanded, setExpanded] = React.useState({}); - - data={data} - columns={columns} - getSubRows={getSubRows} - getGroupTitle={getGroupTitle} - getRowId={getRowId} - expandedIds={expandedIds} - onExpandedChange={setExpandedIds} - {...props} -/>; + const table = useTable({ + columns, + data, + enableExpanding: true, + getSubRows: (item) => ('items' in item ? item.items : undefined), + onExpandedChange: setExpanded, + state: { + expanded, + }, + }); + + return
; +}; ``` ### Reordering ```tsx -const TableWithReordering = withTableReorder(Table); +import {defaultDragHandleColumn, withTableReorder, SortableListDragResult} from '@gravity-ui/table'; -const [data, setData] = React.useState(props.data); - -const handleReorder = React.useCallback( - ({draggedItemKey, baseItemKey}: SortableListDragResult) => { - setData((data) => { - const dataClone = data.slice(); - const index = dataClone.findIndex((item) => getRowId(item) === draggedItemKey); - - if (index >= 0) { - const dragged = dataClone.splice(index, 1)[0]!; - const insertIndex = dataClone.findIndex((value) => getRowId(value) === baseItemKey); +const TableWithReordering = withTableReorder(Table); - if (insertIndex >= 0) { - dataClone.splice(insertIndex + 1, 0, dragged); - } else { - dataClone.unshift(dragged); - } - } +const columns: ColumnDef[] = [ + defaultDragHandleColumn, + // ...other columns +]; - return dataClone; - }); - }, - [getRowId], -); +const data: Person[] = [ + /* ... */ +]; - data={data} onReorder={handleReorder} {...props} />; +const ReorderingExample = () => { + const table = useTable({ + columns, + data, + getRowId: (item) => item.id, + }); + + const handleReorder = React.useCallback( + ({ + draggedItemKey, + targetItemKey, + baseItemKey, + baseNextItemKey, + nestingEnabled, + nextChild, + pullFromParent, + }: SortableListDragResult) => { + // ... + }, + [], + ); + + return ; +}; ``` -Or use reordering provider +Or use `ReorderingProvider`: ```tsx -
+
``` @@ -165,34 +237,62 @@ Or use reordering provider Use if you want to use grid container as the scroll element (if you want to use window see window virtualization section). ```tsx -const TableWithVirtualization = withTableVirtualization(Table); +import {useRowVirtualizer} from '@gravity-ui/table'; + +const columns: ColumnDef[] = [ + /* ... */ +]; -const rowHeight = 20; -const estimateRowSize = () => rowHeight; +const data: Person[] = [ + /* ... */ +]; - - estimateRowSize={estimateRowSize} - overscanRowCount={5} - containerHeight="90vh" - {...props} -/>; +const VirtualizationExample = () => { + const table = useTable({ + columns, + data, + getRowId: (item) => item.id, + }); + + const containerRef = React.useRef(null); + + const rowVirtualizer = useRowVirtualizer({ + count: table.getRowModel().rows.length, + estimateSize: () => 20, + overscan: 5, + getScrollElement: () => containerRef.current, + }); + + return ( +
+
+ + ); +}; ``` -Or use virtualization provider +If you use virtualization with reordering feature you also need to pass `rangeExtractor` option: ```tsx -const rowHeight = 20; -const estimateRowSize = () => rowHeight; - - -
-; +import {getVirtualRowRangeExtractor} from '@gravity-ui/table'; + +// ... + +const tableRef = React.useRef(null); + +const rowVirtualizer = useRowVirtualizer({ + // ... + rangeExtractor: getVirtualRowRangeExtractor(tableRef.current), +}); + +return ( + +); ``` ### Window virtualization @@ -200,45 +300,54 @@ const estimateRowSize = () => rowHeight; Use if you want to use window as the scroll element ```tsx -const TableWithVirtualization = withTableWindowVirtualization(Table); - -const rowHeight = 20; -const estimateRowSize = () => rowHeight; +import {useWindowRowVirtualizer} from '@gravity-ui/table'; - - estimateRowSize={estimateRowSize} - overscanRowCount={5} - {...props} -/>; -``` +const columns: ColumnDef[] = [ + /* ... */ +]; -Or use window virtualization provider +const data: Person[] = [ + /* ... */ +]; -```tsx -const rowHeight = 20; -const estimateRowSize = () => rowHeight; - - -
-; +const WindowVirtualizationExample = () => { + const table = useTable({ + columns, + data, + getRowId: (item) => item.id, + }); + + const rowVirtualizer = useWindowRowVirtualizer({ + count: table.getRowModel().rows.length, + estimateSize: () => 20, + overscan: 5, + }); + + return
; +}; ``` ### Resizing ```tsx -
+const columns: ColumnDef[] = [ + /* ... */ +]; + +const data: Person[] = [ + /* ... */ +]; + +const ResizingDemo = () => { + const table = useTable({ + columns, + data, + enableColumnResizing: true, + columnResizeMode: 'onChange', + }); + + return
; +}; ``` Learn more about the table and the column resizing properties in the react-table [docs](https://tanstack.com/table/v8/docs/api/features/column-sizing) diff --git a/src/components/BaseRow/BaseRow.tsx b/src/components/BaseRow/BaseRow.tsx index 751fb32..4d1d7f7 100644 --- a/src/components/BaseRow/BaseRow.tsx +++ b/src/components/BaseRow/BaseRow.tsx @@ -1,18 +1,20 @@ import React from 'react'; +import {useForkRef} from '@gravity-ui/uikit'; import type {Cell, Row} from '@tanstack/react-table'; +import type {VirtualItem, Virtualizer} from '@tanstack/react-virtual'; +import {renderDefaultGroupHeader} from '../../utils'; import {b} from '../Table/Table.classname'; -import {renderDefaultGroupHeader} from '../utils/renderDefaultGroupHeader'; -export interface BaseRowProps { +export interface BaseRowProps { cellClassName?: string; checkIsGroupRow?: (row: Row) => boolean; children?: React.ReactNode; className?: string; columnsCount: number; getGroupTitle?: (row: Row) => React.ReactNode; - getRowDataAttributes?: ( + getRowAttributes?: ( row: Row, ) => React.DataHTMLAttributes | undefined; onClick?: (row: Row, event: React.MouseEvent) => void; @@ -22,11 +24,13 @@ export interface BaseRowProps { getGroupTitle?: (row: Row) => React.ReactNode, ) => React.ReactNode; row: Row; + rowVirtualizer?: Virtualizer; style?: React.CSSProperties; + virtualItem?: VirtualItem; } export const BaseRow = React.forwardRef( - ( + ( { cellClassName, checkIsGroupRow, @@ -34,25 +38,41 @@ export const BaseRow = React.forwardRef( className, columnsCount, getGroupTitle, - getRowDataAttributes, + getRowAttributes, onClick, renderCell, renderGroupHeader = renderDefaultGroupHeader, row, + rowVirtualizer, style, - }: BaseRowProps, + virtualItem, + }: BaseRowProps, ref: React.Ref, ) => { - const isGroup = checkIsGroupRow?.(row); + const rowRef = useForkRef(rowVirtualizer?.measureElement, ref); - const handleClick = (event: React.MouseEvent) => { - onClick?.(row, event); - }; + const rowStyle = React.useMemo(() => { + if (!virtualItem) { + return style; + } + + return { + top: virtualItem.start, + ...style, + }; + }, [style, virtualItem]); + + const handleClick = React.useCallback( + (event: React.MouseEvent) => { + onClick?.(row, event); + }, + [onClick, row], + ); return ( - {isGroup && ( + {checkIsGroupRow?.(row) ? ( {row.getCanSelect() && renderCell(row.getVisibleCells()[0]!)} - )} - {!isGroup && + ) : ( row .getVisibleCells() .map((cell) => ( {renderCell(cell)} - ))} + )) + )} {children} ); diff --git a/src/components/Cell/Cell.tsx b/src/components/Cell/Cell.tsx index 0d0b141..0f14f17 100644 --- a/src/components/Cell/Cell.tsx +++ b/src/components/Cell/Cell.tsx @@ -9,15 +9,9 @@ export interface CellProps { cell: CellProperties; className?: string; contentClassName?: string; - selectionColumnId?: string; } -export const Cell = ({ - cell, - className, - contentClassName, - selectionColumnId, -}: CellProps) => { +export const Cell = ({cell, className, contentClassName}: CellProps) => { return ( diff --git a/src/components/DragHandle/DragHandle.classname.ts b/src/components/DragHandle/DragHandle.classname.ts index 00b9a57..696d8e4 100644 --- a/src/components/DragHandle/DragHandle.classname.ts +++ b/src/components/DragHandle/DragHandle.classname.ts @@ -1,3 +1,3 @@ -import {block} from '../utils/cn'; +import {block} from '../../utils'; export const b = block('drag-handle'); diff --git a/src/components/DraggableRow/DraggableRow.tsx b/src/components/DraggableRow/DraggableRow.tsx index 08fd96a..30d54ff 100644 --- a/src/components/DraggableRow/DraggableRow.tsx +++ b/src/components/DraggableRow/DraggableRow.tsx @@ -4,18 +4,19 @@ import {useSortable} from '@dnd-kit/sortable'; import {useForkRef} from '@gravity-ui/uikit'; import {useDraggableRowDepth, useDraggableRowStyle} from '../../hooks'; +import {toDataAttributes} from '../../utils'; import type {BaseRowProps} from '../BaseRow'; import {BaseRow} from '../BaseRow'; import {SortableListContext} from '../SortableListContext'; import {TableContext} from '../TableContext'; -import {toDataAttributes} from '../utils/toDataAttributes'; export interface DraggableRowProps extends BaseRowProps {} export const DraggableRow = React.forwardRef( - (props: DraggableRowProps, ref: React.Ref) => { - const {getRowDataAttributes, row, style} = props; - + ( + {getRowAttributes, row, style, ...restProps}: DraggableRowProps, + ref: React.Ref, + ) => { const {setNodeRef, transform, transition, isDragging} = useSortable({ id: row.id, }); @@ -48,10 +49,10 @@ export const DraggableRow = React.forwardRef( }); const getDraggableRowDataAttributes = React.useCallback< - NonNullable['getRowDataAttributes']> + NonNullable['getRowAttributes']> >( (row) => ({ - ...getRowDataAttributes?.(row), + ...getRowAttributes?.(row), ...toDataAttributes({ key: row.id, depth, @@ -61,15 +62,16 @@ export const DraggableRow = React.forwardRef( expanded: isDragActive && isParent, }), }), - [getRowDataAttributes, depth, isDragging, isDragActive, isParent], + [getRowAttributes, depth, isDragging, isDragActive, isParent], ); return ( ); }, diff --git a/src/components/DraggableRowMarker/DraggableRowMarker.classname.ts b/src/components/DraggableRowMarker/DraggableRowMarker.classname.ts index 21a935f..10fd04b 100644 --- a/src/components/DraggableRowMarker/DraggableRowMarker.classname.ts +++ b/src/components/DraggableRowMarker/DraggableRowMarker.classname.ts @@ -1,3 +1,3 @@ -import {block} from '../utils/cn'; +import {block} from '../../utils'; export const b = block('draggable-row-marker'); diff --git a/src/components/HeaderCell/HeaderCell.tsx b/src/components/HeaderCell/HeaderCell.tsx index 7737548..bd48d62 100644 --- a/src/components/HeaderCell/HeaderCell.tsx +++ b/src/components/HeaderCell/HeaderCell.tsx @@ -3,8 +3,8 @@ import React from 'react'; import type {Header} from '@tanstack/react-table'; import {flexRender} from '@tanstack/react-table'; +import {renderDefaultSortIndicator} from '../../utils'; import {b} from '../Table/Table.classname'; -import {renderDefaultSortIndicator} from '../utils/renderDefaultSortIndicator'; export interface HeaderCellProps { className?: string; @@ -12,7 +12,6 @@ export interface HeaderCellProps { header: Header; parentHeader?: Header; renderSortIndicator?: (header: Header, className?: string) => React.ReactNode; - selection?: boolean; sortIndicatorClassName?: string; } @@ -22,7 +21,6 @@ export const HeaderCell = ({ header, parentHeader, renderSortIndicator = renderDefaultSortIndicator, - selection, sortIndicatorClassName, }: HeaderCellProps) => { const {table} = header.getContext(); @@ -67,7 +65,7 @@ export const HeaderCell = ({ rowSpan={rowSpan} onClick={header.column.getToggleSortingHandler()} > -
+
{flexRender(header.column.columnDef.header, header.getContext())}{' '} {header.column.getCanSort() && renderSortIndicator(header, sortIndicatorClassName)} {enableColumnResizing && ( diff --git a/src/components/HeaderRow/HeaderRow.tsx b/src/components/HeaderRow/HeaderRow.tsx index a788adf..8c9adf1 100644 --- a/src/components/HeaderRow/HeaderRow.tsx +++ b/src/components/HeaderRow/HeaderRow.tsx @@ -13,7 +13,6 @@ export interface HeaderRowProps { headerGroup: HeaderGroup; parentHeaderGroup?: HeaderGroup; renderSortIndicator: HeaderCellProps['renderSortIndicator']; - selectionColumnId?: string; sortIndicatorClassName: HeaderCellProps['sortIndicatorClassName']; } @@ -24,7 +23,6 @@ export const HeaderRow = ({ headerGroup, parentHeaderGroup, renderSortIndicator, - selectionColumnId, sortIndicatorClassName, }: HeaderRowProps) => { return ( @@ -39,7 +37,6 @@ export const HeaderRow = ({ (item) => header.column.id === item.column.id, )} renderSortIndicator={renderSortIndicator} - selection={header.column.id === selectionColumnId} sortIndicatorClassName={sortIndicatorClassName} /> ))} diff --git a/src/components/ReorderingProvider/ReorderingProvider.tsx b/src/components/ReorderingProvider/ReorderingProvider.tsx index 2e5c6b4..c21e3d8 100644 --- a/src/components/ReorderingProvider/ReorderingProvider.tsx +++ b/src/components/ReorderingProvider/ReorderingProvider.tsx @@ -1,16 +1,16 @@ import React from 'react'; +import type {Table} from '@tanstack/react-table'; + import type {SortableListProps} from '../SortableList'; import {SortableList} from '../SortableList'; import {SortableListDndContext} from '../SortableListDndContext'; import type {SortableListDndContextProps} from '../SortableListDndContext'; -import type {TableProps} from '../Table'; -import {flattenTableData} from '../utils/flattenTableData'; import './ReorderingProvider.scss'; -export interface ReorderingProviderProps - extends Pick, 'data' | 'getRowId' | 'getSubRows' | 'expandedIds'> { +export interface ReorderingProviderProps { + table: Table; children?: React.ReactNode; dndModifiers?: SortableListDndContextProps['modifiers']; enableNesting?: SortableListProps['nestingEnabled']; @@ -18,19 +18,14 @@ export interface ReorderingProviderProps } export const ReorderingProvider = ({ + table, children, - data, dndModifiers, enableNesting, - expandedIds, - getRowId, - getSubRows, onReorder, }: ReorderingProviderProps) => { - const rowIds = React.useMemo( - () => flattenTableData({data, getRowId, getSubRows, expandedIds, transformItem: getRowId}), - [data, getRowId, getSubRows, expandedIds], - ); + const {rows} = table.getRowModel(); + const rowIds = React.useMemo(() => rows.map((row) => row.id), [rows]); return ( diff --git a/src/components/Row/Row.tsx b/src/components/Row/Row.tsx deleted file mode 100644 index cc0fb71..0000000 --- a/src/components/Row/Row.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; - -import {BaseRow} from '../BaseRow'; -import type {BaseRowProps} from '../BaseRow'; -import {DraggableRow} from '../DraggableRow'; -import type {VirtualRowProps} from '../VirtualRow'; -import {VirtualRow} from '../VirtualRow'; - -export interface RowProps extends BaseRowProps { - draggable?: boolean; - virtualRow?: VirtualRowProps['virtualRow']; -} - -export const Row = React.forwardRef( - (props: RowProps, ref: React.Ref) => { - const {draggable, virtualRow} = props; - - if (virtualRow) { - return ; - } - - if (draggable) { - return ; - } - - return ; - }, -) as (( - props: RowProps & {ref?: React.Ref}, -) => React.ReactElement) & { - displayName: string; -}; - -Row.displayName = 'Row'; diff --git a/src/components/Row/index.ts b/src/components/Row/index.ts deleted file mode 100644 index 7a86ee8..0000000 --- a/src/components/Row/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Row'; diff --git a/src/components/SortIndicator/SortIndicator.classname.ts b/src/components/SortIndicator/SortIndicator.classname.ts index 30d670b..b079f2f 100644 --- a/src/components/SortIndicator/SortIndicator.classname.ts +++ b/src/components/SortIndicator/SortIndicator.classname.ts @@ -1,3 +1,3 @@ -import {block} from '../utils/cn'; +import {block} from '../../utils'; export const b = block('sort-indicator'); diff --git a/src/components/SortableList/SortableList.tsx b/src/components/SortableList/SortableList.tsx index 5bc242a..4dbf202 100644 --- a/src/components/SortableList/SortableList.tsx +++ b/src/components/SortableList/SortableList.tsx @@ -17,20 +17,27 @@ export const SortableList = ({ onDragEnd, nestingEnabled, }: SortableListProps) => { - const {activeItemKey, isChildMode, isParentMode, isNextChildMode, targetItemIndex} = - useSortableList({ - items, - onDragStart, - onDragEnd, - nestingEnabled, - }); + const { + activeItemKey, + activeItemIndex, + isChildMode, + isParentMode, + isNextChildMode, + targetItemIndex, + } = useSortableList({ + items, + onDragStart, + onDragEnd, + nestingEnabled, + }); const contextValue = React.useMemo( () => ({ - isParentMode, + activeItemKey, + activeItemIndex, isChildMode, isNextChildMode, - activeItemKey, + isParentMode, targetItemIndex, }), [isParentMode, isChildMode, isNextChildMode, activeItemKey, targetItemIndex], diff --git a/src/components/SortableList/types.ts b/src/components/SortableList/types.ts index 07adeff..41233c1 100644 --- a/src/components/SortableList/types.ts +++ b/src/components/SortableList/types.ts @@ -3,7 +3,7 @@ export interface SortableListDragResult { targetItemKey?: string; baseItemKey?: string; baseNextItemKey?: string; - nestingEnabled: boolean; + nestingEnabled?: boolean; nextChild?: boolean; pullFromParent?: boolean; } diff --git a/src/components/SortableListDndContext/SortableListDndContext.tsx b/src/components/SortableListDndContext/SortableListDndContext.tsx index 6e134e5..f9e99ba 100644 --- a/src/components/SortableListDndContext/SortableListDndContext.tsx +++ b/src/components/SortableListDndContext/SortableListDndContext.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import type {Modifier} from '@dnd-kit/core'; +import type {MeasuringConfiguration, Modifier} from '@dnd-kit/core'; import { DndContext, MeasuringStrategy, @@ -15,7 +15,7 @@ export interface SortableListDndContextProps { children?: React.ReactNode; } -const measuring = { +const measuring: MeasuringConfiguration = { droppable: { strategy: MeasuringStrategy.WhileDragging, }, diff --git a/src/components/Table/Table.classname.ts b/src/components/Table/Table.classname.ts index f17d6c1..2c4c7a2 100644 --- a/src/components/Table/Table.classname.ts +++ b/src/components/Table/Table.classname.ts @@ -1,3 +1,3 @@ -import {block} from '../utils/cn'; +import {block} from '../../utils'; export const b = block('table'); diff --git a/src/components/Table/Table.scss b/src/components/Table/Table.scss index c233051..2e89429 100644 --- a/src/components/Table/Table.scss +++ b/src/components/Table/Table.scss @@ -83,10 +83,6 @@ $block: '.#{variables.$ns}table'; align-items: center; height: 100%; - - &_selection { - justify-content: center; - } } &__group-title-wrapper { @@ -154,4 +150,48 @@ $block: '.#{variables.$ns}table'; opacity: 1; } } + + &_with-row-virtualization { + display: grid; + + height: auto; + } + + &_with-row-virtualization & { + &__body { + display: grid; + + position: relative; + } + + &__header { + display: grid; + + position: sticky; + inset-block-start: 0; + + z-index: 1; + } + + &__header-row { + display: flex; + + width: 100%; + height: auto; + } + + &__row { + display: flex; + + position: absolute; + + width: 100%; + height: auto; + } + + &__cell, + &__header-cell { + display: flex; + } + } } diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 978e5d0..f6a7bdd 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -2,226 +2,76 @@ import React from 'react'; import type { Cell as CellProperties, - ColumnDef, - ColumnResizeDirection, - ColumnResizeMode, - ColumnSizingInfoState, - ColumnSizingState, - GroupingOptions, - GroupingState, - OnChangeFn, Row as RowProperties, - SortingState, - TableOptions, -} from '@tanstack/react-table'; -import { - getCoreRowModel, - getGroupedRowModel, - getSortedRowModel, - useReactTable, + Table as TableType, } from '@tanstack/react-table'; +import type {VirtualItem, Virtualizer} from '@tanstack/react-virtual'; -import {defaultDragHandleColumn, defaultSelectionColumn} from '../../constants'; -import type {UseColumnsParams, UseExpandedParams, UseSelectionParams} from '../../hooks'; -import {useColumns, useExpanded, useSelection} from '../../hooks'; +import type {BaseRowProps} from '../BaseRow'; +import {BaseRow} from '../BaseRow'; import {Cell} from '../Cell'; +import {DraggableRow} from '../DraggableRow'; import type {HeaderRowProps} from '../HeaderRow'; import {HeaderRow} from '../HeaderRow'; -import type {RowProps} from '../Row'; -import {Row} from '../Row'; import {SortableListContext} from '../SortableListContext'; import {TableContextProvider} from '../TableContextProvider'; import type {TableContextProviderProps} from '../TableContextProvider'; -import {VirtualizationContext} from '../VirtualizationContext'; import {b} from './Table.classname'; import './Table.scss'; -export interface TableProps - extends UseSelectionParams, - UseExpandedParams, - Pick< - Partial>, - 'checkIsGroupRow' | 'renderGroupHeader' | 'getGroupTitle' | 'getRowDataAttributes' - >, - Pick< - Partial>, - 'sortIndicatorClassName' | 'renderSortIndicator' - > { - className?: string; - rowClassName?: string; - headerClassName?: string; +export interface TableProps { bodyClassName?: string; - headerRowClassName?: string; - headerCellClassName?: string; - headerCellContentClassName?: string; cellClassName?: string; cellContentClassName?: string; - withHeader?: boolean; - data: TData[]; - getRowId: (item: TData) => string; - columns: ColumnDef[]; - selectionColumn?: UseColumnsParams['selectionColumn']; - dragHandleColumn?: UseColumnsParams['dragHandleColumn']; - getSubRows?: (item: TData) => undefined | TData[]; - onRowClick?: RowProps['onClick']; + checkIsGroupRow?: BaseRowProps['checkIsGroupRow']; + className?: string; enableNesting?: boolean; - enableSorting?: boolean; - manualSorting?: boolean; - sorting?: SortingState; - onSortingChange?: React.Dispatch>; - grouping?: GroupingState; - enableGrouping?: boolean; - groupedColumnMode?: GroupingOptions['groupedColumnMode']; - manualGrouping?: boolean; - onGroupingChange?: GroupingOptions['onGroupingChange']; - aggregationFns?: GroupingOptions['aggregationFns']; - enableColumnResizing?: boolean; - columnResizeMode?: ColumnResizeMode; - columnResizeDirection?: ColumnResizeDirection; - onColumnSizingChange?: OnChangeFn; - onColumnSizingInfoChange?: OnChangeFn; + getGroupTitle?: BaseRowProps['getGroupTitle']; + getRowAttributes?: BaseRowProps['getRowAttributes']; + headerCellClassName?: string; + headerCellContentClassName?: string; + headerClassName?: string; + headerRowClassName?: string; + onRowClick?: BaseRowProps['onClick']; + renderGroupHeader?: BaseRowProps['renderGroupHeader']; + renderSortIndicator?: HeaderRowProps['renderSortIndicator']; + rowClassName?: string; + rowVirtualizer?: Virtualizer; + sortIndicatorClassName?: HeaderRowProps['sortIndicatorClassName']; + table: TableType; + withHeader?: boolean; } -export const Table = React.memo( - ({ - className, - rowClassName, - headerRowClassName, - headerClassName, - bodyClassName, - headerCellClassName, - headerCellContentClassName, - cellClassName, - cellContentClassName, - getRowId, - columns: providedColumns, - withHeader = true, - data, - selectedIds, - onSelectedChange, - selectionColumn = defaultSelectionColumn as ColumnDef, - dragHandleColumn = defaultDragHandleColumn as ColumnDef, - expandedIds, - onExpandedChange, - getSubRows, - getGroupTitle, - checkIsGroupRow, - renderGroupHeader, - onRowClick, - getRowDataAttributes, - enableNesting, - enableSorting = false, - manualSorting, - sorting, - onSortingChange, - sortIndicatorClassName, - renderSortIndicator, - grouping, - enableGrouping, - groupedColumnMode, - manualGrouping, - onGroupingChange, - aggregationFns, - enableColumnResizing = false, - columnResizeMode = 'onChange', - columnResizeDirection = 'ltr', - onColumnSizingChange, - onColumnSizingInfoChange, - }: TableProps) => { +export const Table = React.forwardRef( + ( + { + bodyClassName, + cellClassName, + cellContentClassName, + checkIsGroupRow, + className, + enableNesting, + getGroupTitle, + getRowAttributes, + headerCellClassName, + headerCellContentClassName, + headerClassName, + headerRowClassName, + onRowClick, + renderGroupHeader, + renderSortIndicator, + rowClassName, + rowVirtualizer, + sortIndicatorClassName, + table, + withHeader = true, + }: TableProps, + ref: React.Ref, + ) => { const draggableContext = React.useContext(SortableListContext); - const draggable = Boolean(draggableContext); - const draggableItemKey = draggableContext?.activeItemKey; - - const virtualizationContext = React.useContext(VirtualizationContext); - const virtual = Boolean(virtualizationContext); - const {totalSize, virtualItems} = virtualizationContext ?? {}; - - const { - enableRowSelection, - enableMultiRowSelection, - rowSelection, - handleRowSelectionChange, - } = useSelection({ - selectedIds, - onSelectedChange, - }); - - const {expanded, getExpandedRowModel, handleExpandedChange, enableExpanding} = - useExpanded({ - expandedIds, - onExpandedChange, - }); - - const columns: ColumnDef[] = useColumns({ - columns: providedColumns, - enableRowSelection, - selectionColumn, - dragHandleColumn, - draggable, - }); - - const tableOptions: TableOptions = { - getRowId, - data, - columns, - getCoreRowModel: getCoreRowModel(), - state: { - rowSelection, - expanded, - sorting, - grouping, - }, - - // selection - enableRowSelection, - enableMultiRowSelection, - onRowSelectionChange: handleRowSelectionChange, - - // expanding - enableExpanding, - getSubRows, - getRowCanExpand: (row) => Boolean(checkIsGroupRow?.(row) || row.subRows?.length), - getExpandedRowModel, - onExpandedChange: handleExpandedChange, - - // sorting - enableSorting, - manualSorting, - getSortedRowModel: enableSorting ? getSortedRowModel() : undefined, - onSortingChange, - - // grouping - enableGrouping, - getGroupedRowModel: enableGrouping ? getGroupedRowModel() : undefined, - groupedColumnMode, - manualGrouping, - onGroupingChange, - aggregationFns, - - // resizing - enableColumnResizing, - columnResizeMode, - columnResizeDirection, - }; - - if (onColumnSizingChange) { - tableOptions.onColumnSizingChange = onColumnSizingChange; - } - - if (onColumnSizingInfoChange) { - tableOptions.onColumnSizingInfoChange = onColumnSizingInfoChange; - } - - const draggableIndex = React.useMemo(() => { - if (!draggableItemKey) { - return undefined; - } - return data.findIndex((item) => getRowId(item) === draggableItemKey); - }, [data, draggableItemKey, getRowId]); - - const table = useReactTable(tableOptions); + const draggingRowIndex = draggableContext?.activeItemIndex ?? -1; const getRowByIndex = React.useCallback['getRowByIndex']>( (rowIndex: number) => { @@ -235,34 +85,12 @@ export const Table = React.memo( return ( ); }, - [cellClassName, cellContentClassName, selectionColumn?.id], - ); - - const renderRow = ( - row: RowProperties, - additionalProps?: Partial>, - ) => ( - + [cellClassName, cellContentClassName], ); const {rows} = table.getRowModel(); @@ -270,12 +98,12 @@ export const Table = React.memo( const headerGroups = table.getHeaderGroups(); return ( - -
{renderGroupHeader(row, getGroupTitle)}
({ maxWidth: cell.column.columnDef.maxSize, }} > -
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+ +
-1 ? draggingRowIndex : undefined} + > {withHeader && ( {headerGroups.map((headerGroup, index) => ( @@ -286,7 +114,6 @@ export const Table = React.memo( className={headerRowClassName} cellClassName={headerCellClassName} cellContentClassName={headerCellContentClassName} - selectionColumnId={selectionColumn?.id} sortIndicatorClassName={sortIndicatorClassName} renderSortIndicator={renderSortIndicator} /> @@ -296,25 +123,43 @@ export const Table = React.memo( - {!virtual && rows.map((row) => renderRow(row))} - {virtual && - virtualItems?.map((virtualRow) => { - const tableRow = rows[virtualRow.index] as RowProperties; - - return renderRow(tableRow, { - virtualRow, - }); - })} + {(rowVirtualizer?.getVirtualItems() || rows).map((virtualItemOrRow) => { + const row = rowVirtualizer + ? rows[virtualItemOrRow.index] + : (virtualItemOrRow as RowProperties); + + const baseRowProps: BaseRowProps = { + row, + checkIsGroupRow, + onClick: onRowClick, + className: rowClassName, + renderCell, + cellClassName, + renderGroupHeader, + getGroupTitle, + columnsCount: table.options.columns.length, + getRowAttributes, + virtualItem: rowVirtualizer + ? (virtualItemOrRow as VirtualItem) + : undefined, + }; + + if (draggableContext) { + return ; + } + + return ; + })}
); }, -) as (( - props: TableProps & {ref?: React.Ref}, +) as (( + props: TableProps & {ref?: React.Ref}, ) => React.ReactElement) & {displayName: string}; Table.displayName = 'Table'; diff --git a/src/components/TableContext/TableContext.ts b/src/components/TableContext/TableContext.ts index 517c184..750189b 100644 --- a/src/components/TableContext/TableContext.ts +++ b/src/components/TableContext/TableContext.ts @@ -1,15 +1,13 @@ import React from 'react'; -import type {Row, TableState} from '@tanstack/react-table'; +import type {Row} from '@tanstack/react-table'; export interface TableContextProps { getRowByIndex: (index: number) => Row | undefined; enableNesting?: boolean; - getTableState: () => TableState; } export const TableContext = React.createContext({ getRowByIndex: () => undefined, enableNesting: false, - getTableState: () => ({}) as TableState, }); diff --git a/src/components/TableContextProvider/TableContextProvider.tsx b/src/components/TableContextProvider/TableContextProvider.tsx index a2fb941..b8a1c81 100644 --- a/src/components/TableContextProvider/TableContextProvider.tsx +++ b/src/components/TableContextProvider/TableContextProvider.tsx @@ -5,9 +5,9 @@ import type {Row} from '@tanstack/react-table'; import type {TableContextProps} from '../TableContext'; import {TableContext} from '../TableContext'; -export interface TableContextProviderProps - extends Pick { +export interface TableContextProviderProps { children?: React.ReactNode; + enableNesting?: TableContextProps['enableNesting']; getRowByIndex: (index: number) => Row | undefined; } @@ -15,11 +15,10 @@ export const TableContextProvider = ({ children, enableNesting, getRowByIndex, - getTableState, }: TableContextProviderProps) => { const contextValue = React.useMemo( - () => ({getRowByIndex, enableNesting, getTableState}) as TableContextProps, - [getRowByIndex, enableNesting, getTableState], + () => ({getRowByIndex, enableNesting}) as TableContextProps, + [getRowByIndex, enableNesting], ); return {children}; diff --git a/src/components/VirtualRow/VirtualRow.tsx b/src/components/VirtualRow/VirtualRow.tsx deleted file mode 100644 index 4a7ca07..0000000 --- a/src/components/VirtualRow/VirtualRow.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; - -import {useForkRef} from '@gravity-ui/uikit'; -import type {VirtualItem} from '@tanstack/react-virtual'; - -import type {BaseRowProps} from '../BaseRow'; -import {Row} from '../Row'; -import {VirtualizationContext} from '../VirtualizationContext'; -import {toDataAttributes} from '../utils/toDataAttributes'; - -export interface VirtualRowProps extends BaseRowProps { - virtualRow: VirtualItem; -} - -export const VirtualRow = React.forwardRef( - (props: VirtualRowProps, ref: React.Ref) => { - const {getRowDataAttributes, virtualRow, style} = props; - - const {measureRow} = React.useContext(VirtualizationContext)!; - - const virtualRowStyle = React.useMemo( - () => ({ - top: virtualRow.start, - ...style, - }), - [style, virtualRow.start], - ); - - const handleRowRef = useForkRef(measureRow, ref); - - const getVirtualRowDataAttributes = React.useCallback< - NonNullable['getRowDataAttributes']> - >( - (row) => ({ - ...getRowDataAttributes?.(row), - ...toDataAttributes({ - index: virtualRow.index, - }), - }), - [getRowDataAttributes, virtualRow.index], - ); - - return ( - - ); - }, -) as (( - props: VirtualRowProps & {ref?: React.Ref}, -) => React.ReactElement) & { - displayName: string; -}; - -VirtualRow.displayName = 'VirtualRow'; diff --git a/src/components/VirtualRow/index.ts b/src/components/VirtualRow/index.ts deleted file mode 100644 index 255c5cc..0000000 --- a/src/components/VirtualRow/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './VirtualRow'; diff --git a/src/components/VirtualizationContainer/VirtualizationContainer.classname.ts b/src/components/VirtualizationContainer/VirtualizationContainer.classname.ts deleted file mode 100644 index 3e6b103..0000000 --- a/src/components/VirtualizationContainer/VirtualizationContainer.classname.ts +++ /dev/null @@ -1,3 +0,0 @@ -import {block} from '../utils/cn'; - -export const b = block('virtualization-container'); diff --git a/src/components/VirtualizationContainer/VirtualizationContainer.scss b/src/components/VirtualizationContainer/VirtualizationContainer.scss deleted file mode 100644 index ec72be8..0000000 --- a/src/components/VirtualizationContainer/VirtualizationContainer.scss +++ /dev/null @@ -1,55 +0,0 @@ -@use '../variables'; - -$block: '.#{variables.$ns}virtualization-container'; -$tableBlock: '.#{variables.$ns}table'; - -#{$block} { - position: relative; - - &_with-scroll { - overflow: auto; - } - - #{$tableBlock} { - // Even though we're still using semantic table tags, we must use CSS grid and flexbox for dynamic row heights - display: grid; - - height: auto; - - &__body { - display: grid; - - position: relative; - } - - &__header { - display: grid; - - position: sticky; - inset-block-start: 0; - - z-index: 1; - } - - &__header-row { - display: flex; - - width: 100%; - height: auto; - } - - &__row { - display: flex; - - position: absolute; - - width: 100%; - height: auto; - } - - &__cell, - &__header-cell { - display: flex; - } - } -} diff --git a/src/components/VirtualizationContainer/VirtualizationContainer.tsx b/src/components/VirtualizationContainer/VirtualizationContainer.tsx deleted file mode 100644 index e2e7587..0000000 --- a/src/components/VirtualizationContainer/VirtualizationContainer.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; - -import {b} from './VirtualizationContainer.classname'; - -import './VirtualizationContainer.scss'; - -export interface TableVirtualizationContainerProps { - children?: React.ReactNode; - className?: string; - height?: string; - withScroll?: boolean; -} - -export const VirtualizationContainer = React.forwardRef< - HTMLDivElement, - TableVirtualizationContainerProps ->(({className, height, withScroll, children}, ref) => { - return ( -
- {children} -
- ); -}); - -VirtualizationContainer.displayName = 'VirtualizationContainer'; diff --git a/src/components/VirtualizationContainer/index.ts b/src/components/VirtualizationContainer/index.ts deleted file mode 100644 index f9aa22c..0000000 --- a/src/components/VirtualizationContainer/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './VirtualizationContainer'; diff --git a/src/components/VirtualizationContext/VirtualizationContext.ts b/src/components/VirtualizationContext/VirtualizationContext.ts deleted file mode 100644 index 1e1c686..0000000 --- a/src/components/VirtualizationContext/VirtualizationContext.ts +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; - -import type {VirtualItem} from '@tanstack/react-virtual'; - -interface TableVirtualizationContextValue { - virtualItems: VirtualItem[]; - totalSize: number; - measureRow: (node: HTMLTableRowElement | null) => void; -} - -export const VirtualizationContext = React.createContext< - TableVirtualizationContextValue | undefined ->(undefined); diff --git a/src/components/VirtualizationContext/index.ts b/src/components/VirtualizationContext/index.ts deleted file mode 100644 index 4cc7a50..0000000 --- a/src/components/VirtualizationContext/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './VirtualizationContext'; diff --git a/src/components/VirtualizationProvider/VirtualizationProvider.tsx b/src/components/VirtualizationProvider/VirtualizationProvider.tsx deleted file mode 100644 index 586766d..0000000 --- a/src/components/VirtualizationProvider/VirtualizationProvider.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react'; - -import {useVirtualizer} from '@tanstack/react-virtual'; - -import {VirtualizationContainer} from '../VirtualizationContainer'; -import {VirtualizationContext} from '../VirtualizationContext'; -import {getVirtualRangeExtractor} from '../utils/getVirtualRangeExtractor'; - -export interface VirtualizationProviderProps { - children?: React.ReactNode; - rowsCount: number; - overscanRowCount?: number; - containerClassName?: string; - containerHeight?: string; - estimateRowSize: (rowIndex: number) => number; -} - -export const VirtualizationProvider = ({ - children, - rowsCount, - overscanRowCount, - containerClassName, - containerHeight, - estimateRowSize, -}: VirtualizationProviderProps) => { - const containerRef = React.useRef(null); - - const rowVirtualizer = useVirtualizer({ - count: rowsCount, - estimateSize: estimateRowSize, - getScrollElement: () => containerRef.current, - measureElement: (element) => element?.getBoundingClientRect().height, - overscan: overscanRowCount, - rangeExtractor: getVirtualRangeExtractor(containerRef.current), - }); - - const virtualItems = rowVirtualizer.getVirtualItems(); - const totalSize = rowVirtualizer.getTotalSize(); - - const contextValue = React.useMemo( - () => ({ - virtualItems, - totalSize, - measureRow: rowVirtualizer.measureElement, - }), - [virtualItems, totalSize, rowVirtualizer.measureElement], - ); - - return ( - - - {children} - - - ); -}; diff --git a/src/components/VirtualizationProvider/index.ts b/src/components/VirtualizationProvider/index.ts deleted file mode 100644 index cc2e744..0000000 --- a/src/components/VirtualizationProvider/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './VirtualizationProvider'; diff --git a/src/components/WindowVirtualizationProvider/WindowVirtualizationProvider.tsx b/src/components/WindowVirtualizationProvider/WindowVirtualizationProvider.tsx deleted file mode 100644 index a551067..0000000 --- a/src/components/WindowVirtualizationProvider/WindowVirtualizationProvider.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; - -import {useWindowVirtualizer} from '@tanstack/react-virtual'; - -import {VirtualizationContainer} from '../VirtualizationContainer'; -import {VirtualizationContext} from '../VirtualizationContext'; -import {getVirtualRangeExtractor} from '../utils/getVirtualRangeExtractor'; - -export interface WindowVirtualizationProviderProps { - rowsCount: number; - overscanRowCount?: number; - containerClassName?: string; - estimateRowSize: (rowIndex: number) => number; - children?: React.ReactNode; -} - -export const WindowVirtualizationProvider = ({ - children, - rowsCount, - overscanRowCount, - containerClassName, - estimateRowSize, -}: WindowVirtualizationProviderProps) => { - const containerRef = React.useRef(null); - - const rowVirtualizer = useWindowVirtualizer({ - count: rowsCount, - estimateSize: estimateRowSize, - measureElement: (element) => element?.getBoundingClientRect().height, - overscan: overscanRowCount, - rangeExtractor: getVirtualRangeExtractor(containerRef.current), - }); - - const virtualItems = rowVirtualizer.getVirtualItems(); - const totalSize = rowVirtualizer.getTotalSize(); - - const contextValue = React.useMemo( - () => ({ - virtualItems, - totalSize, - measureRow: rowVirtualizer.measureElement, - }), - [virtualItems, totalSize, rowVirtualizer.measureElement], - ); - - return ( - - - {children} - - - ); -}; diff --git a/src/components/WindowVirtualizationProvider/index.ts b/src/components/WindowVirtualizationProvider/index.ts deleted file mode 100644 index 4cd108d..0000000 --- a/src/components/WindowVirtualizationProvider/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './WindowVirtualizationProvider'; diff --git a/src/components/__stories__/DefaultDemo.tsx b/src/components/__stories__/DefaultDemo.tsx new file mode 100644 index 0000000..f45b3a9 --- /dev/null +++ b/src/components/__stories__/DefaultDemo.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +import {useTable} from '../../hooks'; +import {Table} from '../Table'; + +import {columns} from './constants/columns'; +import {data} from './constants/data'; + +export const DefaultDemo = () => { + const table = useTable({ + columns, + data, + }); + + return ; +}; diff --git a/src/components/__stories__/Grid2.stories.tsx b/src/components/__stories__/Grid2.stories.tsx deleted file mode 100644 index 317a232..0000000 --- a/src/components/__stories__/Grid2.stories.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import React from 'react'; - -import type {Meta, StoryFn} from '@storybook/react'; - -import {Table} from '../Table'; - -import {cnGridDemo} from './GridDemo.classname'; -import {GroupingDemo} from './GroupingDemo'; -import {GroupingDemo2} from './GroupingDemo2'; -import {GroupingWithSelectionDemo} from './GroupingWithSelectionDemo'; -import {ReorderingDemo} from './ReorderingDemo'; -import {ReorderingTreeDemo} from './ReorderingTreeDemo'; -import {ReorderingWithVirtualizationDemo} from './ReorderingWithVirtualizationDemo'; -import {SortingDemo} from './SortingDemo'; -import {TreeDemo} from './TreeDemo'; -import {TreeWithGroupsDemo} from './TreeWithGroupsDemo'; -import {VirtualizationDemo} from './VirtualizationDemo'; -import {WindowVirtualizationDemo} from './WindowVirtualizationDemo'; -import {WithSelectionDemo} from './WithSelectionDemo'; -import {columns} from './constants/columns'; -import {data} from './constants/data'; -import type {Item} from './types'; -import {generateData} from './utils'; - -import './GridDemo.scss'; - -export default { - title: 'Table', - component: Table, -} as Meta; - -const Template: StoryFn> = (args) =>
; - -export const Default: StoryFn> = Template.bind({}); - -Default.args = { - data, - columns, - getRowId: (item: Item) => item.id, - onSelectedChange: undefined, - onRowClick: undefined, -}; - -export const WithoutHeader: StoryFn> = Template.bind({}); - -WithoutHeader.args = { - data, - columns, - getRowId: (item: Item) => item.id, - withHeader: false, - onSelectedChange: undefined, - onRowClick: undefined, -}; - -export const HeaderGroups: StoryFn> = Template.bind({}); - -HeaderGroups.args = { - data, - columns: [ - { - id: 'id', - header: 'ID', - accessorKey: 'id', - }, - { - id: 'columns-parent', - header: 'Columns header', - columns: [ - { - id: 'columns', - header: 'Columns', - columns, - }, - ], - }, - { - id: 'actions', - header: 'Actions', - accessorKey: 'id', - columns: [ - { - id: 'edit', - header: 'Edit', - accessorKey: 'id', - }, - { - id: 'delete', - header: 'Delete', - accessorKey: 'id', - }, - ], - }, - ], - getRowId: (item: Item) => item.id, - onSelectedChange: undefined, - onRowClick: undefined, - className: cnGridDemo('header-groups-grid'), -}; - -const WithSelectionTemplate: StoryFn> = (args) => ( - -); - -export const WithSelection: StoryFn> = WithSelectionTemplate.bind({}); - -WithSelection.args = { - data, - columns, - getRowId: (item: Item) => item.id, - onRowClick: undefined, -}; - -const SortingTemplate: StoryFn> = (args) => ; - -export const Sorting: StoryFn> = SortingTemplate.bind({}); - -Sorting.args = { - data, - columns, - getRowId: (item: Item) => item.id, - onRowClick: undefined, - onSelectedChange: undefined, -}; - -const GroupingTemplate: StoryFn = () => ; -const GroupingTemplate2: StoryFn = () => ; - -export const Grouping: StoryFn = GroupingTemplate.bind({}); -export const Grouping2: StoryFn = GroupingTemplate2.bind({}); - -const GroupingWithSelectionTemplate: StoryFn = () => ; - -export const GroupingWithSelection: StoryFn = GroupingWithSelectionTemplate.bind({}); - -const TreeTemplate: StoryFn = () => ; - -export const Tree: StoryFn = TreeTemplate.bind({}); - -const TreeWithGroupsTemplate: StoryFn = () => ; - -export const TreeWithGroups: StoryFn = TreeWithGroupsTemplate.bind({}); - -const ReorderingTemplate: StoryFn> = (args) => ; - -export const Reordering: StoryFn> = ReorderingTemplate.bind({}); - -Reordering.args = { - data, - columns, - getRowId: (item: Item) => item.id, - onRowClick: undefined, - onSelectedChange: undefined, -}; - -const ReorderingTreeTemplate: StoryFn = () => ; - -export const ReorderingTree: StoryFn = ReorderingTreeTemplate.bind({}); - -const VirtualizationTemplate: StoryFn> = (args) => ( - -); - -export const Virtualization: StoryFn> = VirtualizationTemplate.bind({}); - -Virtualization.args = { - data: generateData(300), - columns, - getRowId: (item: Item) => item.id, - onRowClick: undefined, - onSelectedChange: undefined, -}; - -const WindowVirtualizationTemplate: StoryFn> = (args) => ( - -); - -export const WindowVirtualization: StoryFn> = WindowVirtualizationTemplate.bind( - {}, -); - -WindowVirtualization.args = { - data: generateData(300), - columns, - getRowId: (item: Item) => item.id, - onRowClick: undefined, - onSelectedChange: undefined, -}; - -const ReorderingWithVirtualizationTemplate: StoryFn> = (args) => ( - -); - -export const ReorderingWithVirtualization: StoryFn> = - ReorderingWithVirtualizationTemplate.bind({}); - -ReorderingWithVirtualization.args = { - data: generateData(300), - columns, - getRowId: (item: Item) => item.id, - onRowClick: undefined, - onSelectedChange: undefined, -}; - -export const Resizing: StoryFn> = Template.bind({}); - -Resizing.args = { - data, - columns, - getRowId: (item: Item) => item.id, - onSelectedChange: undefined, - onRowClick: undefined, - onColumnSizingChange: undefined, - onColumnSizingInfoChange: undefined, - enableColumnResizing: true, -}; diff --git a/src/components/__stories__/GridDemo.classname.ts b/src/components/__stories__/GridDemo.classname.ts deleted file mode 100644 index 36ba8ec..0000000 --- a/src/components/__stories__/GridDemo.classname.ts +++ /dev/null @@ -1,3 +0,0 @@ -import {cn} from '@bem-react/classname'; - -export const cnGridDemo = cn('grid-demo'); diff --git a/src/components/__stories__/GridDemo.scss b/src/components/__stories__/GridDemo.scss deleted file mode 100644 index 259c8c6..0000000 --- a/src/components/__stories__/GridDemo.scss +++ /dev/null @@ -1,10 +0,0 @@ -.grid-demo { - &-header-groups-grid { - border-collapse: collapse; - - th, - td { - border: 1px solid var(--g-color-line-generic); - } - } -} diff --git a/src/components/__stories__/GroupingDemo.tsx b/src/components/__stories__/GroupingDemo.tsx index 2b01918..e403412 100644 --- a/src/components/__stories__/GroupingDemo.tsx +++ b/src/components/__stories__/GroupingDemo.tsx @@ -1,30 +1,30 @@ import React from 'react'; +import type {ExpandedState, Row} from '@tanstack/react-table'; + +import {useTable} from '../../hooks'; import {Table} from '../Table'; -import type {TableProps} from '../Table'; import type {GroupOrItem} from './constants/grouping'; import {columns, data} from './constants/grouping'; -const getSubRows = (item: GroupOrItem) => ('items' in item ? item.items : undefined); -const getGroupTitle: TableProps['getGroupTitle'] = (row) => row.getValue('name'); -const getRowId = (item: GroupOrItem) => item.id; -const checkIsGroupRow: TableProps['checkIsGroupRow'] = (row) => - 'items' in row.original; +const checkIsGroupRow = (row: Row) => 'items' in row.original; +const getGroupTitle = (row: Row) => row.getValue('name'); + +export const GroupingDemo = () => { + const [expanded, setExpanded] = React.useState({}); -export function GroupingDemo() { - const [expandedIds, setExpandedIds] = React.useState([]); + const table = useTable({ + columns, + data, + enableExpanding: true, + getSubRows: (item) => ('items' in item ? item.items : undefined), + onExpandedChange: setExpanded, + state: { + expanded, + }, + checkIsGroupRow, + }); - return ( - - data={data} - columns={columns} - getSubRows={getSubRows} - getGroupTitle={getGroupTitle} - getRowId={getRowId} - expandedIds={expandedIds} - onExpandedChange={setExpandedIds} - checkIsGroupRow={checkIsGroupRow} - /> - ); -} + return
; +}; diff --git a/src/components/__stories__/GroupingDemo2.tsx b/src/components/__stories__/GroupingDemo2.tsx index 48094dd..5dd3eb5 100644 --- a/src/components/__stories__/GroupingDemo2.tsx +++ b/src/components/__stories__/GroupingDemo2.tsx @@ -1,33 +1,35 @@ import React from 'react'; -import type {TableProps} from '../Table'; +import type {ExpandedState, Row} from '@tanstack/react-table'; + +import {useTable} from '../../hooks'; import {Table} from '../Table'; import {columns} from './constants/columns'; import type {Item} from './types'; import {generateData} from './utils'; -const getGroupTitle: TableProps['getGroupTitle'] = (row) => row.getValue('name'); -const getRowId = (item: Item) => item.id; -const checkIsGroupRow: TableProps['checkIsGroupRow'] = (row) => row.getIsGrouped(); - const data = generateData(300); -const grouping: (keyof Item)[] = ['status', 'age']; - -export function GroupingDemo2() { - const [expandedIds, setExpandedIds] = React.useState([]); - - return ( - - data={data} - columns={columns} - getGroupTitle={getGroupTitle} - getRowId={getRowId} - expandedIds={expandedIds} - onExpandedChange={setExpandedIds} - checkIsGroupRow={checkIsGroupRow} - enableGrouping - grouping={grouping} - /> - ); -} +const grouping: Array = ['status', 'age']; + +const checkIsGroupRow = (row: Row) => row.getIsGrouped(); +const getGroupTitle = (row: Row) => row.getValue('name'); + +export const GroupingDemo2 = () => { + const [expanded, setExpanded] = React.useState({}); + + const table = useTable({ + columns, + data, + enableExpanding: true, + enableGrouping: true, + onExpandedChange: setExpanded, + state: { + expanded, + grouping, + }, + checkIsGroupRow, + }); + + return
; +}; diff --git a/src/components/__stories__/GroupingWithSelectionDemo.tsx b/src/components/__stories__/GroupingWithSelectionDemo.tsx index 80db0f7..47bfab3 100644 --- a/src/components/__stories__/GroupingWithSelectionDemo.tsx +++ b/src/components/__stories__/GroupingWithSelectionDemo.tsx @@ -1,34 +1,41 @@ import React from 'react'; -import type {TableProps} from '../Table'; +import type {ColumnDef, ExpandedState, Row, RowSelectionState} from '@tanstack/react-table'; + +import {defaultSelectionColumn} from '../../constants'; +import {useTable} from '../../hooks'; import {Table} from '../Table'; import type {GroupOrItem} from './constants/grouping'; -import {columns, data} from './constants/grouping'; - -const getSubRows = (item: GroupOrItem) => ('items' in item ? item.items : undefined); -const getGroupTitle: TableProps['getGroupTitle'] = (row) => row.getValue('name'); -const getRowId = (item: GroupOrItem) => item.id; - -const checkIsGroupRow: TableProps['checkIsGroupRow'] = (row) => - 'items' in row.original; - -export function GroupingWithSelectionDemo() { - const [expandedIds, setExpandedIds] = React.useState([]); - const [selectedIds, setSelectedIds] = React.useState([]); - - return ( - - data={data} - columns={columns} - getSubRows={getSubRows} - getGroupTitle={getGroupTitle} - getRowId={getRowId} - expandedIds={expandedIds} - onExpandedChange={setExpandedIds} - selectedIds={selectedIds} - onSelectedChange={setSelectedIds} - checkIsGroupRow={checkIsGroupRow} - /> - ); -} +import {data, columns as originalColumns} from './constants/grouping'; + +const columns: ColumnDef[] = [ + defaultSelectionColumn as ColumnDef, + ...originalColumns, +]; + +const checkIsGroupRow = (row: Row) => 'items' in row.original; +const getGroupTitle = (row: Row) => row.getValue('name'); + +export const GroupingWithSelectionDemo = () => { + const [expanded, setExpanded] = React.useState({}); + const [rowSelection, setRowSelection] = React.useState({}); + + const table = useTable({ + columns, + data, + enableExpanding: true, + enableRowSelection: true, + enableMultiRowSelection: true, + getSubRows: (item) => ('items' in item ? item.items : undefined), + onExpandedChange: setExpanded, + onRowSelectionChange: setRowSelection, + state: { + expanded, + rowSelection, + }, + checkIsGroupRow, + }); + + return
; +}; diff --git a/src/components/__stories__/HeaderGroupsDemo.classname.ts b/src/components/__stories__/HeaderGroupsDemo.classname.ts new file mode 100644 index 0000000..55a06b6 --- /dev/null +++ b/src/components/__stories__/HeaderGroupsDemo.classname.ts @@ -0,0 +1,3 @@ +import {cn} from '@bem-react/classname'; + +export const cnHeaderGroupsDemo = cn('header-groups-demo'); diff --git a/src/components/__stories__/HeaderGroupsDemo.scss b/src/components/__stories__/HeaderGroupsDemo.scss new file mode 100644 index 0000000..fa55bae --- /dev/null +++ b/src/components/__stories__/HeaderGroupsDemo.scss @@ -0,0 +1,8 @@ +.header-groups-demo { + border-collapse: collapse; + + th, + td { + border: 1px solid var(--g-color-line-generic); + } +} diff --git a/src/components/__stories__/HeaderGroupsDemo.tsx b/src/components/__stories__/HeaderGroupsDemo.tsx new file mode 100644 index 0000000..425f123 --- /dev/null +++ b/src/components/__stories__/HeaderGroupsDemo.tsx @@ -0,0 +1,58 @@ +import React from 'react'; + +import type {ColumnDef} from '@tanstack/react-table'; + +import {useTable} from '../../hooks'; +import {Table} from '../Table'; + +import {cnHeaderGroupsDemo} from './HeaderGroupsDemo.classname'; +import {columns as nestedColumns} from './constants/columns'; +import {data} from './constants/data'; +import type {Item} from './types'; + +import './HeaderGroupsDemo.scss'; + +const columns: ColumnDef[] = [ + { + id: 'id', + header: 'ID', + accessorKey: 'id', + }, + { + id: 'columns-parent', + header: 'Columns header', + columns: [ + { + id: 'columns', + header: 'Columns', + columns: nestedColumns, + }, + ], + }, + { + id: 'actions', + header: 'Actions', + accessorKey: 'id', + columns: [ + { + id: 'edit', + header: 'Edit', + accessorKey: 'id', + }, + { + id: 'delete', + header: 'Delete', + accessorKey: 'id', + }, + ], + }, +]; + +export const HeaderGroupsDemo = () => { + const table = useTable({ + columns, + data, + }); + + return
; +}; diff --git a/src/components/__stories__/ReorderingDemo.tsx b/src/components/__stories__/ReorderingDemo.tsx index 580a772..50ae7e8 100644 --- a/src/components/__stories__/ReorderingDemo.tsx +++ b/src/components/__stories__/ReorderingDemo.tsx @@ -1,29 +1,41 @@ import React from 'react'; +import type {ColumnDef} from '@tanstack/react-table'; + +import {defaultDragHandleColumn} from '../../constants'; import {withTableReorder} from '../../hocs'; +import {useTable} from '../../hooks'; import type {SortableListDragResult} from '../SortableList'; import {Table} from '../Table'; -import type {TableProps} from '../Table'; + +import {columns as originalColumns} from './constants/columns'; +import {data as originalData} from './constants/data'; +import type {Item} from './types'; const TableWithReordering = withTableReorder(Table); -export const ReorderingDemo = (props: TableProps) => { - const {getRowId} = props; +const columns: ColumnDef[] = [defaultDragHandleColumn as ColumnDef, ...originalColumns]; - const [data, setData] = React.useState(props.data); +export const ReorderingDemo = () => { + const [data, setData] = React.useState(originalData); - const handleDragEnd = React.useCallback( + const table = useTable({ + columns, + data, + getRowId: (item) => item.id, + }); + + const handleReorder = React.useCallback( ({draggedItemKey, baseItemKey}: SortableListDragResult) => { - setData((data) => { - const dataClone = data.slice(); + setData((prevData) => { + const dataClone = prevData.slice(); + + const index = dataClone.findIndex((item) => item.id === draggedItemKey); - const index = dataClone.findIndex((item) => getRowId(item) === draggedItemKey); if (index >= 0) { - const dragged = dataClone.splice(index, 1)[0]!; + const dragged = dataClone.splice(index, 1)[0] as Item; + const insertIndex = dataClone.findIndex((item) => item.id === baseItemKey); - const insertIndex = dataClone.findIndex( - (value) => getRowId(value) === baseItemKey, - ); if (insertIndex >= 0) { dataClone.splice(insertIndex + 1, 0, dragged); } else { @@ -34,8 +46,8 @@ export const ReorderingDemo = (props: TableProps) => { return dataClone; }); }, - [getRowId], + [], ); - return {...props} data={data} onReorder={handleDragEnd} />; + return ; }; diff --git a/src/components/__stories__/ReorderingTreeDemo.tsx b/src/components/__stories__/ReorderingTreeDemo.tsx index e3a1345..0b54996 100644 --- a/src/components/__stories__/ReorderingTreeDemo.tsx +++ b/src/components/__stories__/ReorderingTreeDemo.tsx @@ -1,33 +1,40 @@ import React from 'react'; +import type {ColumnDef, ExpandedState} from '@tanstack/react-table'; + +import {defaultDragHandleColumn} from '../../constants'; import {withTableReorder} from '../../hocs'; +import {useTable} from '../../hooks'; import {Table} from '../Table'; import type {TreeItem} from './constants/tree'; -import {draggableTreeColumns, data as initialData} from './constants/tree'; +import {draggableTreeColumns, data as originalData} from './constants/tree'; import {useTreeDataReordering} from './hooks/useTreeDataReordering'; const TableWithReordering = withTableReorder(Table); -const getSubRows = (item: TreeItem) => item.children; -const getRowId = (item: TreeItem) => item.id; - -export function ReorderingTreeDemo() { - const [expandedIds, setExpandedIds] = React.useState([]); - const [data, setData] = React.useState(initialData); - - const handleDragEnd = useTreeDataReordering({data, setData}); - - return ( - - data={data} - columns={draggableTreeColumns} - getSubRows={getSubRows} - getRowId={getRowId} - expandedIds={expandedIds} - onExpandedChange={setExpandedIds} - onReorder={handleDragEnd} - enableNesting - /> - ); -} +const columns: ColumnDef[] = [ + defaultDragHandleColumn as ColumnDef, + ...draggableTreeColumns, +]; + +export const ReorderingTreeDemo = () => { + const [expanded, setExpanded] = React.useState({}); + const [data, setData] = React.useState(originalData); + + const handleReorder = useTreeDataReordering({data, setData}); + + const table = useTable({ + columns, + data, + enableExpanding: true, + getRowId: (item) => item.id, + getSubRows: (item) => item.children, + onExpandedChange: setExpanded, + state: { + expanded, + }, + }); + + return ; +}; diff --git a/src/components/__stories__/ReorderingWithVirtualizationDemo.tsx b/src/components/__stories__/ReorderingWithVirtualizationDemo.tsx index ca5590c..fd7f9f1 100644 --- a/src/components/__stories__/ReorderingWithVirtualizationDemo.tsx +++ b/src/components/__stories__/ReorderingWithVirtualizationDemo.tsx @@ -1,35 +1,50 @@ import React from 'react'; -import type {TableProps} from '../../components/Table'; -import {Table} from '../../components/Table'; +import type {ColumnDef} from '@tanstack/react-table'; + +import {defaultDragHandleColumn} from '../../constants'; import {withTableReorder} from '../../hocs'; +import {useTable, useWindowRowVirtualizer} from '../../hooks'; +import {getVirtualRowRangeExtractor} from '../../utils'; import type {SortableListDragResult} from '../SortableList'; -import {WindowVirtualizationProvider} from '../WindowVirtualizationProvider'; +import {Table} from '../Table'; + +import {columns as originalColumns} from './constants/columns'; +import type {Item} from './types'; +import {generateData} from './utils'; -const GridWithReordering = withTableReorder(Table); +const TableWithReordering = withTableReorder(Table); -const rowHeight = 20; -const estimateRowSize = () => rowHeight; +const columns: ColumnDef[] = [defaultDragHandleColumn as ColumnDef, ...originalColumns]; -export const ReorderingWithVirtualizationDemo = ( - props: TableProps, -) => { - const {getRowId} = props; +export const ReorderingWithVirtualizationDemo = () => { + const tableRef = React.useRef(null); + const [data, setData] = React.useState(() => generateData(300)); - const [data, setData] = React.useState(props.data); + const table = useTable({ + columns, + data, + getRowId: (item) => item.id, + }); - const handleDragEnd = React.useCallback( + const rowVirtualizer = useWindowRowVirtualizer({ + count: table.getRowModel().rows.length, + estimateSize: () => 20, + overscan: 5, + rangeExtractor: getVirtualRowRangeExtractor(tableRef.current), + }); + + const handleReorder = React.useCallback( ({draggedItemKey, baseItemKey}: SortableListDragResult) => { - setData((data) => { - const dataClone = data.slice(); + setData((prevData) => { + const dataClone = prevData.slice(); + + const index = dataClone.findIndex((item) => item.id === draggedItemKey); - const index = dataClone.findIndex((item) => getRowId(item) === draggedItemKey); if (index >= 0) { - const dragged = dataClone.splice(index, 1)[0]!; + const dragged = dataClone.splice(index, 1)[0] as Item; + const insertIndex = dataClone.findIndex((item) => item.id === baseItemKey); - const insertIndex = dataClone.findIndex( - (value) => getRowId(value) === baseItemKey, - ); if (insertIndex >= 0) { dataClone.splice(insertIndex + 1, 0, dragged); } else { @@ -40,16 +55,15 @@ export const ReorderingWithVirtualizationDemo = ( return dataClone; }); }, - [getRowId], + [], ); return ( - - {...props} data={data} onReorder={handleDragEnd} /> - + ); }; diff --git a/src/components/__stories__/ResizingDemo.tsx b/src/components/__stories__/ResizingDemo.tsx new file mode 100644 index 0000000..6d4e202 --- /dev/null +++ b/src/components/__stories__/ResizingDemo.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import {useTable} from '../../hooks'; +import {Table} from '../Table'; + +import {columns} from './constants/columns'; +import {data} from './constants/data'; + +export const ResizingDemo = () => { + const table = useTable({ + columns, + data, + enableColumnResizing: true, + columnResizeMode: 'onChange', + }); + + return
; +}; diff --git a/src/components/__stories__/SortingDemo.tsx b/src/components/__stories__/SortingDemo.tsx index f3ed635..7ab3911 100644 --- a/src/components/__stories__/SortingDemo.tsx +++ b/src/components/__stories__/SortingDemo.tsx @@ -1,10 +1,26 @@ import React from 'react'; -import type {TableProps} from '../Table'; +import type {SortingState} from '@tanstack/react-table'; + +import {useTable} from '../../hooks'; import {Table} from '../Table'; -export const SortingDemo = (props: TableProps) => { - const [sorting, setSorting] = React.useState['sorting']>>([]); +import {columns} from './constants/columns'; +import {data} from './constants/data'; + +export const SortingDemo = () => { + const [sorting, setSorting] = React.useState([]); + + const table = useTable({ + columns, + data, + enableSorting: true, + getRowId: (item) => item.id, + onSortingChange: setSorting, + state: { + sorting, + }, + }); - return
; + return
; }; diff --git a/src/components/__stories__/Table.stories.tsx b/src/components/__stories__/Table.stories.tsx new file mode 100644 index 0000000..ee3adff --- /dev/null +++ b/src/components/__stories__/Table.stories.tsx @@ -0,0 +1,75 @@ +import React from 'react'; + +import type {Meta, StoryFn} from '@storybook/react'; + +import {Table} from '../Table'; + +import {DefaultDemo} from './DefaultDemo'; +import {GroupingDemo} from './GroupingDemo'; +import {GroupingDemo2} from './GroupingDemo2'; +import {GroupingWithSelectionDemo} from './GroupingWithSelectionDemo'; +import {HeaderGroupsDemo} from './HeaderGroupsDemo'; +import {ReorderingDemo} from './ReorderingDemo'; +import {ReorderingTreeDemo} from './ReorderingTreeDemo'; +import {ReorderingWithVirtualizationDemo} from './ReorderingWithVirtualizationDemo'; +import {ResizingDemo} from './ResizingDemo'; +import {SortingDemo} from './SortingDemo'; +import {TreeDemo} from './TreeDemo'; +import {TreeWithGroupsDemo} from './TreeWithGroupsDemo'; +import {VirtualizationDemo} from './VirtualizationDemo'; +import {WindowVirtualizationDemo} from './WindowVirtualizationDemo'; +import {WithSelectionDemo} from './WithSelectionDemo'; +import {WithoutHeaderDemo} from './WithoutHeaderDemo'; + +export default { + title: 'Table', + component: Table, +} as Meta; + +const DefaultTemplate: StoryFn = () => ; +export const Default: StoryFn = DefaultTemplate.bind({}); + +const WithoutHeaderTemplate: StoryFn = () => ; +export const WithoutHeader: StoryFn = WithoutHeaderTemplate.bind({}); + +const HeaderGroupsTemplate: StoryFn = () => ; +export const HeaderGroups: StoryFn = HeaderGroupsTemplate.bind({}); + +const WithSelectionTemplate: StoryFn = () => ; +export const WithSelection: StoryFn = WithSelectionTemplate.bind({}); + +const SortingTemplate: StoryFn = () => ; +export const Sorting: StoryFn = SortingTemplate.bind({}); + +const GroupingTemplate: StoryFn = () => ; +export const Grouping: StoryFn = GroupingTemplate.bind({}); + +const GroupingTemplate2: StoryFn = () => ; +export const Grouping2: StoryFn = GroupingTemplate2.bind({}); + +const GroupingWithSelectionTemplate: StoryFn = () => ; +export const GroupingWithSelection: StoryFn = GroupingWithSelectionTemplate.bind({}); + +const TreeTemplate: StoryFn = () => ; +export const Tree: StoryFn = TreeTemplate.bind({}); + +const TreeWithGroupsTemplate: StoryFn = () => ; +export const TreeWithGroups: StoryFn = TreeWithGroupsTemplate.bind({}); + +const ReorderingTemplate: StoryFn = () => ; +export const Reordering: StoryFn = ReorderingTemplate.bind({}); + +const ReorderingTreeTemplate: StoryFn = () => ; +export const ReorderingTree: StoryFn = ReorderingTreeTemplate.bind({}); + +const VirtualizationTemplate: StoryFn = () => ; +export const Virtualization: StoryFn = VirtualizationTemplate.bind({}); + +const WindowVirtualizationTemplate: StoryFn = () => ; +export const WindowVirtualization: StoryFn = WindowVirtualizationTemplate.bind({}); + +const ReorderingWithVirtualizationTemplate: StoryFn = () => ; +export const ReorderingWithVirtualization: StoryFn = ReorderingWithVirtualizationTemplate.bind({}); + +const ResizingTemplate: StoryFn = () => ; +export const Resizing: StoryFn = ResizingTemplate.bind({}); diff --git a/src/components/__stories__/TreeDemo.tsx b/src/components/__stories__/TreeDemo.tsx index e4d69d6..e6c5c1c 100644 --- a/src/components/__stories__/TreeDemo.tsx +++ b/src/components/__stories__/TreeDemo.tsx @@ -1,24 +1,25 @@ import React from 'react'; +import type {ExpandedState} from '@tanstack/react-table'; + +import {useTable} from '../../hooks'; import {Table} from '../Table'; -import type {TreeItem} from './constants/tree'; import {columns, data} from './constants/tree'; -const getSubRows = (item: TreeItem) => item.children; -const getRowId = (item: TreeItem) => item.id; +export const TreeDemo = () => { + const [expanded, setExpanded] = React.useState({}); -export function TreeDemo() { - const [expandedIds, setExpandedIds] = React.useState([]); + const table = useTable({ + columns, + data, + getSubRows: (item) => item.children, + enableExpanding: true, + onExpandedChange: setExpanded, + state: { + expanded, + }, + }); - return ( - - data={data} - columns={columns} - getSubRows={getSubRows} - getRowId={getRowId} - expandedIds={expandedIds} - onExpandedChange={setExpandedIds} - /> - ); -} + return
; +}; diff --git a/src/components/__stories__/TreeWithGroupsDemo.tsx b/src/components/__stories__/TreeWithGroupsDemo.tsx index 2aaafec..e43a0ca 100644 --- a/src/components/__stories__/TreeWithGroupsDemo.tsx +++ b/src/components/__stories__/TreeWithGroupsDemo.tsx @@ -1,30 +1,30 @@ import React from 'react'; -import type {TableProps} from '../Table'; +import type {ExpandedState, Row} from '@tanstack/react-table'; + +import {useTable} from '../../hooks'; import {Table} from '../Table'; import type {TreeGroupItem} from './constants/tree'; import {groupsColumns, groupsData} from './constants/tree'; -const getSubRows = (item: TreeGroupItem) => ('items' in item ? item.items : item.children); -const getGroupTitle: TableProps['getGroupTitle'] = (row) => row.getValue('name'); -const getRowId = (item: TreeGroupItem) => item.id; -const checkIsGroupRow: TableProps['checkIsGroupRow'] = (row) => - 'items' in row.original; +const checkIsGroupRow = (row: Row) => 'items' in row.original; +const getGroupTitle = (row: Row) => row.getValue('name'); + +export const TreeWithGroupsDemo = () => { + const [expanded, setExpanded] = React.useState({}); -export function TreeWithGroupsDemo() { - const [expandedIds, setExpandedIds] = React.useState([]); + const table = useTable({ + columns: groupsColumns, + data: groupsData, + enableExpanding: true, + getSubRows: (item) => ('items' in item ? item.items : item.children), + onExpandedChange: setExpanded, + state: { + expanded, + }, + checkIsGroupRow, + }); - return ( - - data={groupsData} - columns={groupsColumns} - getSubRows={getSubRows} - getGroupTitle={getGroupTitle} - getRowId={getRowId} - expandedIds={expandedIds} - onExpandedChange={setExpandedIds} - checkIsGroupRow={checkIsGroupRow} - /> - ); -} + return
; +}; diff --git a/src/components/__stories__/VirtualizationDemo.tsx b/src/components/__stories__/VirtualizationDemo.tsx index 072fa4d..9a0305b 100644 --- a/src/components/__stories__/VirtualizationDemo.tsx +++ b/src/components/__stories__/VirtualizationDemo.tsx @@ -1,21 +1,32 @@ import React from 'react'; -import {withTableVirtualization} from '../../hocs'; +import {useRowVirtualizer, useTable} from '../../hooks'; import {Table} from '../Table'; -import type {TableProps} from '../Table'; -const TableWithVirtualization = withTableVirtualization(Table); +import {columns} from './constants/columns'; +import {generateData} from './utils'; -const rowHeight = 20; -const estimateRowSize = () => rowHeight; +const data = generateData(300); + +export const VirtualizationDemo = () => { + const table = useTable({ + columns, + data, + getRowId: (item) => item.id, + }); + + const containerRef = React.useRef(null); + + const rowVirtualizer = useRowVirtualizer({ + count: table.getRowModel().rows.length, + estimateSize: () => 20, + overscan: 5, + getScrollElement: () => containerRef.current, + }); -export const VirtualizationDemo = (props: TableProps) => { return ( - - {...props} - estimateRowSize={estimateRowSize} - overscanRowCount={5} - containerHeight="90vh" - /> +
+
+ ); }; diff --git a/src/components/__stories__/WindowVirtualizationDemo.tsx b/src/components/__stories__/WindowVirtualizationDemo.tsx index 9d2c904..f8b5ec7 100644 --- a/src/components/__stories__/WindowVirtualizationDemo.tsx +++ b/src/components/__stories__/WindowVirtualizationDemo.tsx @@ -1,20 +1,25 @@ import React from 'react'; -import {withTableWindowVirtualization} from '../../hocs'; -import type {TableProps} from '../Table'; +import {useTable, useWindowRowVirtualizer} from '../../hooks'; import {Table} from '../Table'; -const TableWithWindowVirtualization = withTableWindowVirtualization(Table); +import {columns} from './constants/columns'; +import {generateData} from './utils'; -const rowHeight = 20; -const estimateRowSize = () => rowHeight; +const data = generateData(300); -export const WindowVirtualizationDemo = (props: TableProps) => { - return ( - - {...props} - estimateRowSize={estimateRowSize} - overscanRowCount={5} - /> - ); +export const WindowVirtualizationDemo = () => { + const table = useTable({ + columns, + data, + getRowId: (item) => item.id, + }); + + const rowVirtualizer = useWindowRowVirtualizer({ + count: table.getRowModel().rows.length, + estimateSize: () => 20, + overscan: 5, + }); + + return
; }; diff --git a/src/components/__stories__/WithSelectionDemo.tsx b/src/components/__stories__/WithSelectionDemo.tsx index 82d33a5..2979a7a 100644 --- a/src/components/__stories__/WithSelectionDemo.tsx +++ b/src/components/__stories__/WithSelectionDemo.tsx @@ -1,10 +1,30 @@ import React from 'react'; -import type {TableProps} from '../Table'; +import type {ColumnDef, RowSelectionState} from '@tanstack/react-table'; + +import {defaultSelectionColumn} from '../../constants'; +import {useTable} from '../../hooks'; import {Table} from '../Table'; -export const WithSelectionDemo = (props: TableProps) => { - const [selectedIds, setSelectedIds] = React.useState([]); +import {columns as originalColumns} from './constants/columns'; +import {data} from './constants/data'; +import type {Item} from './types'; + +const columns: ColumnDef[] = [defaultSelectionColumn as ColumnDef, ...originalColumns]; + +export const WithSelectionDemo = () => { + const [rowSelection, setRowSelection] = React.useState({}); + + const table = useTable({ + columns, + data, + enableRowSelection: true, + enableMultiRowSelection: true, + onRowSelectionChange: setRowSelection, + state: { + rowSelection, + }, + }); - return
; + return
; }; diff --git a/src/components/__stories__/WithoutHeaderDemo.tsx b/src/components/__stories__/WithoutHeaderDemo.tsx new file mode 100644 index 0000000..6a5dcdb --- /dev/null +++ b/src/components/__stories__/WithoutHeaderDemo.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +import {useTable} from '../../hooks'; +import {Table} from '../Table'; + +import {columns} from './constants/columns'; +import {data} from './constants/data'; + +export const WithoutHeaderDemo = () => { + const table = useTable({ + columns, + data, + }); + + return
; +}; diff --git a/src/components/__stories__/constants/columns.tsx b/src/components/__stories__/constants/columns.tsx index b5361c3..12f17e9 100644 --- a/src/components/__stories__/constants/columns.tsx +++ b/src/components/__stories__/constants/columns.tsx @@ -1,9 +1,10 @@ import React from 'react'; -import type {TableProps} from '../../Table'; +import type {ColumnDef} from '@tanstack/react-table'; + import type {Item} from '../types'; -export const columns: TableProps['columns'] = [ +export const columns: ColumnDef[] = [ {accessorKey: 'name', header: 'Name', size: 100}, {accessorKey: 'age', header: 'Age', size: 100}, { diff --git a/src/components/__stories__/constants/grouping.ts b/src/components/__stories__/constants/grouping.ts index c2e58b1..589eeb4 100644 --- a/src/components/__stories__/constants/grouping.ts +++ b/src/components/__stories__/constants/grouping.ts @@ -1,4 +1,5 @@ -import type {TableProps} from '../../Table'; +import type {ColumnDef} from '@tanstack/react-table'; + import type {Item} from '../types'; export interface Group { @@ -9,7 +10,7 @@ export interface Group { export type GroupOrItem = Group | Item; -export const columns: TableProps['columns'] = [ +export const columns: ColumnDef[] = [ {accessorKey: 'name', header: 'Name', size: 200}, {accessorKey: 'age', header: 'Age', size: 100}, ]; diff --git a/src/components/__stories__/constants/tree.tsx b/src/components/__stories__/constants/tree.tsx index 666714b..ca05225 100644 --- a/src/components/__stories__/constants/tree.tsx +++ b/src/components/__stories__/constants/tree.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import type {TableProps} from '../../Table'; +import type {ColumnDef} from '@tanstack/react-table'; + import {DraggableTreeNameCell} from '../cells/DraggableTreeNameCell'; import {TreeNameCell} from '../cells/TreeNameCell'; import type {Item} from '../types'; @@ -9,7 +10,7 @@ export interface TreeItem extends Item { children?: TreeItem[]; } -export const columns: TableProps['columns'] = [ +export const columns: ColumnDef[] = [ { accessorKey: 'name', header: 'Name', @@ -21,7 +22,7 @@ export const columns: TableProps['columns'] = [ {accessorKey: 'age', header: 'Age', size: 100}, ]; -export const draggableTreeColumns: TableProps['columns'] = [ +export const draggableTreeColumns: ColumnDef[] = [ { accessorKey: 'name', header: 'Name', @@ -122,4 +123,4 @@ export const groupsData = [ }, ]; -export const groupsColumns = columns as TableProps['columns']; +export const groupsColumns = columns as ColumnDef[]; diff --git a/src/components/__stories__/hooks/useTreeDataReordering.ts b/src/components/__stories__/hooks/useTreeDataReordering.ts index 8161e13..fe44bbc 100644 --- a/src/components/__stories__/hooks/useTreeDataReordering.ts +++ b/src/components/__stories__/hooks/useTreeDataReordering.ts @@ -17,7 +17,7 @@ type UpdateRankPayload = { before?: string; }; -export function useTreeDataReordering({data, setData}: UseTreeDataReorderingProps) { +export const useTreeDataReordering = ({data, setData}: UseTreeDataReorderingProps) => { return React.useCallback( // eslint-disable-next-line complexity ({ @@ -197,4 +197,4 @@ export function useTreeDataReordering({data, setData}: UseTreeDataReorderingProp }, [data, setData], ); -} +}; diff --git a/src/components/__stories__/utils.ts b/src/components/__stories__/utils.ts index 4c3a18b..26effaf 100644 --- a/src/components/__stories__/utils.ts +++ b/src/components/__stories__/utils.ts @@ -1,10 +1,10 @@ import type {Item} from './types'; -export function generateData(length: number) { +export const generateData = (length: number) => { return Array.from({length}, (_item, index) => ({ id: index.toString(), name: `Item ${index}`, age: (index * 2) % 100, status: ['free', 'busy', 'unknown'][index % 3], })) as Item[]; -} +}; diff --git a/src/components/index.ts b/src/components/index.ts index 19c9f5d..57894de 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -6,7 +6,6 @@ export * from './DragHandle'; export * from './HeaderCell'; export * from './HeaderRow'; export * from './ReorderingProvider'; -export * from './Row'; export * from './SortableList'; export * from './SortableListContext'; export * from './SortableListDndContext'; @@ -14,8 +13,3 @@ export * from './SortIndicator'; export * from './Table'; export * from './TableContext'; export * from './TableContextProvider'; -export * from './VirtualizationContainer'; -export * from './VirtualizationContext'; -export * from './VirtualizationProvider'; -export * from './VirtualRow'; -export * from './WindowVirtualizationProvider'; diff --git a/src/components/utils/flattenTableData.ts b/src/components/utils/flattenTableData.ts deleted file mode 100644 index 8991c58..0000000 --- a/src/components/utils/flattenTableData.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type {TableProps} from '../Table'; - -export interface FlattenTableDataParams - extends Pick, 'data' | 'expandedIds' | 'getSubRows' | 'getRowId'> { - transformItem: (item: TData) => TTransformedData; -} - -export function flattenTableData({ - data, - transformItem = (item) => item as unknown as TTransformedData, - getSubRows, - expandedIds, - getRowId, -}: FlattenTableDataParams): TTransformedData[] { - const result: TTransformedData[] = []; - - for (const item of data) { - result.push(transformItem(item)); - - const subItems = getSubRows?.(item); - if (subItems && expandedIds?.includes(getRowId(item))) { - result.push( - ...flattenTableData({ - data: subItems, - getSubRows, - expandedIds, - transformItem, - getRowId, - }), - ); - } - } - - return result; -} diff --git a/src/components/utils/getVirtualRangeExtractor.ts b/src/components/utils/getVirtualRangeExtractor.ts deleted file mode 100644 index b2994ec..0000000 --- a/src/components/utils/getVirtualRangeExtractor.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {defaultRangeExtractor} from '@tanstack/react-virtual'; -import type {Range} from '@tanstack/react-virtual'; - -export function getVirtualRangeExtractor(container?: HTMLElement | null) { - return function (range: Range) { - const table = container?.querySelector('[data-draggable-index]'); - const draggableItemIndex = Number(table?.getAttribute('data-draggable-index') ?? -1); - - const result = defaultRangeExtractor(range); - - if (draggableItemIndex !== -1 && !result.includes(draggableItemIndex)) { - result.push(draggableItemIndex); - } - - return result; - }; -} diff --git a/src/components/utils/renderDefaultGroupHeader.tsx b/src/components/utils/renderDefaultGroupHeader.tsx deleted file mode 100644 index 4de2edc..0000000 --- a/src/components/utils/renderDefaultGroupHeader.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; - -import {ArrowToggle} from '@gravity-ui/uikit'; -import type {Row} from '@tanstack/react-table'; - -import {b} from '../Table/Table.classname'; - -export function renderDefaultGroupHeader( - row: Row, - getGroupTitle?: (row: Row) => React.ReactNode, -) { - return ( -
-

- -

-
- ); -} diff --git a/src/components/utils/renderDefaultSortIndicator.tsx b/src/components/utils/renderDefaultSortIndicator.tsx deleted file mode 100644 index fba6782..0000000 --- a/src/components/utils/renderDefaultSortIndicator.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; - -import type {Header} from '@tanstack/react-table'; - -import {SortIndicator} from '../SortIndicator'; -import {b} from '../Table/Table.classname'; - -export function renderDefaultSortIndicator( - header: Header, - className?: string, -) { - return ( - - ); -} diff --git a/src/constants/defaultSelectionColumn.tsx b/src/constants/defaultSelectionColumn.tsx index 495b03f..1462236 100644 --- a/src/constants/defaultSelectionColumn.tsx +++ b/src/constants/defaultSelectionColumn.tsx @@ -9,6 +9,7 @@ export const defaultSelectionColumn: ColumnDef = { diff --git a/src/demo/DocsDecorator/DocsDecorator.tsx b/src/demo/DocsDecorator/DocsDecorator.tsx index ba012c6..1c33d74 100644 --- a/src/demo/DocsDecorator/DocsDecorator.tsx +++ b/src/demo/DocsDecorator/DocsDecorator.tsx @@ -5,7 +5,7 @@ import {DocsContainer} from '@storybook/blocks'; import type {DocsContainerProps} from '@storybook/blocks'; import {themes} from '../../../.storybook/theme'; -import {cn} from '../../components/utils/cn'; +import {cn} from '../../utils'; import './DocsDecorator.scss'; diff --git a/src/hocs/index.ts b/src/hocs/index.ts index b4fa519..baf881b 100644 --- a/src/hocs/index.ts +++ b/src/hocs/index.ts @@ -1,3 +1 @@ export * from './withTableReorder'; -export * from './withTableVirtualization'; -export * from './withTableWindowVirtualization'; diff --git a/src/hocs/withTableReorder.tsx b/src/hocs/withTableReorder.tsx index 98e12cd..db30229 100644 --- a/src/hocs/withTableReorder.tsx +++ b/src/hocs/withTableReorder.tsx @@ -3,33 +3,47 @@ import React from 'react'; import {ReorderingProvider} from '../components'; import type {ReorderingProviderProps, TableProps} from '../components'; -export interface WithTableReorderProps - extends TableProps, +export interface WithTableReorderProps< + TData, + TScrollElement extends Element | Window = HTMLDivElement, +> extends TableProps, Pick, 'onReorder' | 'dndModifiers' | 'enableNesting'> {} export function withTableReorder( - Component: ( - props: TableProps & ExtraProps, + Component: ( + props: TableProps & {ref?: React.Ref}, ) => React.ReactElement, ) { - const TableWithReorder = (props: WithTableReorderProps) => { - const {data, getRowId, getSubRows, expandedIds, enableNesting, onReorder, dndModifiers} = - props; - - return ( - - {...props} /> - - ); - }; + const TableWithReorder = React.forwardRef( + ( + { + table, + dndModifiers, + enableNesting, + onReorder, + ...restProps + }: WithTableReorderProps, + ref: React.Ref, + ) => { + return ( + + + + ); + }, + ) as (( + props: WithTableReorderProps & {ref?: React.Ref}, + ) => React.ReactElement) & {displayName: string}; TableWithReorder.displayName = `withTableReorder(${Component.name})`; diff --git a/src/hocs/withTableVirtualization.tsx b/src/hocs/withTableVirtualization.tsx deleted file mode 100644 index 664882f..0000000 --- a/src/hocs/withTableVirtualization.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; - -import type {TableProps, VirtualizationProviderProps} from '../components'; -import {VirtualizationProvider} from '../components'; - -export interface WithTableVirtualizationProps - extends TableProps, - Pick< - VirtualizationProviderProps, - 'overscanRowCount' | 'containerClassName' | 'estimateRowSize' | 'containerHeight' - > {} - -export function withTableVirtualization( - Component: ( - props: TableProps & ExtraProps, - ) => React.ReactElement, -) { - const TableWithVirtualization = (props: WithTableVirtualizationProps) => { - const {data, overscanRowCount, containerClassName, containerHeight, estimateRowSize} = - props; - - return ( - - {...props} /> - - ); - }; - - TableWithVirtualization.displayName = `withTableVirtualization(${Component.name})`; - - return TableWithVirtualization; -} diff --git a/src/hocs/withTableWindowVirtualization.tsx b/src/hocs/withTableWindowVirtualization.tsx deleted file mode 100644 index d9531c4..0000000 --- a/src/hocs/withTableWindowVirtualization.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; - -import type {TableProps, WindowVirtualizationProviderProps} from '../components'; -import {WindowVirtualizationProvider} from '../components'; - -export interface WithTableWindowVirtualizationProps - extends TableProps, - Pick< - WindowVirtualizationProviderProps, - 'overscanRowCount' | 'containerClassName' | 'estimateRowSize' - > {} - -export function withTableWindowVirtualization( - Component: ( - props: TableProps & ExtraProps, - ) => React.ReactElement, -) { - const TableWithWindowVirtualization = ( - props: WithTableWindowVirtualizationProps, - ) => { - const {data, overscanRowCount, containerClassName, estimateRowSize} = props; - - return ( - - {...props} /> - - ); - }; - - TableWithWindowVirtualization.displayName = `withTableWindowVirtualization(${Component.name})`; - - return TableWithWindowVirtualization; -} diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 43022a7..57354fb 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,6 +1,6 @@ -export * from './useColumns'; export * from './useDraggableRowDepth'; export * from './useDraggableRowStyle'; -export * from './useExpanded'; -export * from './useSelection'; +export * from './useRowVirtualizer'; export * from './useSortableList'; +export * from './useTable'; +export * from './useWindowRowVirtualizer'; diff --git a/src/hooks/useColumns.ts b/src/hooks/useColumns.ts deleted file mode 100644 index f367ed2..0000000 --- a/src/hooks/useColumns.ts +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; - -import type {ColumnDef} from '@tanstack/react-table'; - -export interface UseColumnsParams { - columns: ColumnDef[]; - dragHandleColumn: ColumnDef; - draggable?: boolean; - enableRowSelection?: boolean; - selectionColumn: ColumnDef; -} - -export function useColumns({ - columns: providedColumns, - dragHandleColumn, - draggable, - enableRowSelection, - selectionColumn, -}: UseColumnsParams) { - return React.useMemo(() => { - const result = [...providedColumns]; - - if (enableRowSelection) { - result.unshift(selectionColumn); - } - - if (draggable) { - result.unshift(dragHandleColumn); - } - - return result; - }, [providedColumns, enableRowSelection, draggable, selectionColumn, dragHandleColumn]); -} diff --git a/src/hooks/useExpanded.ts b/src/hooks/useExpanded.ts deleted file mode 100644 index 3530a57..0000000 --- a/src/hooks/useExpanded.ts +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; - -import {getExpandedRowModel} from '@tanstack/react-table'; -import type {ExpandedOptions, ExpandedStateList} from '@tanstack/react-table'; - -export interface UseExpandedParams { - expandedIds?: string[]; - onExpandedChange?: (expandedIds: string[]) => void; -} - -export function useExpanded({expandedIds, onExpandedChange}: UseExpandedParams) { - const expanded = React.useMemo(() => { - const result: Record = {}; - - for (const id of expandedIds ?? []) { - result[id] = true; - } - - return result; - }, [expandedIds]); - - const handleExpandedChange = React.useCallback< - NonNullable['onExpandedChange']> - >( - (getNewExpanded) => { - const newExpanded = ( - typeof getNewExpanded === 'function' ? getNewExpanded(expanded) : getNewExpanded - ) as ExpandedStateList; - - onExpandedChange?.(Object.keys(newExpanded).filter((key) => newExpanded[key])); - }, - [onExpandedChange, expanded], - ); - - const enableExpanding = Boolean(onExpandedChange); - - return { - enableExpanding, - getExpandedRowModel: enableExpanding ? getExpandedRowModel() : undefined, - expanded, - handleExpandedChange, - }; -} diff --git a/src/hooks/useRowVirtualizer.ts b/src/hooks/useRowVirtualizer.ts new file mode 100644 index 0000000..095f73a --- /dev/null +++ b/src/hooks/useRowVirtualizer.ts @@ -0,0 +1,11 @@ +import {useVirtualizer as useTanstackVirtualizer} from '@tanstack/react-virtual'; + +export type UseRowVirtualizerOptions = Parameters< + typeof useTanstackVirtualizer +>[0]; + +export const useRowVirtualizer = ( + options: UseRowVirtualizerOptions, +) => { + return useTanstackVirtualizer(options); +}; diff --git a/src/hooks/useSelection.ts b/src/hooks/useSelection.ts deleted file mode 100644 index 7fb3904..0000000 --- a/src/hooks/useSelection.ts +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; - -import type {RowSelectionOptions, RowSelectionState} from '@tanstack/react-table'; - -export interface UseSelectionParams { - selectedIds?: string[]; - onSelectedChange?: (selectedIds: string[]) => void; -} - -export function useSelection({selectedIds, onSelectedChange}: UseSelectionParams) { - const rowSelection = React.useMemo(() => { - const result: Record = {}; - - for (const id of selectedIds ?? []) { - result[id] = true; - } - - return result; - }, [selectedIds]); - - const handleRowSelectionChange = React.useCallback< - NonNullable['onRowSelectionChange']> - >( - (getNewRowSelection) => { - const newRowSelection = ( - getNewRowSelection as (oldSelection: RowSelectionState) => RowSelectionState - )(rowSelection); - - onSelectedChange?.(Object.keys(newRowSelection).filter((key) => newRowSelection[key])); - }, - [onSelectedChange, rowSelection], - ); - - return { - enableRowSelection: Boolean(onSelectedChange), - enableMultiRowSelection: Boolean(onSelectedChange), - rowSelection, - handleRowSelectionChange, - }; -} diff --git a/src/hooks/useSortableList.ts b/src/hooks/useSortableList.ts index 2ff8c27..55fb292 100644 --- a/src/hooks/useSortableList.ts +++ b/src/hooks/useSortableList.ts @@ -1,7 +1,6 @@ import React from 'react'; import {useDndMonitor} from '@dnd-kit/core'; -import type {DragEndEvent, DragMoveEvent, DragOverEvent, DragStartEvent} from '@dnd-kit/core'; import type {SortableListDragResult} from '../components'; @@ -18,16 +17,34 @@ export const useSortableList = ({ items, onDragStart, onDragEnd, - nestingEnabled = true, + nestingEnabled, childModeOffset = 20, nextChildModeOffset = 10, }: UseSortableListParams) => { const [activeItemKey, setActiveItemKey] = React.useState(null); - const [targetItemIndex, setTargetItemIndex] = React.useState(-1); + const [targetItemIndex, setTargetItemIndex] = React.useState(-1); const [isParentMode, setIsParentMode] = React.useState(false); const [isChildMode, setIsChildMode] = React.useState(false); const [isNextChildMode, setIsNextChildMode] = React.useState(false); + const itemIndexMap = React.useMemo(() => { + const map = new Map(); + + items.forEach((item, index) => { + map.set(item, index); + }); + + return map; + }, [items]); + + const getItemIndex = (item?: string | null) => { + if (typeof item !== 'undefined' && item !== null) { + return itemIndexMap.get(item) ?? -1; + } + + return -1; + }; + const resetState = () => { setActiveItemKey(null); setTargetItemIndex(-1); @@ -36,55 +53,58 @@ export const useSortableList = ({ setIsNextChildMode(false); }; - const getTargetItemKey = (activeItemKey: string, overItemKey?: string) => { - if (overItemKey) { - const isForward = items.indexOf(overItemKey) > items.indexOf(activeItemKey); + const getTargetItemKey = (activeId: string, overId?: string) => { + if (overId) { + const overItemIndex = getItemIndex(overId); + const activeItemIndex = getItemIndex(activeId); - if (!isForward) { - const overItemIndex = items.indexOf(overItemKey); + if (overItemIndex <= activeItemIndex) { return items[overItemIndex - 1]; } } - return overItemKey; + return overId; }; useDndMonitor({ - onDragStart: ({active: {id: activeId}}: DragStartEvent) => { - setActiveItemKey(activeId as string); - onDragStart?.(activeId as string); + onDragStart: (event) => { + setActiveItemKey(event.active.id as string); + onDragStart?.(event.active.id as string); document.body.style.setProperty('cursor', 'grabbing'); }, - onDragMove: ({delta}: DragMoveEvent) => { + onDragMove: (event) => { if (nestingEnabled) { - setIsParentMode(delta.x < -childModeOffset); - setIsChildMode(delta.x > childModeOffset); - setIsNextChildMode(delta.x > nextChildModeOffset && delta.x <= childModeOffset); + setIsParentMode(event.delta.x < -childModeOffset); + setIsChildMode(event.delta.x > childModeOffset); + + setIsNextChildMode( + event.delta.x > nextChildModeOffset && event.delta.x <= childModeOffset, + ); } }, - onDragOver: ({active, over}: DragOverEvent) => { - const activeItemKey = active.id; - - const targetItemKey = getTargetItemKey(activeItemKey as string, over?.id as string); - - const targetItemIndex = - targetItemKey && targetItemKey !== activeItemKey - ? items.indexOf(targetItemKey) - : -1; - - setTargetItemIndex(targetItemIndex); + onDragOver: (event) => { + const targetItemKey = getTargetItemKey( + event.active.id as string, + event.over?.id as string, + ); + + setTargetItemIndex( + targetItemKey && targetItemKey !== event.active.id + ? getItemIndex(targetItemKey) + : -1, + ); }, - onDragEnd: ({active, over}: DragEndEvent) => { + onDragEnd: (event) => { document.body.style.setProperty('cursor', 'default'); - if (!over) { + if (!event.over) { resetState(); return; } - const draggedItemKey = active.id as string; - const targetItemKey = getTargetItemKey(draggedItemKey, over.id as string); + const draggedItemKey = event.active.id as string; + const targetItemKey = getTargetItemKey(draggedItemKey, event.over.id as string); if (targetItemKey === draggedItemKey) { resetState(); @@ -98,12 +118,10 @@ export const useSortableList = ({ nestingEnabled, }); } else { - const targetItemIndex = targetItemKey ? items.indexOf(targetItemKey) : -1; - onDragEnd?.({ draggedItemKey, baseItemKey: targetItemKey, - baseNextItemKey: items[targetItemIndex + 1], + baseNextItemKey: items[targetItemKey ? getItemIndex(targetItemKey) + 1 : 0], nestingEnabled, nextChild: isNextChildMode, pullFromParent: isParentMode, @@ -117,6 +135,7 @@ export const useSortableList = ({ return { activeItemKey, + activeItemIndex: getItemIndex(activeItemKey), targetItemIndex, isParentMode, isChildMode, diff --git a/src/hooks/useTable.ts b/src/hooks/useTable.ts new file mode 100644 index 0000000..dd2004a --- /dev/null +++ b/src/hooks/useTable.ts @@ -0,0 +1,45 @@ +import type {TableOptions} from '@tanstack/react-table'; +import { + getCoreRowModel, + getExpandedRowModel, + getGroupedRowModel, + getSortedRowModel, + useReactTable, +} from '@tanstack/react-table'; + +import type {BaseRowProps} from '../components'; + +interface PassedTableOptions extends Omit, 'getCoreRowModel'> { + getCoreRowModel?: TableOptions['getCoreRowModel']; +} + +export interface UseTableOptions extends PassedTableOptions { + checkIsGroupRow?: BaseRowProps['checkIsGroupRow']; +} + +export const useTable = (options: UseTableOptions) => { + const tableOptions: TableOptions = { + ...options, + enableColumnResizing: options.enableColumnResizing ?? false, + enableExpanding: options.enableExpanding ?? false, + enableGrouping: options.enableGrouping ?? false, + enableMultiRowSelection: options.enableMultiRowSelection ?? false, + enableRowSelection: options.enableRowSelection ?? false, + enableSorting: options.enableSorting ?? false, + getCoreRowModel: options.getCoreRowModel ?? getCoreRowModel(), + getExpandedRowModel: options.enableExpanding + ? options.getExpandedRowModel ?? getExpandedRowModel() + : undefined, + getGroupedRowModel: options.enableGrouping + ? options.getGroupedRowModel ?? getGroupedRowModel() + : undefined, + getRowCanExpand: (row) => Boolean(options.checkIsGroupRow?.(row) || row.subRows?.length), + getSortedRowModel: options.enableSorting + ? options.getSortedRowModel ?? getSortedRowModel() + : undefined, + manualGrouping: options.manualGrouping ?? false, + manualSorting: options.manualSorting ?? false, + }; + + return useReactTable(tableOptions); +}; diff --git a/src/hooks/useWindowRowVirtualizer.ts b/src/hooks/useWindowRowVirtualizer.ts new file mode 100644 index 0000000..94e4c57 --- /dev/null +++ b/src/hooks/useWindowRowVirtualizer.ts @@ -0,0 +1,9 @@ +import {useWindowVirtualizer as useTanstackWindowVirtualizer} from '@tanstack/react-virtual'; + +export type UseWindowRowVirtualizerOptions = Parameters< + typeof useTanstackWindowVirtualizer +>[0]; + +export const useWindowRowVirtualizer = (options: UseWindowRowVirtualizerOptions) => { + return useTanstackWindowVirtualizer(options); +}; diff --git a/src/index.ts b/src/index.ts index 48b305a..fc96f21 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,21 +1,18 @@ -export { - ReorderingProvider, - Table, - VirtualizationProvider, - WindowVirtualizationProvider, -} from './components'; +export {ReorderingProvider, Table} from './components'; +export type {ReorderingProviderProps, SortableListDragResult, TableProps} from './components'; -export type { - ReorderingProviderProps, - TableProps, - VirtualizationProviderProps, - WindowVirtualizationProviderProps, -} from './components'; +export {defaultDragHandleColumn, defaultSelectionColumn} from './constants'; + +export {withTableReorder} from './hocs'; +export type {WithTableReorderProps} from './hocs'; -export {withTableReorder, withTableVirtualization, withTableWindowVirtualization} from './hocs'; +export {useDraggableRowDepth, useRowVirtualizer, useTable, useWindowRowVirtualizer} from './hooks'; export type { - WithTableReorderProps, - WithTableVirtualizationProps, - WithTableWindowVirtualizationProps, -} from './hocs'; + UseDraggableRowDepthParams, + UseRowVirtualizerOptions, + UseTableOptions, + UseWindowRowVirtualizerOptions, +} from './hooks'; + +export {getVirtualRowRangeExtractor} from './utils'; diff --git a/src/components/utils/__tests__/toDataAttributes.test.ts b/src/utils/__tests__/toDataAttributes.test.ts similarity index 100% rename from src/components/utils/__tests__/toDataAttributes.test.ts rename to src/utils/__tests__/toDataAttributes.test.ts diff --git a/src/components/utils/cn.ts b/src/utils/cn.ts similarity index 100% rename from src/components/utils/cn.ts rename to src/utils/cn.ts diff --git a/src/utils/getVirtualRowRangeExtractor.ts b/src/utils/getVirtualRowRangeExtractor.ts new file mode 100644 index 0000000..fe69e38 --- /dev/null +++ b/src/utils/getVirtualRowRangeExtractor.ts @@ -0,0 +1,16 @@ +import {defaultRangeExtractor} from '@tanstack/react-virtual'; +import type {Range} from '@tanstack/react-virtual'; + +export const getVirtualRowRangeExtractor = (table?: HTMLTableElement | null) => { + return (range: Range): number[] => { + const draggingRowIndex = Number(table?.getAttribute('data-dragging-row-index') ?? -1); + + const result = defaultRangeExtractor(range); + + if (draggingRowIndex !== -1 && !result.includes(draggingRowIndex)) { + result.push(draggingRowIndex); + } + + return result; + }; +}; diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..b6850d5 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,5 @@ +export * from './cn'; +export * from './getVirtualRowRangeExtractor'; +export * from './renderDefaultGroupHeader'; +export * from './renderDefaultSortIndicator'; +export * from './toDataAttributes'; diff --git a/src/utils/renderDefaultGroupHeader.tsx b/src/utils/renderDefaultGroupHeader.tsx new file mode 100644 index 0000000..8b1c946 --- /dev/null +++ b/src/utils/renderDefaultGroupHeader.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +import {ArrowToggle} from '@gravity-ui/uikit'; +import type {Row} from '@tanstack/react-table'; + +import {b} from '../components/Table/Table.classname'; + +export const renderDefaultGroupHeader = ( + row: Row, + getGroupTitle?: (row: Row) => React.ReactNode, +) => ( +
+

+ +

+
+); diff --git a/src/utils/renderDefaultSortIndicator.tsx b/src/utils/renderDefaultSortIndicator.tsx new file mode 100644 index 0000000..2633e85 --- /dev/null +++ b/src/utils/renderDefaultSortIndicator.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import type {Header} from '@tanstack/react-table'; + +import {SortIndicator} from '../components'; +import {b} from '../components/Table/Table.classname'; + +export const renderDefaultSortIndicator = ( + header: Header, + className?: string, +) => ( + +); diff --git a/src/components/utils/toDataAttributes.ts b/src/utils/toDataAttributes.ts similarity index 83% rename from src/components/utils/toDataAttributes.ts rename to src/utils/toDataAttributes.ts index 34a4c25..5561401 100644 --- a/src/components/utils/toDataAttributes.ts +++ b/src/utils/toDataAttributes.ts @@ -1,12 +1,12 @@ -function toKebabCase(s: string) { +const toKebabCase = (s: string) => { return s.replace(/(^|[a-z])([A-Z])/g, (_, ...matches) => matches[0] ? `${matches[0]}-${matches[1].toLowerCase()}` : matches[1].toLowerCase(), ); -} +}; -export function toDataAttributes( +export const toDataAttributes = ( dataset?: Record, -): Record | undefined { +): Record | undefined => { if (!dataset) { return undefined; } @@ -24,4 +24,4 @@ export function toDataAttributes( } return htmlAttrs; -} +};