From 33db774a4d25b5901dfa0136d8d56d09d5089a9a Mon Sep 17 00:00:00 2001 From: Dmitry Artemov Date: Tue, 10 Sep 2024 13:03:08 +0200 Subject: [PATCH] feat(BaseTable): additional properties for classNames and attributes customization (#36) --- src/components/BaseCell/BaseCell.tsx | 15 +- .../BaseDragHandle/BaseDragHandle.tsx | 11 +- .../BaseDraggableRow/BaseDraggableRow.tsx | 17 +- .../BaseFooterCell/BaseFooterCell.tsx | 18 +- .../BaseFooterRow/BaseFooterRow.tsx | 46 +++-- .../BaseGroupHeader/BaseGroupHeader.tsx | 4 +- .../BaseHeaderCell/BaseHeaderCell.tsx | 33 +-- .../BaseHeaderRow/BaseHeaderRow.tsx | 52 ++--- src/components/BaseRow/BaseRow.tsx | 58 +++--- src/components/BaseTable/BaseTable.tsx | 193 +++++++++--------- .../ReorderingProvider/ReorderingProvider.tsx | 4 +- src/components/SortableList/SortableList.tsx | 27 +-- src/components/SortableList/types.ts | 2 +- .../SortableListContext.tsx | 5 +- src/components/Table/Table.tsx | 18 +- src/components/TableContext/TableContext.ts | 13 -- src/components/TableContext/index.ts | 1 - .../TableContextProvider.tsx | 25 --- src/components/TableContextProvider/index.ts | 1 - .../cells/DraggableTreeNameCell.tsx | 11 +- src/components/__stories__/constants/tree.tsx | 4 +- src/components/index.ts | 2 - src/constants/defaultDragHandleColumn.tsx | 4 +- src/hocs/withTableReorder.tsx | 7 +- src/hooks/useDraggableRowDepth.tsx | 18 +- src/hooks/useDraggableRowStyle.ts | 8 +- src/hooks/useSortableList.ts | 10 +- src/index.ts | 11 +- src/utils/getAriaMultiselectable.ts | 4 +- src/utils/index.ts | 3 + src/utils/shouldRenderFooterCell.ts | 5 + src/utils/shouldRenderFooterRow.ts | 5 + src/utils/shouldRenderHeaderCell.ts | 18 ++ 33 files changed, 331 insertions(+), 322 deletions(-) delete mode 100644 src/components/TableContext/TableContext.ts delete mode 100644 src/components/TableContext/index.ts delete mode 100644 src/components/TableContextProvider/TableContextProvider.tsx delete mode 100644 src/components/TableContextProvider/index.ts create mode 100644 src/utils/shouldRenderFooterCell.ts create mode 100644 src/utils/shouldRenderFooterRow.ts create mode 100644 src/utils/shouldRenderHeaderCell.ts diff --git a/src/components/BaseCell/BaseCell.tsx b/src/components/BaseCell/BaseCell.tsx index 35cdd28..79387f7 100644 --- a/src/components/BaseCell/BaseCell.tsx +++ b/src/components/BaseCell/BaseCell.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import type {Cell as CellType} from '@tanstack/react-table'; +import type {Cell} from '@tanstack/react-table'; import {flexRender} from '@tanstack/react-table'; import {getCellClassModes, getCellStyles} from '../../utils'; @@ -8,11 +8,11 @@ import {b} from '../BaseTable/BaseTable.classname'; export interface BaseCellProps extends Omit, 'className'> { - cell?: CellType; - className?: string | ((cell?: CellType) => string); + cell?: Cell; + className?: string | ((cell?: Cell) => string); attributes?: | React.TdHTMLAttributes - | ((cell?: CellType) => React.TdHTMLAttributes); + | ((cell?: Cell) => React.TdHTMLAttributes); } export const BaseCell = ({ @@ -23,18 +23,15 @@ export const BaseCell = ({ attributes: attributesProp, ...restProps }: BaseCellProps) => { - const className = React.useMemo(() => { - return typeof classNameProp === 'function' ? classNameProp(cell) : classNameProp; - }, [cell, classNameProp]); - const attributes = typeof attributesProp === 'function' ? attributesProp(cell) : attributesProp; + const className = typeof classNameProp === 'function' ? classNameProp(cell) : classNameProp; return ( {cell ? flexRender(cell.column.columnDef.cell, cell.getContext()) : children} diff --git a/src/components/BaseDragHandle/BaseDragHandle.tsx b/src/components/BaseDragHandle/BaseDragHandle.tsx index 90e39e3..6f0194e 100644 --- a/src/components/BaseDragHandle/BaseDragHandle.tsx +++ b/src/components/BaseDragHandle/BaseDragHandle.tsx @@ -1,11 +1,11 @@ import React from 'react'; import {useSortable} from '@dnd-kit/sortable'; -import type {Row} from '@tanstack/react-table'; +import type {Row, Table} from '@tanstack/react-table'; import {useDraggableRowDepth} from '../../hooks'; import {BaseDraggableRowMarker} from '../BaseDraggableRowMarker'; -import {TableContext} from '../TableContext'; +import {SortableListContext} from '../SortableListContext'; import {b} from './BaseDragHandle.classname'; @@ -13,16 +13,17 @@ import './BaseDragHandle.scss'; export interface BaseDragHandleProps { row: Row; + table: Table; } -export const BaseDragHandle = ({row}: BaseDragHandleProps) => { +export const BaseDragHandle = ({row, table}: BaseDragHandleProps) => { const {attributes, listeners, isDragging} = useSortable({ id: row.id, }); - const {enableNesting} = React.useContext(TableContext); + const {enableNesting} = React.useContext(SortableListContext) || {}; - const {depth} = useDraggableRowDepth({row, isDragging}); + const {depth} = useDraggableRowDepth({row, table, isDragging}); return ( diff --git a/src/components/BaseDraggableRow/BaseDraggableRow.tsx b/src/components/BaseDraggableRow/BaseDraggableRow.tsx index 9590596..0e87f4c 100644 --- a/src/components/BaseDraggableRow/BaseDraggableRow.tsx +++ b/src/components/BaseDraggableRow/BaseDraggableRow.tsx @@ -1,13 +1,12 @@ import React from 'react'; import {useForkRef} from '@gravity-ui/uikit'; -import type {Row as RowType} from '@tanstack/react-table'; +import type {Row} from '@tanstack/react-table'; import {useDraggableRowDepth, useDraggableRowStyle} from '../../hooks'; import type {BaseRowProps} from '../BaseRow'; import {BaseRow} from '../BaseRow'; import {SortableListContext} from '../SortableListContext'; -import {TableContext} from '../TableContext'; export interface BaseDraggableRowProps< TData, @@ -20,6 +19,7 @@ export const BaseDraggableRow = React.forwardRef( attributes: attributesProp, row, style, + table, ...restProps }: BaseDraggableRowProps, ref: React.Ref, @@ -28,6 +28,7 @@ export const BaseDraggableRow = React.forwardRef( isChildMode, activeItemKey, targetItemIndex = -1, + enableNesting, // The `useSortable` hook is provided by `@dnd-kit/sortable` library and imported via `SortableListContext`. // This is a temporary solution to prevent importing the entire `@dnd-kit` library // when the user doesn't use the reordering feature. @@ -43,8 +44,6 @@ export const BaseDraggableRow = React.forwardRef( id: row.id, }) || {}; - const {enableNesting} = React.useContext(TableContext); - const isDragActive = Boolean(activeItemKey); const isParent = isChildMode && targetItemIndex === row.index; @@ -52,6 +51,7 @@ export const BaseDraggableRow = React.forwardRef( const {isFirstChild, depth} = useDraggableRowDepth({ row, + table, isDragging, }); @@ -62,11 +62,11 @@ export const BaseDraggableRow = React.forwardRef( isDragging, isDragActive, isFirstChild, - nestingEnabled: enableNesting, + enableNesting, }); - const getDraggableRowDataAttributes = React.useCallback( - (draggableRow: RowType) => { + const getDraggableRowAttributes = React.useCallback( + (draggableRow: Row) => { const attributes = typeof attributesProp === 'function' ? attributesProp(draggableRow) @@ -88,9 +88,10 @@ export const BaseDraggableRow = React.forwardRef( return ( ); diff --git a/src/components/BaseFooterCell/BaseFooterCell.tsx b/src/components/BaseFooterCell/BaseFooterCell.tsx index 3aa3e2c..4f10d6c 100644 --- a/src/components/BaseFooterCell/BaseFooterCell.tsx +++ b/src/components/BaseFooterCell/BaseFooterCell.tsx @@ -7,17 +7,22 @@ import {getCellStyles, getHeaderCellClassModes} from '../../utils'; import {b} from '../BaseTable/BaseTable.classname'; export interface BaseFooterCellProps { - className?: string; header: Header; + attributes?: + | React.ThHTMLAttributes + | ((header: Header) => React.ThHTMLAttributes); + className?: string | ((header: Header) => string); } export const BaseFooterCell = ({ - className, header, + attributes: attributesProp, + className: classNameProp, }: BaseFooterCellProps) => { - if (header.isPlaceholder) { - return null; - } + const attributes = + typeof attributesProp === 'function' ? attributesProp(header) : attributesProp; + + const className = typeof classNameProp === 'function' ? classNameProp(header) : classNameProp; const rowSpan = header.depth - header.column.depth; @@ -26,7 +31,8 @@ export const BaseFooterCell = ({ className={b('footer-cell', getHeaderCellClassModes(header), className)} colSpan={header.colSpan > 1 ? header.colSpan : undefined} rowSpan={rowSpan > 1 ? rowSpan : undefined} - style={getCellStyles(header)} + {...attributes} + style={getCellStyles(header, attributes?.style)} > {flexRender(header.column.columnDef.footer, header.getContext())} diff --git a/src/components/BaseFooterRow/BaseFooterRow.tsx b/src/components/BaseFooterRow/BaseFooterRow.tsx index 89160c1..133e3fc 100644 --- a/src/components/BaseFooterRow/BaseFooterRow.tsx +++ b/src/components/BaseFooterRow/BaseFooterRow.tsx @@ -1,35 +1,49 @@ import React from 'react'; -import type {HeaderGroup} from '@tanstack/react-table'; +import type {Header, HeaderGroup} from '@tanstack/react-table'; +import {shouldRenderFooterCell} from '../../utils'; import type {BaseFooterCellProps} from '../BaseFooterCell'; import {BaseFooterCell} from '../BaseFooterCell'; import {b} from '../BaseTable/BaseTable.classname'; -export interface BaseFooterRowProps - extends React.TdHTMLAttributes { - cellClassName: BaseFooterCellProps['className']; - className?: string; +export interface BaseFooterRowProps + extends Omit, 'className'> { footerGroup: HeaderGroup; + attributes?: + | React.HTMLAttributes + | ((footerGroup: HeaderGroup) => React.HTMLAttributes); + cellAttributes?: BaseFooterCellProps['attributes']; + cellClassName?: BaseFooterCellProps['className']; + className?: string | ((footerGroup: HeaderGroup) => string); } -export const BaseFooterRow = ({ - cellClassName, - className, +export const BaseFooterRow = ({ footerGroup, + attributes: attributesProp, + cellAttributes, + cellClassName, + className: classNameProp, ...restProps }: BaseFooterRowProps) => { - const isEmptyRow = footerGroup.headers.every((header) => !header.column.columnDef.footer); + const attributes = + typeof attributesProp === 'function' ? attributesProp(footerGroup) : attributesProp; - if (isEmptyRow) { - return null; - } + const className = + typeof classNameProp === 'function' ? classNameProp(footerGroup) : classNameProp; return ( - - {footerGroup.headers.map((header) => ( - - ))} + + {footerGroup.headers.map((header) => + shouldRenderFooterCell(header) ? ( + } + attributes={cellAttributes} + className={cellClassName} + /> + ) : null, + )} ); }; diff --git a/src/components/BaseGroupHeader/BaseGroupHeader.tsx b/src/components/BaseGroupHeader/BaseGroupHeader.tsx index d590c34..ae30407 100644 --- a/src/components/BaseGroupHeader/BaseGroupHeader.tsx +++ b/src/components/BaseGroupHeader/BaseGroupHeader.tsx @@ -7,15 +7,15 @@ import {b} from './BaseGroupHeader.classname'; import './BaseGroupHeader.scss'; export interface BaseGroupHeaderProps { + row: Row; className?: string; getGroupTitle?: (row: Row) => React.ReactNode; - row: Row; } export const BaseGroupHeader = ({ + row, className, getGroupTitle, - row, }: BaseGroupHeaderProps) => { return (

diff --git a/src/components/BaseHeaderCell/BaseHeaderCell.tsx b/src/components/BaseHeaderCell/BaseHeaderCell.tsx index af4fd23..14c3259 100644 --- a/src/components/BaseHeaderCell/BaseHeaderCell.tsx +++ b/src/components/BaseHeaderCell/BaseHeaderCell.tsx @@ -26,11 +26,11 @@ export interface BaseHeaderCellProps { resizeHandleClassName?: string; sortIndicatorClassName?: string; attributes?: - | React.TdHTMLAttributes + | React.ThHTMLAttributes | (( header: Header, parentHeader?: Header, - ) => React.TdHTMLAttributes); + ) => React.ThHTMLAttributes); } export const BaseHeaderCell = ({ @@ -43,43 +43,26 @@ export const BaseHeaderCell = ({ sortIndicatorClassName, attributes: attributesProp, }: BaseHeaderCellProps) => { - const className = React.useMemo(() => { - return typeof classNameProp === 'function' - ? classNameProp(header, parentHeader) - : classNameProp; - }, [classNameProp, header, parentHeader]); - - const isPlaceholderRowSpannedCell = - header.isPlaceholder && - parentHeader?.isPlaceholder && - parentHeader.placeholderId === header.placeholderId; - - const isLeafRowSpannedCell = - !header.isPlaceholder && - header.id === header.column.id && - header.depth - header.column.depth > 1; - - if (isPlaceholderRowSpannedCell || isLeafRowSpannedCell) { - return null; - } - - const rowSpan = header.isPlaceholder ? header.getLeafHeaders().length : 1; - const attributes = typeof attributesProp === 'function' ? attributesProp(header, parentHeader) : attributesProp; + const className = + typeof classNameProp === 'function' ? classNameProp(header, parentHeader) : classNameProp; + + const rowSpan = header.isPlaceholder ? header.getLeafHeaders().length : 1; + return ( 1 ? header.colSpan : undefined} rowSpan={rowSpan > 1 ? rowSpan : undefined} onClick={header.column.getToggleSortingHandler()} - style={getCellStyles(header)} aria-sort={getAriaSort(header.column.getIsSorted())} aria-colindex={getHeaderCellAriaColIndex(header)} {...attributes} + style={getCellStyles(header, attributes?.style)} > {flexRender(header.column.columnDef.header, header.getContext())}{' '} {header.column.getCanSort() && diff --git a/src/components/BaseHeaderRow/BaseHeaderRow.tsx b/src/components/BaseHeaderRow/BaseHeaderRow.tsx index 20031e4..7cb5c7b 100644 --- a/src/components/BaseHeaderRow/BaseHeaderRow.tsx +++ b/src/components/BaseHeaderRow/BaseHeaderRow.tsx @@ -2,13 +2,14 @@ import React from 'react'; import type {Header, HeaderGroup} from '@tanstack/react-table'; +import {shouldRenderHeaderCell} from '../../utils'; import type {BaseHeaderCellProps} from '../BaseHeaderCell'; import {BaseHeaderCell} from '../BaseHeaderCell'; import type {BaseResizeHandleProps} from '../BaseResizeHandle'; import {b} from '../BaseTable/BaseTable.classname'; -export interface BaseHeaderRowProps - extends Omit, 'className'> { +export interface BaseHeaderRowProps + extends Omit, 'className'> { cellClassName?: BaseHeaderCellProps['className']; className?: | string @@ -28,7 +29,7 @@ export interface BaseHeaderRowProps cellAttributes?: BaseHeaderCellProps['attributes']; } -export const BaseHeaderRow = ({ +export const BaseHeaderRow = ({ cellClassName, className: classNameProp, headerGroup, @@ -41,34 +42,37 @@ export const BaseHeaderRow = ({ cellAttributes, ...restProps }: BaseHeaderRowProps) => { - const className = React.useMemo(() => { - return typeof classNameProp === 'function' - ? classNameProp(headerGroup, parentHeaderGroup) - : classNameProp; - }, [classNameProp, headerGroup, parentHeaderGroup]); - const attributes = typeof attributesProp === 'function' ? attributesProp(headerGroup, parentHeaderGroup) : attributesProp; + const className = + typeof classNameProp === 'function' + ? classNameProp(headerGroup, parentHeaderGroup) + : classNameProp; + return ( - {headerGroup.headers.map((header) => ( - } - parentHeader={parentHeaderGroup?.headers.find( - (item) => header.column.id === item.column.id, - )} - renderResizeHandle={renderResizeHandle} - renderSortIndicator={renderSortIndicator} - resizeHandleClassName={resizeHandleClassName} - sortIndicatorClassName={sortIndicatorClassName} - attributes={cellAttributes} - /> - ))} + {headerGroup.headers.map((header) => { + const parentHeader = parentHeaderGroup?.headers.find( + (item) => header.column.id === item.column.id, + ); + + return shouldRenderHeaderCell(header, parentHeader) ? ( + } + parentHeader={parentHeader} + renderResizeHandle={renderResizeHandle} + renderSortIndicator={renderSortIndicator} + resizeHandleClassName={resizeHandleClassName} + sortIndicatorClassName={sortIndicatorClassName} + attributes={cellAttributes} + /> + ) : null; + })} ); }; diff --git a/src/components/BaseRow/BaseRow.tsx b/src/components/BaseRow/BaseRow.tsx index 41a2190..1b44b05 100644 --- a/src/components/BaseRow/BaseRow.tsx +++ b/src/components/BaseRow/BaseRow.tsx @@ -1,43 +1,44 @@ import React from 'react'; import {useForkRef} from '@gravity-ui/uikit'; -import type {Row as RowType} from '@tanstack/react-table'; +import type {Row, Table} from '@tanstack/react-table'; import type {VirtualItem, Virtualizer} from '@tanstack/react-virtual'; -import {BaseCell} from '../BaseCell'; import type {BaseCellProps} from '../BaseCell'; +import {BaseCell} from '../BaseCell'; import type {BaseGroupHeaderProps} from '../BaseGroupHeader'; import {BaseGroupHeader} from '../BaseGroupHeader'; import {b} from '../BaseTable/BaseTable.classname'; export interface BaseRowProps - extends Omit, 'className' | 'onClick'> { + extends Omit, 'className' | 'onClick'> { cellClassName?: BaseCellProps['className']; - className?: string | ((row?: RowType) => string); - getGroupTitle?: (row: RowType) => React.ReactNode; - getIsCustomRow?: (row: RowType) => boolean; - getIsGroupHeaderRow?: (row: RowType) => boolean; + className?: string | ((row?: Row) => string); + getGroupTitle?: (row: Row) => React.ReactNode; + getIsCustomRow?: (row: Row) => boolean; + getIsGroupHeaderRow?: (row: Row) => boolean; groupHeaderClassName?: string; - onClick?: (row: RowType, event: React.MouseEvent) => void; + onClick?: (row: Row, event: React.MouseEvent) => void; renderCustomRowContent?: (props: { - Cell: typeof BaseCell; + row: Row; + Cell: React.FunctionComponent>; cellClassName?: BaseCellProps['className']; - row: RowType; }) => React.ReactNode; renderGroupHeader?: (props: BaseGroupHeaderProps) => React.ReactNode; renderGroupHeaderRowContent?: (props: { - Cell: typeof BaseCell; + row: Row; + Cell: React.FunctionComponent>; cellClassName?: BaseCellProps['className']; - getGroupTitle?: (row: RowType) => React.ReactNode; - row: RowType; + getGroupTitle?: (row: Row) => React.ReactNode; }) => React.ReactNode; - row: RowType; + row: Row; rowVirtualizer?: Virtualizer; style?: React.CSSProperties; + table: Table; virtualItem?: VirtualItem; attributes?: | React.HTMLAttributes - | ((row: RowType) => React.HTMLAttributes); + | ((row: Row) => React.HTMLAttributes); cellAttributes?: BaseCellProps['attributes']; } @@ -66,9 +67,10 @@ export const BaseRow = React.forwardRef( ) => { const rowRef = useForkRef(rowVirtualizer?.measureElement, ref); - const className = React.useMemo(() => { - return typeof classNameProp === 'function' ? classNameProp(row) : classNameProp; - }, [classNameProp, row]); + const attributes = + typeof attributesProp === 'function' ? attributesProp(row) : attributesProp; + + const className = typeof classNameProp === 'function' ? classNameProp(row) : classNameProp; const handleClick = React.useCallback( (event: React.MouseEvent) => { @@ -81,10 +83,10 @@ export const BaseRow = React.forwardRef( if (getIsGroupHeaderRow?.(row)) { return renderGroupHeaderRowContent ? ( renderGroupHeaderRowContent({ + row, Cell: BaseCell, cellClassName, getGroupTitle, - row, }) ) : ( {renderGroupHeader ? ( renderGroupHeader({ + row, className: b('group-header', groupHeaderClassName), getGroupTitle, - row, }) ) : ( )} @@ -111,7 +113,7 @@ export const BaseRow = React.forwardRef( } if (getIsCustomRow?.(row) && renderCustomRowContent) { - return renderCustomRowContent({Cell: BaseCell, cellClassName, row}); + return renderCustomRowContent({row, Cell: BaseCell, cellClassName}); } return row @@ -127,16 +129,9 @@ export const BaseRow = React.forwardRef( )); }; - const attributes = - typeof attributesProp === 'function' ? attributesProp(row) : attributesProp; - return ( {renderRowContent()} diff --git a/src/components/BaseTable/BaseTable.tsx b/src/components/BaseTable/BaseTable.tsx index 85f319e..a3169eb 100644 --- a/src/components/BaseTable/BaseTable.tsx +++ b/src/components/BaseTable/BaseTable.tsx @@ -1,78 +1,88 @@ import React from 'react'; -import type {Row as RowType, Table as TableType} from '@tanstack/react-table'; +import type {Row, Table} from '@tanstack/react-table'; import type {VirtualItem, Virtualizer} from '@tanstack/react-virtual'; -import {getAriaMultiselectable, getCellClassModes} from '../../utils'; +import {getAriaMultiselectable, getCellClassModes, shouldRenderFooterRow} from '../../utils'; import {BaseDraggableRow} from '../BaseDraggableRow'; +import type {BaseFooterRowProps} from '../BaseFooterRow'; import {BaseFooterRow} from '../BaseFooterRow'; import type {BaseHeaderRowProps} from '../BaseHeaderRow'; import {BaseHeaderRow} from '../BaseHeaderRow'; -import {BaseRow} from '../BaseRow'; import type {BaseRowProps} from '../BaseRow'; +import {BaseRow} from '../BaseRow'; import {SortableListContext} from '../SortableListContext'; -import {TableContextProvider} from '../TableContextProvider'; -import type {TableContextProviderProps} from '../TableContextProvider'; import {b} from './BaseTable.classname'; import './BaseTable.scss'; export interface BaseTableProps { + table: Table; + attributes?: React.TableHTMLAttributes; + bodyAttributes?: React.HTMLAttributes; bodyClassName?: string; + cellAttributes?: BaseRowProps['cellAttributes']; cellClassName?: BaseRowProps['cellClassName']; className?: string; emptyContent?: React.ReactNode | (() => React.ReactNode); - enableNesting?: boolean; - footerCellClassName?: string; + footerAttributes?: React.HTMLAttributes; + footerCellAttributes?: BaseFooterRowProps['cellAttributes']; + footerCellClassName?: BaseFooterRowProps['cellClassName']; footerClassName?: string; - footerRowClassName?: string; + footerRowAttributes?: BaseFooterRowProps['attributes']; + footerRowClassName?: BaseFooterRowProps['className']; getGroupTitle?: BaseRowProps['getGroupTitle']; getIsCustomRow?: BaseRowProps['getIsCustomRow']; getIsGroupHeaderRow?: BaseRowProps['getIsGroupHeaderRow']; groupHeaderClassName?: BaseRowProps['groupHeaderClassName']; - headerCellClassName?: BaseHeaderRowProps['cellClassName']; + headerAttributes?: React.HTMLAttributes; + headerCellAttributes?: BaseHeaderRowProps['cellAttributes']; + headerCellClassName?: BaseHeaderRowProps['cellClassName']; headerClassName?: string; - headerRowClassName?: BaseHeaderRowProps['className']; + headerRowAttributes?: BaseHeaderRowProps['attributes']; + headerRowClassName?: BaseHeaderRowProps['className']; onRowClick?: BaseRowProps['onClick']; renderCustomRowContent?: BaseRowProps['renderCustomRowContent']; renderGroupHeader?: BaseRowProps['renderGroupHeader']; renderGroupHeaderRowContent?: BaseRowProps['renderGroupHeaderRowContent']; - renderResizeHandle?: BaseHeaderRowProps['renderResizeHandle']; - renderSortIndicator?: BaseHeaderRowProps['renderSortIndicator']; - resizeHandleClassName?: BaseHeaderRowProps['resizeHandleClassName']; + renderResizeHandle?: BaseHeaderRowProps['renderResizeHandle']; + renderSortIndicator?: BaseHeaderRowProps['renderSortIndicator']; + resizeHandleClassName?: BaseHeaderRowProps['resizeHandleClassName']; + rowAttributes?: BaseRowProps['attributes']; rowClassName?: BaseRowProps['className']; rowVirtualizer?: Virtualizer; - sortIndicatorClassName?: BaseHeaderRowProps['sortIndicatorClassName']; + sortIndicatorClassName?: BaseHeaderRowProps['sortIndicatorClassName']; stickyFooter?: boolean; stickyHeader?: boolean; - table: TableType; withFooter?: boolean; withHeader?: boolean; - attributes?: React.TableHTMLAttributes; - headerAttributes?: React.HTMLAttributes; - headerRowAttributes?: BaseHeaderRowProps['attributes']; - headerCellAttributes?: BaseHeaderRowProps['cellAttributes']; - bodyAttributes?: React.HTMLAttributes; - rowAttributes?: BaseRowProps['attributes']; - cellAttributes?: BaseRowProps['cellAttributes']; } export const BaseTable = React.forwardRef( ( { + table, + attributes, + bodyAttributes, bodyClassName, + cellAttributes, cellClassName, className, emptyContent, - enableNesting, + footerAttributes, + footerCellAttributes, footerCellClassName, footerClassName, + footerRowAttributes, footerRowClassName, getGroupTitle, getIsGroupHeaderRow, + headerAttributes, + headerCellAttributes, headerCellClassName, headerClassName, + headerRowAttributes, headerRowClassName, onRowClick, renderGroupHeader, @@ -80,34 +90,20 @@ export const BaseTable = React.forwardRef( renderResizeHandle, renderSortIndicator, resizeHandleClassName, + rowAttributes, rowClassName, rowVirtualizer, sortIndicatorClassName, - stickyFooter, - stickyHeader, - table, - withFooter, + stickyFooter = false, + stickyHeader = false, + withFooter = false, withHeader = true, - attributes, - headerAttributes, - headerRowAttributes, - headerCellAttributes, - bodyAttributes, - rowAttributes, - cellAttributes, }: BaseTableProps, ref: React.Ref, ) => { const draggableContext = React.useContext(SortableListContext); const draggingRowIndex = draggableContext?.activeItemIndex ?? -1; - const getRowByIndex = React.useCallback['getRowByIndex']>( - (rowIndex: number) => { - return table.getRowModel().rows[rowIndex]; - }, - [table], - ); - const {rows} = table.getRowModel(); const headerGroups = withHeader && table.getHeaderGroups(); @@ -123,16 +119,16 @@ export const BaseTable = React.forwardRef( const bodyRows = rowVirtualizer?.getVirtualItems() || rows; if (bodyRows.length === 0) { - const rClassName = + const emptyRowClassName = typeof rowClassName === 'function' ? rowClassName() : rowClassName; - const cClassName = + const emptyCellClassName = typeof cellClassName === 'function' ? cellClassName() : cellClassName; return ( - + {typeof emptyContent === 'function' ? emptyContent() : emptyContent} @@ -144,7 +140,7 @@ export const BaseTable = React.forwardRef( return bodyRows.map((virtualItemOrRow) => { const row = rowVirtualizer ? rows[virtualItemOrRow.index] - : (virtualItemOrRow as RowType); + : (virtualItemOrRow as Row); const rowProps: BaseRowProps = { cellClassName, @@ -158,6 +154,7 @@ export const BaseTable = React.forwardRef( renderGroupHeaderRowContent, row, rowVirtualizer, + table, virtualItem: rowVirtualizer ? (virtualItemOrRow as VirtualItem) : undefined, @@ -176,63 +173,69 @@ export const BaseTable = React.forwardRef( }; return ( - - -1 ? draggingRowIndex : undefined} - aria-colcount={colCount > 0 ? colCount : undefined} - aria-rowcount={rowCount > 0 ? rowCount : undefined} - aria-multiselectable={getAriaMultiselectable(table)} - {...attributes} +
-1 ? draggingRowIndex : undefined} + aria-colcount={colCount > 0 ? colCount : undefined} + aria-rowcount={rowCount > 0 ? rowCount : undefined} + aria-multiselectable={getAriaMultiselectable(table)} + {...attributes} + > + {headerGroups && ( + + {headerGroups.map((headerGroup, index) => ( + + ))} + + )} + - {headerGroups && ( - - {headerGroups.map((headerGroup, index) => ( - - ))} - - )} - + {footerGroups && ( + - {renderBodyRows()} - - {footerGroups && ( - - {footerGroups.map((footerGroup, index) => ( + {footerGroups.map((footerGroup, index) => + shouldRenderFooterRow(footerGroup) ? ( - ))} - - )} -
-
+ ) : null, + )} + + )} + ); }, ) as (( diff --git a/src/components/ReorderingProvider/ReorderingProvider.tsx b/src/components/ReorderingProvider/ReorderingProvider.tsx index c21e3d8..a6889cd 100644 --- a/src/components/ReorderingProvider/ReorderingProvider.tsx +++ b/src/components/ReorderingProvider/ReorderingProvider.tsx @@ -13,7 +13,7 @@ export interface ReorderingProviderProps { table: Table; children?: React.ReactNode; dndModifiers?: SortableListDndContextProps['modifiers']; - enableNesting?: SortableListProps['nestingEnabled']; + enableNesting?: SortableListProps['enableNesting']; onReorder?: SortableListProps['onDragEnd']; } @@ -29,7 +29,7 @@ export const ReorderingProvider = ({ return ( - + {children} diff --git a/src/components/SortableList/SortableList.tsx b/src/components/SortableList/SortableList.tsx index c3951eb..69f26cb 100644 --- a/src/components/SortableList/SortableList.tsx +++ b/src/components/SortableList/SortableList.tsx @@ -4,6 +4,7 @@ import {SortableContext, useSortable, verticalListSortingStrategy} from '@dnd-ki import type {UseSortableListParams} from '../../hooks'; import {useSortableList} from '../../hooks'; +import type {SortableListContextValue} from '../SortableListContext'; import {SortableListContext} from '../SortableListContext'; export interface SortableListProps extends UseSortableListParams { @@ -15,7 +16,7 @@ export const SortableList = ({ items, onDragStart, onDragEnd, - nestingEnabled, + enableNesting, }: SortableListProps) => { const { activeItemKey, @@ -28,21 +29,23 @@ export const SortableList = ({ items, onDragStart, onDragEnd, - nestingEnabled, + enableNesting, }); const contextValue = React.useMemo( - () => ({ - activeItemKey, - activeItemIndex, - isChildMode, - isNextChildMode, - isParentMode, - targetItemIndex, - useSortable, - }), + () => + ({ + activeItemKey, + activeItemIndex, + isChildMode, + isNextChildMode, + isParentMode, + targetItemIndex, + enableNesting, + useSortable, + }) satisfies SortableListContextValue, // eslint-disable-next-line react-hooks/exhaustive-deps - [activeItemKey, isChildMode, isNextChildMode, isParentMode, targetItemIndex], + [activeItemKey, isChildMode, isNextChildMode, isParentMode, targetItemIndex, enableNesting], ); return ( diff --git a/src/components/SortableList/types.ts b/src/components/SortableList/types.ts index 41233c1..a96bcb5 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; + enableNesting?: boolean; nextChild?: boolean; pullFromParent?: boolean; } diff --git a/src/components/SortableListContext/SortableListContext.tsx b/src/components/SortableListContext/SortableListContext.tsx index 19af919..10e866c 100644 --- a/src/components/SortableListContext/SortableListContext.tsx +++ b/src/components/SortableListContext/SortableListContext.tsx @@ -4,9 +4,10 @@ import type {useSortable} from '@dnd-kit/sortable'; import type {useSortableList} from '../../hooks'; -type SortableListContextValue = ReturnType & { +export interface SortableListContextValue extends ReturnType { + enableNesting?: boolean; useSortable?: typeof useSortable; -}; +} export const SortableListContext = React.createContext( undefined, diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index d28c08c..f16092d 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -15,7 +15,7 @@ export const Table = React.forwardRef( { className, cellClassName: cellClassNameProp, - footerCellClassName, + footerCellClassName: footerCellClassNameProp, headerCellClassName: headerCellClassNameProp, headerClassName, ...props @@ -24,24 +24,34 @@ export const Table = React.forwardRef( ) => { const cellClassName: TableProps['cellClassName'] = React.useMemo(() => { if (typeof cellClassNameProp === 'function') { - return (...params) => b('cell', cellClassNameProp(...params)); + return (...args) => b('cell', cellClassNameProp(...args)); } + return b('cell', cellClassNameProp); }, [cellClassNameProp]); const headerCellClassName: TableProps['headerCellClassName'] = React.useMemo(() => { if (typeof headerCellClassNameProp === 'function') { - return (...params) => b('header-cell', headerCellClassNameProp(...params)); + return (...args) => b('header-cell', headerCellClassNameProp(...args)); } + return b('header-cell', headerCellClassNameProp); }, [headerCellClassNameProp]); + const footerCellClassName: TableProps['footerCellClassName'] = React.useMemo(() => { + if (typeof footerCellClassNameProp === 'function') { + return (...args) => b('footer-cell', footerCellClassNameProp(...args)); + } + + return b('footer-cell', footerCellClassNameProp); + }, [footerCellClassNameProp]); + return ( (index: number) => Row | undefined; - enableNesting?: boolean; -} - -export const TableContext = React.createContext({ - getRowByIndex: () => undefined, - enableNesting: false, -}); diff --git a/src/components/TableContext/index.ts b/src/components/TableContext/index.ts deleted file mode 100644 index 895968f..0000000 --- a/src/components/TableContext/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './TableContext'; diff --git a/src/components/TableContextProvider/TableContextProvider.tsx b/src/components/TableContextProvider/TableContextProvider.tsx deleted file mode 100644 index b8a1c81..0000000 --- a/src/components/TableContextProvider/TableContextProvider.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; - -import type {Row} from '@tanstack/react-table'; - -import type {TableContextProps} from '../TableContext'; -import {TableContext} from '../TableContext'; - -export interface TableContextProviderProps { - children?: React.ReactNode; - enableNesting?: TableContextProps['enableNesting']; - getRowByIndex: (index: number) => Row | undefined; -} - -export const TableContextProvider = ({ - children, - enableNesting, - getRowByIndex, -}: TableContextProviderProps) => { - const contextValue = React.useMemo( - () => ({getRowByIndex, enableNesting}) as TableContextProps, - [getRowByIndex, enableNesting], - ); - - return {children}; -}; diff --git a/src/components/TableContextProvider/index.ts b/src/components/TableContextProvider/index.ts deleted file mode 100644 index 114fbbc..0000000 --- a/src/components/TableContextProvider/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './TableContextProvider'; diff --git a/src/components/__stories__/cells/DraggableTreeNameCell.tsx b/src/components/__stories__/cells/DraggableTreeNameCell.tsx index 278d424..9cf2608 100644 --- a/src/components/__stories__/cells/DraggableTreeNameCell.tsx +++ b/src/components/__stories__/cells/DraggableTreeNameCell.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {useSortable} from '@dnd-kit/sortable'; -import type {Row} from '@tanstack/react-table'; +import type {Row, Table} from '@tanstack/react-table'; import {useDraggableRowDepth} from '../../../hooks'; @@ -9,15 +9,20 @@ import {TreeNameCell} from './TreeNameCell'; export interface DraggableTreeNameCellProps { row: Row; + table: Table; value: string; } -export const DraggableTreeNameCell = ({row, value}: DraggableTreeNameCellProps) => { +export const DraggableTreeNameCell = ({ + row, + table, + value, +}: DraggableTreeNameCellProps) => { const {isDragging} = useSortable({ id: row.id, }); - const {depth} = useDraggableRowDepth({row, isDragging}); + const {depth} = useDraggableRowDepth({row, table, isDragging}); return ; }; diff --git a/src/components/__stories__/constants/tree.tsx b/src/components/__stories__/constants/tree.tsx index ca05225..44301d9 100644 --- a/src/components/__stories__/constants/tree.tsx +++ b/src/components/__stories__/constants/tree.tsx @@ -27,7 +27,9 @@ export const draggableTreeColumns: ColumnDef[] = [ accessorKey: 'name', header: 'Name', size: 200, - cell: (info) => ()} />, + cell: ({getValue, row, table}) => ( + ()} /> + ), }, {accessorKey: 'age', header: 'Age', size: 100}, ]; diff --git a/src/components/index.ts b/src/components/index.ts index f83f083..7d61c68 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -15,5 +15,3 @@ export * from './SortableListContext'; export * from './SortableListDndContext'; export * from './BaseTable'; export * from './Table'; -export * from './TableContext'; -export * from './TableContextProvider'; diff --git a/src/constants/defaultDragHandleColumn.tsx b/src/constants/defaultDragHandleColumn.tsx index 4c4843f..b56dfd5 100644 --- a/src/constants/defaultDragHandleColumn.tsx +++ b/src/constants/defaultDragHandleColumn.tsx @@ -7,9 +7,7 @@ import {BaseDragHandle} from '../components'; export const defaultDragHandleColumn: ColumnDef = { id: '_drag', header: '', - cell: ({row}) => { - return ; - }, + cell: ({row, table}) => , size: 14, minSize: 14, }; diff --git a/src/hocs/withTableReorder.tsx b/src/hocs/withTableReorder.tsx index 50d8bba..6caca52 100644 --- a/src/hocs/withTableReorder.tsx +++ b/src/hocs/withTableReorder.tsx @@ -32,12 +32,7 @@ export const withTableReorder = ( enableNesting={enableNesting} onReorder={onReorder} > - + ); }, diff --git a/src/hooks/useDraggableRowDepth.tsx b/src/hooks/useDraggableRowDepth.tsx index a3051df..7d79f9c 100644 --- a/src/hooks/useDraggableRowDepth.tsx +++ b/src/hooks/useDraggableRowDepth.tsx @@ -1,34 +1,36 @@ import React from 'react'; -import type {Row} from '@tanstack/react-table'; +import type {Row, Table} from '@tanstack/react-table'; -import {SortableListContext, TableContext} from '../components'; +import {SortableListContext} from '../components'; -export interface UseDraggableRowDepthParams { +export interface UseDraggableRowDepthOptions { row: Row; isDragging?: boolean; + table: Table; } export const useDraggableRowDepth = ({ row, + table, isDragging, -}: UseDraggableRowDepthParams) => { +}: UseDraggableRowDepthOptions) => { const { isChildMode, isParentMode, isNextChildMode, targetItemIndex = -1, + enableNesting, } = React.useContext(SortableListContext) ?? {}; - const {getRowByIndex, enableNesting} = React.useContext(TableContext); - let isFirstChild = isDragging && targetItemIndex === -1; let depth = 0; if (enableNesting) { if (isDragging && targetItemIndex !== -1) { - const targetItemDepth = getRowByIndex(targetItemIndex)?.depth ?? 0; - const nextItemDepth = getRowByIndex(targetItemIndex + 1)?.depth ?? 0; + const rows = table.getRowModel().rows; + const targetItemDepth = rows[targetItemIndex]?.depth ?? 0; + const nextItemDepth = rows[targetItemIndex + 1]?.depth ?? 0; isFirstChild = nextItemDepth > targetItemDepth; diff --git a/src/hooks/useDraggableRowStyle.ts b/src/hooks/useDraggableRowStyle.ts index d45d3e7..517ad96 100644 --- a/src/hooks/useDraggableRowStyle.ts +++ b/src/hooks/useDraggableRowStyle.ts @@ -10,7 +10,7 @@ export interface UseDraggableRowStyleParams isFirstChild?: boolean; draggableChildRowOffset?: number; style?: React.CSSProperties; - nestingEnabled?: boolean; + enableNesting?: boolean; } const defaultStyle = {}; @@ -23,7 +23,7 @@ export const useDraggableRowStyle = ({ isDragActive, isFirstChild, draggableChildRowOffset = 32, - nestingEnabled, + enableNesting, }: UseDraggableRowStyleParams) => { const {isChildMode, isParentMode} = React.useContext(SortableListContext) ?? {}; @@ -34,7 +34,7 @@ export const useDraggableRowStyle = ({ let x = 0; - if (nestingEnabled && isDragging) { + if (enableNesting && isDragging) { if (isParentMode) { x = -draggableChildRowOffset; } else if (isChildMode && !isFirstChild) { @@ -57,6 +57,6 @@ export const useDraggableRowStyle = ({ style, transform, transition, - nestingEnabled, + enableNesting, ]); }; diff --git a/src/hooks/useSortableList.ts b/src/hooks/useSortableList.ts index 55fb292..d79c89f 100644 --- a/src/hooks/useSortableList.ts +++ b/src/hooks/useSortableList.ts @@ -8,7 +8,7 @@ export interface UseSortableListParams { items: string[]; onDragStart?: (activeId: string) => void; onDragEnd?: (result: SortableListDragResult) => void; - nestingEnabled?: boolean; + enableNesting?: boolean; childModeOffset?: number; nextChildModeOffset?: number; } @@ -17,7 +17,7 @@ export const useSortableList = ({ items, onDragStart, onDragEnd, - nestingEnabled, + enableNesting, childModeOffset = 20, nextChildModeOffset = 10, }: UseSortableListParams) => { @@ -74,7 +74,7 @@ export const useSortableList = ({ document.body.style.setProperty('cursor', 'grabbing'); }, onDragMove: (event) => { - if (nestingEnabled) { + if (enableNesting) { setIsParentMode(event.delta.x < -childModeOffset); setIsChildMode(event.delta.x > childModeOffset); @@ -115,14 +115,14 @@ export const useSortableList = ({ onDragEnd?.({ draggedItemKey, targetItemKey, - nestingEnabled, + enableNesting, }); } else { onDragEnd?.({ draggedItemKey, baseItemKey: targetItemKey, baseNextItemKey: items[targetItemKey ? getItemIndex(targetItemKey) + 1 : 0], - nestingEnabled, + enableNesting, nextChild: isNextChildMode, pullFromParent: isParentMode, }); diff --git a/src/index.ts b/src/index.ts index d8ccda6..489b718 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,5 @@ -export {ReorderingProvider, BaseTable, Table} from './components'; -export type { - ReorderingProviderProps, - SortableListDragResult, - BaseTableProps, - TableProps, -} from './components'; +export {BaseTable, ReorderingProvider, Table} from './components'; +export type {BaseTableProps, ReorderingProviderProps, TableProps} from './components'; export {defaultDragHandleColumn, defaultSelectionColumn} from './constants'; @@ -14,7 +9,7 @@ export type {WithTableReorderProps} from './hocs'; export {useDraggableRowDepth, useRowVirtualizer, useTable, useWindowRowVirtualizer} from './hooks'; export type { - UseDraggableRowDepthParams, + UseDraggableRowDepthOptions, UseRowVirtualizerOptions, UseTableOptions, UseWindowRowVirtualizerOptions, diff --git a/src/utils/getAriaMultiselectable.ts b/src/utils/getAriaMultiselectable.ts index 2df115b..b0018db 100644 --- a/src/utils/getAriaMultiselectable.ts +++ b/src/utils/getAriaMultiselectable.ts @@ -1,6 +1,6 @@ -import type {Table as TableType} from '@tanstack/table-core/build/lib/types'; +import type {Table} from '@tanstack/table-core/build/lib/types'; -export const getAriaMultiselectable = (table: TableType) => { +export const getAriaMultiselectable = (table: Table) => { if (table.options.enableRowSelection) { return Boolean(table.options.enableMultiRowSelection); } diff --git a/src/utils/index.ts b/src/utils/index.ts index 36530c0..e377fc8 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -7,3 +7,6 @@ export * from './getColumnPinningClassModes'; export * from './getHeaderCellAriaColIndex'; export * from './getHeaderCellClassModes'; export * from './getVirtualRowRangeExtractor'; +export * from './shouldRenderFooterCell'; +export * from './shouldRenderFooterRow'; +export * from './shouldRenderHeaderCell'; diff --git a/src/utils/shouldRenderFooterCell.ts b/src/utils/shouldRenderFooterCell.ts new file mode 100644 index 0000000..9531364 --- /dev/null +++ b/src/utils/shouldRenderFooterCell.ts @@ -0,0 +1,5 @@ +import type {Header} from '@tanstack/react-table'; + +export const shouldRenderFooterCell = (header: Header) => { + return !header.isPlaceholder; +}; diff --git a/src/utils/shouldRenderFooterRow.ts b/src/utils/shouldRenderFooterRow.ts new file mode 100644 index 0000000..069d475 --- /dev/null +++ b/src/utils/shouldRenderFooterRow.ts @@ -0,0 +1,5 @@ +import type {HeaderGroup} from '@tanstack/react-table'; + +export const shouldRenderFooterRow = (footerGroup: HeaderGroup) => { + return footerGroup.headers.every((header) => !header.column.columnDef.footer); +}; diff --git a/src/utils/shouldRenderHeaderCell.ts b/src/utils/shouldRenderHeaderCell.ts new file mode 100644 index 0000000..579cbba --- /dev/null +++ b/src/utils/shouldRenderHeaderCell.ts @@ -0,0 +1,18 @@ +import type {Header} from '@tanstack/react-table'; + +export const shouldRenderHeaderCell = ( + header: Header, + parentHeader?: Header, +) => { + const isPlaceholderRowSpannedCell = + header.isPlaceholder && + parentHeader?.isPlaceholder && + parentHeader.placeholderId === header.placeholderId; + + const isLeafRowSpannedCell = + !header.isPlaceholder && + header.id === header.column.id && + header.depth - header.column.depth > 1; + + return !(isPlaceholderRowSpannedCell || isLeafRowSpannedCell); +};