From 78061574aa5c1be5bc5e7bb7d39935834e0d59da Mon Sep 17 00:00:00 2001 From: Nahrin Date: Fri, 19 Jun 2026 11:29:11 -0400 Subject: [PATCH 1/9] refactor: convert ExpandIcon from class to function component (#431) Convert ExpandIcon from React.PureComponent class to a function component wrapped with React.memo to preserve shallow prop comparison behavior. - Replace static defaultProps with JS default parameters - Replace constructor bind with useCallback hook - Move propTypes to a static property on the function component - No behavioral changes Resolves part of #431 Co-Authored-By: Claude Opus 4.6 --- src/ExpandIcon.tsx | 54 +++++++++++++++++++--------------------------- 1 file changed, 22 insertions(+), 32 deletions(-) diff --git a/src/ExpandIcon.tsx b/src/ExpandIcon.tsx index d2dcb0c4..d8da7b8a 100644 --- a/src/ExpandIcon.tsx +++ b/src/ExpandIcon.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import PropTypes from 'prop-types'; import cn from 'classnames'; @@ -14,28 +14,17 @@ export interface ExpandIconProps { /** * default ExpandIcon for BaseTable */ -class ExpandIcon extends React.PureComponent { - static defaultProps = { - depth: 0, - indentSize: 16, - }; - - static propTypes = { - expandable: PropTypes.bool, - expanded: PropTypes.bool, - indentSize: PropTypes.number, - depth: PropTypes.number, - onExpand: PropTypes.func, - }; - - constructor(props: ExpandIconProps) { - super(props); - - this._handleClick = this._handleClick.bind(this); - } +const ExpandIcon: React.FC = React.memo( + ({ expandable, expanded, indentSize = 16, depth = 0, onExpand, ...rest }) => { + const handleClick = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + onExpand!(!expanded); + }, + [onExpand, expanded], + ); - render() { - const { expandable, expanded, indentSize, depth, onExpand, ...rest } = this.props; if (!expandable && indentSize === 0) return null; const cls = cn('BaseTable__expand-icon', { @@ -45,7 +34,7 @@ class ExpandIcon extends React.PureComponent {
{ textAlign: 'center', transition: 'transform 0.15s ease-out', transform: `rotate(${expandable && expanded ? 90 : 0}deg)`, - marginLeft: (depth || 0) * (indentSize || 16), + marginLeft: depth * indentSize, }} > {expandable && '\u25B8'}
); - } + }, +); - _handleClick(e: React.MouseEvent) { - e.stopPropagation(); - e.preventDefault(); - const { onExpand, expanded } = this.props; - onExpand!(!expanded); - } -} +ExpandIcon.propTypes = { + expandable: PropTypes.bool, + expanded: PropTypes.bool, + indentSize: PropTypes.number, + depth: PropTypes.number, + onExpand: PropTypes.func, +}; export default ExpandIcon; From 2094fae7d0de7aa347b358f46053fe9ad9c6ce08 Mon Sep 17 00:00:00 2001 From: Nahrin Date: Fri, 19 Jun 2026 11:31:22 -0400 Subject: [PATCH 2/9] refactor: convert ColumnResizer from class to function component (#431) Convert ColumnResizer from React.PureComponent class to a function component wrapped with React.memo. - Replace instance variables with useRef for mutable drag state (isDragging, lastX, width, handleRef) - Use refs for props accessed in DOM event listeners (column, onResize, onResizeStop, minWidth) to avoid stale closures - Replace constructor binds with useCallback hooks - Replace componentWillUnmount with useEffect cleanup - Replace static defaultProps with JS default parameters - Move propTypes to a property on the function component - No behavioral changes Resolves part of #431 Co-Authored-By: Claude Opus 4.6 --- src/ColumnResizer.tsx | 335 +++++++++++++++++++++--------------------- 1 file changed, 169 insertions(+), 166 deletions(-) diff --git a/src/ColumnResizer.tsx b/src/ColumnResizer.tsx index 7aac0c60..027d74e3 100644 --- a/src/ColumnResizer.tsx +++ b/src/ColumnResizer.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useRef, useCallback, useEffect } from 'react'; import PropTypes from 'prop-types'; import { noop, addClassName, removeClassName } from './utils'; @@ -68,86 +68,148 @@ export interface ColumnResizerProps { /** * ColumnResizer for BaseTable */ -class ColumnResizer extends React.PureComponent { - isDragging = false; - lastX: number | null = INVALID_VALUE; - width = 0; - handleRef: HTMLDivElement | null = null; - - static defaultProps = { - onResizeStart: noop, - onResize: noop, - onResizeStop: noop, - minWidth: 30, - }; - - static propTypes = { - /** - * Custom style for the drag handler - */ - style: PropTypes.object, - /** - * The column object to be dragged - */ - column: PropTypes.object, - /** - * A callback function when resizing started - * The callback is of the shape of `(column) => *` - */ - onResizeStart: PropTypes.func, - /** - * A callback function when resizing the column - * The callback is of the shape of `(column, width) => *` - */ - onResize: PropTypes.func, - /** - * A callback function when resizing stopped - * The callback is of the shape of `(column) => *` - */ - onResizeStop: PropTypes.func, - /** - * Minimum width of the column could be resized to if the column's `minWidth` is not set - */ - minWidth: PropTypes.number, - }; - - constructor(props: ColumnResizerProps) { - super(props); - - this._setHandleRef = this._setHandleRef.bind(this); - this._handleClick = this._handleClick.bind(this); - this._handleMouseDown = this._handleMouseDown.bind(this); - this._handleMouseUp = this._handleMouseUp.bind(this); - this._handleTouchStart = this._handleTouchStart.bind(this); - this._handleTouchEnd = this._handleTouchEnd.bind(this); - this._handleDragStart = this._handleDragStart.bind(this); - this._handleDragStop = this._handleDragStop.bind(this); - this._handleDrag = this._handleDrag.bind(this); - } +const ColumnResizer: React.FC = React.memo( + ({ style, column, onResizeStart = noop, onResize = noop, onResizeStop = noop, minWidth = 30, ...rest }) => { + const handleRef = useRef(null); + const isDraggingRef = useRef(false); + const lastXRef = useRef(INVALID_VALUE); + const widthRef = useRef(0); + + // Refs to hold latest props for use in DOM event listeners + const columnRef = useRef(column); + columnRef.current = column; + const onResizeRef = useRef(onResize); + onResizeRef.current = onResize; + const onResizeStopRef = useRef(onResizeStop); + onResizeStopRef.current = onResizeStop; + const minWidthRef = useRef(minWidth); + minWidthRef.current = minWidth; + + const handleDrag = useCallback((e: any) => { + let clientX = e.clientX; + if (e.type === eventsFor.touch.move) { + e.preventDefault(); + if (e.targetTouches && e.targetTouches[0]) clientX = e.targetTouches[0].clientX; + } - componentWillUnmount() { - if (this.handleRef) { - const { ownerDocument } = this.handleRef; - ownerDocument.removeEventListener(eventsFor.mouse.move, this._handleDrag); - ownerDocument.removeEventListener(eventsFor.mouse.stop, this._handleDragStop); - ownerDocument.removeEventListener(eventsFor.touch.move, this._handleDrag); - ownerDocument.removeEventListener(eventsFor.touch.stop, this._handleDragStop); - removeUserSelectStyles(ownerDocument); - } - } + const { offsetParent } = handleRef.current!; + const offsetParentRect = (offsetParent as HTMLElement).getBoundingClientRect(); + const x = clientX + (offsetParent as HTMLElement).scrollLeft - offsetParentRect.left; + + if (lastXRef.current === INVALID_VALUE) { + lastXRef.current = x; + return; + } + + const col = columnRef.current!; + const { width, maxWidth, minWidth: colMinWidth = minWidthRef.current } = col; + const movedX = x - lastXRef.current!; + if (!movedX) return; + + widthRef.current = widthRef.current + movedX; + lastXRef.current = x; - render() { - const { style, column, onResizeStart, onResize, onResizeStop, minWidth, ...rest } = this.props; + let newWidth = widthRef.current; + if (maxWidth && newWidth > maxWidth) { + newWidth = maxWidth; + } else if (newWidth < colMinWidth!) { + newWidth = colMinWidth!; + } + + if (newWidth === width) return; + onResizeRef.current!(col, newWidth); + }, []); + + const handleDragStop = useCallback( + (e: any) => { + if (!isDraggingRef.current) return; + isDraggingRef.current = false; + + onResizeStopRef.current!(columnRef.current!); + + const { ownerDocument } = handleRef.current!; + removeUserSelectStyles(ownerDocument); + ownerDocument.removeEventListener(dragEventFor.move, handleDrag); + ownerDocument.removeEventListener(dragEventFor.stop, handleDragStop); + }, + [handleDrag], + ); + + const handleDragStart = useCallback( + (e: any) => { + if (typeof e.button === 'number' && e.button !== 0) return; + + isDraggingRef.current = true; + lastXRef.current = INVALID_VALUE; + widthRef.current = columnRef.current!.width; + onResizeStart!(columnRef.current!); + + const { ownerDocument } = handleRef.current!; + addUserSelectStyles(ownerDocument); + ownerDocument.addEventListener(dragEventFor.move, handleDrag); + ownerDocument.addEventListener(dragEventFor.stop, handleDragStop); + }, + [onResizeStart, handleDrag, handleDragStop], + ); + + const handleClick = useCallback((e: React.MouseEvent) => { + e.stopPropagation(); + }, []); + + const handleMouseDown = useCallback( + (e: React.MouseEvent) => { + dragEventFor = eventsFor.mouse; + handleDragStart(e as any); + }, + [handleDragStart], + ); + + const handleMouseUp = useCallback( + (e: React.MouseEvent) => { + dragEventFor = eventsFor.mouse; + handleDragStop(e as any); + }, + [handleDragStop], + ); + + const handleTouchStart = useCallback( + (e: React.TouchEvent) => { + dragEventFor = eventsFor.touch; + handleDragStart(e as any); + }, + [handleDragStart], + ); + + const handleTouchEnd = useCallback( + (e: React.TouchEvent) => { + dragEventFor = eventsFor.touch; + handleDragStop(e as any); + }, + [handleDragStop], + ); + + useEffect(() => { + return () => { + if (handleRef.current) { + const { ownerDocument } = handleRef.current; + ownerDocument.removeEventListener(eventsFor.mouse.move, handleDrag); + ownerDocument.removeEventListener(eventsFor.mouse.stop, handleDragStop); + ownerDocument.removeEventListener(eventsFor.touch.move, handleDrag); + ownerDocument.removeEventListener(eventsFor.touch.stop, handleDragStop); + removeUserSelectStyles(ownerDocument); + } + }; + }, [handleDrag, handleDragStop]); return (
{ }} /> ); - } - - _setHandleRef(ref: HTMLDivElement | null) { - this.handleRef = ref; - } - - _handleClick(e: React.MouseEvent) { - e.stopPropagation(); - } - - _handleMouseDown(e: React.MouseEvent) { - dragEventFor = eventsFor.mouse; - this._handleDragStart(e as any); - } - - _handleMouseUp(e: React.MouseEvent) { - dragEventFor = eventsFor.mouse; - this._handleDragStop(e as any); - } - - _handleTouchStart(e: React.TouchEvent) { - dragEventFor = eventsFor.touch; - this._handleDragStart(e as any); - } - - _handleTouchEnd(e: React.TouchEvent) { - dragEventFor = eventsFor.touch; - this._handleDragStop(e as any); - } - - _handleDragStart(e: any) { - if (typeof e.button === 'number' && e.button !== 0) return; - - this.isDragging = true; - this.lastX = INVALID_VALUE; - this.width = this.props.column!.width; - this.props.onResizeStart!(this.props.column!); - - const { ownerDocument } = this.handleRef!; - addUserSelectStyles(ownerDocument); - ownerDocument.addEventListener(dragEventFor.move, this._handleDrag); - ownerDocument.addEventListener(dragEventFor.stop, this._handleDragStop); - } - - _handleDragStop(e: any) { - if (!this.isDragging) return; - this.isDragging = false; - - this.props.onResizeStop!(this.props.column!); - - const { ownerDocument } = this.handleRef!; - removeUserSelectStyles(ownerDocument); - ownerDocument.removeEventListener(dragEventFor.move, this._handleDrag); - ownerDocument.removeEventListener(dragEventFor.stop, this._handleDragStop); - } - - _handleDrag(e: any) { - let clientX = e.clientX; - if (e.type === eventsFor.touch.move) { - e.preventDefault(); - if (e.targetTouches && e.targetTouches[0]) clientX = e.targetTouches[0].clientX; - } - - const { offsetParent } = this.handleRef!; - const offsetParentRect = (offsetParent as HTMLElement).getBoundingClientRect(); - const x = clientX + (offsetParent as HTMLElement).scrollLeft - offsetParentRect.left; - - if (this.lastX === INVALID_VALUE) { - this.lastX = x; - return; - } - - const { column, minWidth: MIN_WIDTH } = this.props; - const { width, maxWidth, minWidth = MIN_WIDTH } = column!; - const movedX = x - this.lastX!; - if (!movedX) return; - - this.width = this.width + movedX; - this.lastX = x; - - let newWidth = this.width; - if (maxWidth && newWidth > maxWidth) { - newWidth = maxWidth; - } else if (newWidth < minWidth!) { - newWidth = minWidth!; - } - - if (newWidth === width) return; - this.props.onResize!(column!, newWidth); - } -} + }, +); + +ColumnResizer.propTypes = { + /** + * Custom style for the drag handler + */ + style: PropTypes.object, + /** + * The column object to be dragged + */ + column: PropTypes.object, + /** + * A callback function when resizing started + * The callback is of the shape of `(column) => *` + */ + onResizeStart: PropTypes.func, + /** + * A callback function when resizing the column + * The callback is of the shape of `(column, width) => *` + */ + onResize: PropTypes.func, + /** + * A callback function when resizing stopped + * The callback is of the shape of `(column) => *` + */ + onResizeStop: PropTypes.func, + /** + * Minimum width of the column could be resized to if the column's `minWidth` is not set + */ + minWidth: PropTypes.number, +}; export default ColumnResizer; From 54ddffdd0f3ce345c07ea2edc68cf7e86fd76b80 Mon Sep 17 00:00:00 2001 From: Nahrin Date: Fri, 19 Jun 2026 11:33:43 -0400 Subject: [PATCH 3/9] refactor: convert TableRow from class to function component (#431) Convert TableRow from React.PureComponent class to a function component wrapped with React.memo. - Replace this.state { measured } with useState hook - Replace this.ref with useRef hook - Replace componentDidMount with useEffect for initial measurement - Replace componentDidUpdate re-measure cycle with two useEffect hooks: one to detect when re-measurement is needed, one to trigger it - Replace _getEventHandlers with useMemo - Replace _handleExpand with useCallback - Replace static defaultProps with JS default parameters - Move propTypes to a property on the function component - No behavioral changes Resolves part of #431 Co-Authored-By: Claude Opus 4.6 --- src/TableRow.tsx | 306 ++++++++++++++++++++++------------------------- 1 file changed, 146 insertions(+), 160 deletions(-) diff --git a/src/TableRow.tsx b/src/TableRow.tsx index af8f1b08..f96bd7dc 100644 --- a/src/TableRow.tsx +++ b/src/TableRow.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState, useRef, useCallback, useEffect, useMemo } from 'react'; import PropTypes from 'prop-types'; import { renderElement } from './utils'; @@ -34,95 +34,127 @@ export interface TableRowProps { [key: string]: any; } -interface TableRowState { - measured: boolean; -} - /** * Row component for BaseTable */ -class TableRow extends React.PureComponent { - ref: HTMLElement | null = null; - - static defaultProps = { - tagName: 'div', - }; - - static propTypes = { - isScrolling: PropTypes.bool, - className: PropTypes.string, - style: PropTypes.object, - columns: PropTypes.arrayOf(PropTypes.object).isRequired, - rowData: PropTypes.object.isRequired, - rowIndex: PropTypes.number.isRequired, - rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - expandColumnKey: PropTypes.string, - depth: PropTypes.number, - rowEventHandlers: PropTypes.object, - rowRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), - cellRenderer: PropTypes.func, - expandIconRenderer: PropTypes.func, - estimatedRowHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), - getIsResetting: PropTypes.func, - onRowHover: PropTypes.func, - onRowExpand: PropTypes.func, - onRowHeightChange: PropTypes.func, - tagName: PropTypes.elementType, - }; - - constructor(props: TableRowProps) { - super(props); - - this.state = { - measured: false, - }; - - this._setRef = this._setRef.bind(this); - this._handleExpand = this._handleExpand.bind(this); - } - - componentDidMount() { - this.props.estimatedRowHeight && this.props.rowIndex >= 0 && this._measureHeight(true); - } - - componentDidUpdate(prevProps: TableRowProps, prevState: TableRowState) { - if ( - this.props.estimatedRowHeight && - this.props.rowIndex >= 0 && - !this.props.getIsResetting!() && - this.state.measured && - prevState.measured - ) { - this.setState({ measured: false }, () => this._measureHeight()); - } - } - - render() { - const { - isScrolling, - className, - style, - columns, - rowIndex, - rowData, - expandColumnKey, - depth, - rowEventHandlers, - estimatedRowHeight, - rowRenderer, - cellRenderer, - expandIconRenderer, - tagName: Tag = 'div', - // omit the following from rest - rowKey, - getIsResetting, - onRowHover, - onRowExpand, - onRowHeightChange, - ...rest - } = this.props; - - const expandIcon = expandIconRenderer!({ rowData, rowIndex, depth, onExpand: this._handleExpand }); +const TableRow: React.FC = React.memo( + ({ + isScrolling, + className, + style, + columns, + rowIndex, + rowData, + expandColumnKey, + depth, + rowEventHandlers, + estimatedRowHeight, + rowRenderer, + cellRenderer, + expandIconRenderer, + tagName: Tag = 'div', + // omit the following from rest + rowKey, + getIsResetting, + onRowHover, + onRowExpand, + onRowHeightChange, + ...rest + }) => { + const [measured, setMeasured] = useState(false); + const ref = useRef(null); + + const measureHeight = useCallback( + (initialMeasure?: boolean) => { + if (!ref.current) return; + + const height = ref.current.getBoundingClientRect().height; + setMeasured(true); + if (initialMeasure || height !== (style as any)?.height) + onRowHeightChange!( + rowKey!, + height, + rowIndex, + columns[0] && !(columns[0] as any).__placeholder__ && columns[0].frozen, + ); + }, + [style, rowKey, onRowHeightChange, rowIndex, columns], + ); + + // componentDidMount: initial measurement + useEffect(() => { + if (estimatedRowHeight && rowIndex >= 0) { + measureHeight(true); + } + }, []); + + // componentDidUpdate: re-measure when props change + const prevMeasuredRef = useRef(false); + useEffect(() => { + const prevMeasured = prevMeasuredRef.current; + prevMeasuredRef.current = measured; + + if (estimatedRowHeight && rowIndex >= 0 && !getIsResetting!() && measured && prevMeasured) { + setMeasured(false); + } + }); + + // When measured transitions to false, trigger re-measurement + useEffect(() => { + if (!measured && estimatedRowHeight && rowIndex >= 0) { + measureHeight(); + } + }, [measured, estimatedRowHeight, rowIndex, measureHeight]); + + const handleExpand = useCallback( + (expanded: boolean) => { + onRowExpand && onRowExpand({ expanded, rowData, rowIndex, rowKey: rowKey! }); + }, + [onRowExpand, rowData, rowIndex, rowKey], + ); + + const eventHandlers = useMemo(() => { + const handlers: Record = rowEventHandlers || {}; + const result: Record void> = {}; + Object.keys(handlers).forEach((eventKey) => { + const callback = handlers[eventKey]; + if (typeof callback === 'function') { + result[eventKey] = (event: React.SyntheticEvent) => { + callback({ rowData, rowIndex, rowKey, event }); + }; + } + }); + + if (onRowHover) { + const mouseEnterHandler = result['onMouseEnter']; + result['onMouseEnter'] = (event: React.SyntheticEvent) => { + onRowHover({ + hovered: true, + rowData, + rowIndex, + rowKey: rowKey!, + event, + }); + mouseEnterHandler && mouseEnterHandler(event); + }; + + const mouseLeaveHandler = result['onMouseLeave']; + result['onMouseLeave'] = (event: React.SyntheticEvent) => { + onRowHover({ + hovered: false, + rowData, + rowIndex, + rowKey: rowKey!, + event, + }); + mouseLeaveHandler && mouseLeaveHandler(event); + }; + } + + return result; + }, [rowEventHandlers, rowData, rowIndex, rowKey, onRowHover]); + + const expandIcon = expandIconRenderer!({ rowData, rowIndex, depth, onExpand: handleExpand }); let cells: React.ReactNode = columns.map((column, columnIndex) => cellRenderer!({ isScrolling, @@ -139,17 +171,15 @@ class TableRow extends React.PureComponent { cells = renderElement(rowRenderer as any, { isScrolling, cells, columns, rowData, rowIndex, depth }); } - const eventHandlers = this._getEventHandlers(rowEventHandlers); - if (estimatedRowHeight && rowIndex >= 0) { const { height, ...otherStyles } = style || ({} as any); return ( {cells} @@ -161,73 +191,29 @@ class TableRow extends React.PureComponent { {cells} ); - } - - _setRef(ref: HTMLElement | null) { - this.ref = ref; - } - - _handleExpand(expanded: boolean) { - const { onRowExpand, rowData, rowIndex, rowKey } = this.props; - onRowExpand && onRowExpand({ expanded, rowData, rowIndex, rowKey: rowKey! }); - } - - _measureHeight(initialMeasure?: boolean) { - if (!this.ref) return; - - const { style, rowKey, onRowHeightChange, rowIndex, columns } = this.props; - const height = this.ref.getBoundingClientRect().height; - this.setState({ measured: true }, () => { - if (initialMeasure || height !== (style as any)?.height) - onRowHeightChange!( - rowKey!, - height, - rowIndex, - columns[0] && !(columns[0] as any).__placeholder__ && columns[0].frozen, - ); - }); - } - - _getEventHandlers(handlers: Record = {}) { - const { rowData, rowIndex, rowKey, onRowHover } = this.props; - const eventHandlers: Record void> = {}; - Object.keys(handlers).forEach((eventKey) => { - const callback = handlers[eventKey]; - if (typeof callback === 'function') { - eventHandlers[eventKey] = (event: React.SyntheticEvent) => { - callback({ rowData, rowIndex, rowKey, event }); - }; - } - }); - - if (onRowHover) { - const mouseEnterHandler = eventHandlers['onMouseEnter']; - eventHandlers['onMouseEnter'] = (event: React.SyntheticEvent) => { - onRowHover({ - hovered: true, - rowData, - rowIndex, - rowKey: rowKey!, - event, - }); - mouseEnterHandler && mouseEnterHandler(event); - }; - - const mouseLeaveHandler = eventHandlers['onMouseLeave']; - eventHandlers['onMouseLeave'] = (event: React.SyntheticEvent) => { - onRowHover({ - hovered: false, - rowData, - rowIndex, - rowKey: rowKey!, - event, - }); - mouseLeaveHandler && mouseLeaveHandler(event); - }; - } - - return eventHandlers; - } -} + }, +); + +TableRow.propTypes = { + isScrolling: PropTypes.bool, + className: PropTypes.string, + style: PropTypes.object, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + rowData: PropTypes.object.isRequired, + rowIndex: PropTypes.number.isRequired, + rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + expandColumnKey: PropTypes.string, + depth: PropTypes.number, + rowEventHandlers: PropTypes.object, + rowRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), + cellRenderer: PropTypes.func, + expandIconRenderer: PropTypes.func, + estimatedRowHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), + getIsResetting: PropTypes.func, + onRowHover: PropTypes.func, + onRowExpand: PropTypes.func, + onRowHeightChange: PropTypes.func, + tagName: PropTypes.elementType, +}; export default TableRow; From 508e011160bc4db04ba7b59b1d5f7afb754a2fb3 Mon Sep 17 00:00:00 2001 From: Nahrin Date: Fri, 19 Jun 2026 11:45:09 -0400 Subject: [PATCH 4/9] refactor: convert TableHeader from class to function component (#431) Convert TableHeader from React.PureComponent class to a function component using forwardRef + useImperativeHandle + React.memo. - Use forwardRef to expose imperative scrollTo() and forceUpdate() methods - Replace this.headerRef with useRef hook - Replace constructor-bound render methods with useCallback hooks - Use useReducer to implement forceUpdate behavior - Update GridTable header ref type to TableHeaderHandle - No behavioral changes Resolves part of #431 Co-Authored-By: Claude Opus 4.6 --- src/GridTable.tsx | 5 +- src/TableHeader.tsx | 128 +++++++++++++++++++++++++------------------- 2 files changed, 76 insertions(+), 57 deletions(-) diff --git a/src/GridTable.tsx b/src/GridTable.tsx index 94a9094a..dc933797 100644 --- a/src/GridTable.tsx +++ b/src/GridTable.tsx @@ -5,6 +5,7 @@ import { FixedSizeGrid, VariableSizeGrid } from 'react-window'; import memoize from 'memoize-one'; import Header from './TableHeader'; +import type { TableHeaderHandle } from './TableHeader'; import { getEstimatedTotalRowsHeight } from './utils'; import type { ColumnShape, RowData, RowKey } from './types'; @@ -41,7 +42,7 @@ export interface GridTableProps { * A wrapper of the Grid for internal only */ class GridTable extends React.PureComponent { - headerRef: InstanceType | null = null; + headerRef: TableHeaderHandle | null = null; bodyRef: InstanceType | InstanceType | null = null; innerRef: HTMLElement | null = null; @@ -215,7 +216,7 @@ class GridTable extends React.PureComponent { ); } - _setHeaderRef(ref: InstanceType | null) { + _setHeaderRef(ref: TableHeaderHandle | null) { this.headerRef = ref; } diff --git a/src/TableHeader.tsx b/src/TableHeader.tsx index a203f0dc..4408b8a1 100644 --- a/src/TableHeader.tsx +++ b/src/TableHeader.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useRef, useCallback, useImperativeHandle, useReducer } from 'react'; import PropTypes from 'prop-types'; import type { ColumnShape, RowData } from './types'; @@ -27,54 +27,60 @@ export interface TableHeaderProps { hoveredRowKey?: string | number | null; } -class TableHeader extends React.PureComponent { - headerRef: HTMLDivElement | null = null; - - static propTypes = { - className: PropTypes.string, - width: PropTypes.number.isRequired, - height: PropTypes.number.isRequired, - headerHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.arrayOf(PropTypes.number)]).isRequired, - rowWidth: PropTypes.number.isRequired, - rowHeight: PropTypes.number.isRequired, - columns: PropTypes.arrayOf(PropTypes.object).isRequired, - data: PropTypes.array.isRequired, - frozenData: PropTypes.array, - headerRenderer: PropTypes.func.isRequired, - rowRenderer: PropTypes.func.isRequired, - }; - - constructor(props: TableHeaderProps) { - super(props); - - this.renderHeaderRow = this.renderHeaderRow.bind(this); - this.renderFrozenRow = this.renderFrozenRow.bind(this); - this._setRef = this._setRef.bind(this); - } - - scrollTo(offset: number) { - requestAnimationFrame(() => { - if (this.headerRef) this.headerRef.scrollLeft = offset; - }); - } - - renderHeaderRow(height: number, index: number) { - const { columns, headerRenderer } = this.props; - if (height <= 0) return null; +export interface TableHeaderHandle { + scrollTo: (offset: number) => void; + forceUpdate: () => void; +} - const style: React.CSSProperties = { width: '100%', height }; - return headerRenderer({ style, columns, headerIndex: index }); - } +const InnerTableHeader = React.forwardRef( + ( + { + className, + width, + height, + headerHeight, + rowWidth, + rowHeight, + columns, + data, + frozenData, + headerRenderer, + rowRenderer, + }, + ref, + ) => { + const headerRef = useRef(null); + const [, forceRender] = useReducer((x: number) => x + 1, 0); + + useImperativeHandle(ref, () => ({ + scrollTo(offset: number) { + requestAnimationFrame(() => { + if (headerRef.current) headerRef.current.scrollLeft = offset; + }); + }, + forceUpdate() { + forceRender(); + }, + })); + + const renderHeaderRow = useCallback( + (rowHeight: number, index: number) => { + if (rowHeight <= 0) return null; + const style: React.CSSProperties = { width: '100%', height: rowHeight }; + return headerRenderer({ style, columns, headerIndex: index }); + }, + [columns, headerRenderer], + ); - renderFrozenRow(rowData: RowData, index: number) { - const { columns, rowHeight, rowRenderer } = this.props; - const style: React.CSSProperties = { width: '100%', height: rowHeight }; - const rowIndex = -index - 1; - return rowRenderer({ style, columns, rowData, rowIndex }); - } + const renderFrozenRow = useCallback( + (rowData: RowData, index: number) => { + const style: React.CSSProperties = { width: '100%', height: rowHeight }; + const rowIndex = -index - 1; + return rowRenderer({ style, columns, rowData, rowIndex }); + }, + [columns, rowHeight, rowRenderer], + ); - render() { - const { className, width, height, rowWidth, headerHeight, frozenData } = this.props; if (height <= 0) return null; const style: React.CSSProperties = { @@ -91,18 +97,30 @@ class TableHeader extends React.PureComponent { const rowHeights = Array.isArray(headerHeight) ? headerHeight : [headerHeight]; return ( -
+
- {rowHeights.map(this.renderHeaderRow)} - {frozenData && frozenData.map(this.renderFrozenRow)} + {rowHeights.map(renderHeaderRow)} + {frozenData && frozenData.map(renderFrozenRow)}
); - } - - _setRef(ref: HTMLDivElement | null) { - this.headerRef = ref; - } -} + }, +); + +InnerTableHeader.propTypes = { + className: PropTypes.string, + width: PropTypes.number.isRequired, + height: PropTypes.number.isRequired, + headerHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.arrayOf(PropTypes.number)]).isRequired, + rowWidth: PropTypes.number.isRequired, + rowHeight: PropTypes.number.isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + data: PropTypes.array.isRequired, + frozenData: PropTypes.array, + headerRenderer: PropTypes.func.isRequired, + rowRenderer: PropTypes.func.isRequired, +}; + +const TableHeader = React.memo(InnerTableHeader); export default TableHeader; From 876d41fc83984d5664fd48e6ed7904c27feddf69 Mon Sep 17 00:00:00 2001 From: Nahrin Date: Fri, 19 Jun 2026 12:34:50 -0400 Subject: [PATCH 5/9] refactor: convert GridTable from class to function component (#431) - Replace class with forwardRef + React.memo function component - Convert instance methods to useImperativeHandle (resetAfterRowIndex, forceUpdateTable, scrollToPosition, scrollToTop, scrollToLeft, scrollToRow, getTotalRowsHeight) - Convert memoized helpers to useMemo/useCallback hooks - Update BaseTable ref types to use GridTableHandle interface Co-Authored-By: Claude Opus 4.6 --- src/BaseTable.tsx | 13 +- src/GridTable.tsx | 309 ++++++++++++++++++++++------------------------ 2 files changed, 157 insertions(+), 165 deletions(-) diff --git a/src/BaseTable.tsx b/src/BaseTable.tsx index bf66fd78..fd8cd0b9 100644 --- a/src/BaseTable.tsx +++ b/src/BaseTable.tsx @@ -4,6 +4,7 @@ import cn from 'classnames'; import memoize from 'memoize-one'; import GridTable from './GridTable'; +import type { GridTableHandle } from './GridTable'; import TableHeaderRow from './TableHeaderRow'; import TableRow from './TableRow'; import TableHeaderCell from './TableHeaderCell'; @@ -444,9 +445,9 @@ class BaseTable extends React.PureComponent { columnManager: ColumnManager; tableNode: HTMLDivElement | null = null; - table: GridTable | null = null; - leftTable: GridTable | null = null; - rightTable: GridTable | null = null; + table: GridTableHandle | null = null; + leftTable: GridTableHandle | null = null; + rightTable: GridTableHandle | null = null; _isResetting: boolean; _resetIndex: number | null; @@ -1191,15 +1192,15 @@ class BaseTable extends React.PureComponent { this.tableNode = ref; } - _setMainTableRef(ref: GridTable | null) { + _setMainTableRef(ref: GridTableHandle | null) { this.table = ref; } - _setLeftTableRef(ref: GridTable | null) { + _setLeftTableRef(ref: GridTableHandle | null) { this.leftTable = ref; } - _setRightTableRef(ref: GridTable | null) { + _setRightTableRef(ref: GridTableHandle | null) { this.rightTable = ref; } diff --git a/src/GridTable.tsx b/src/GridTable.tsx index dc933797..7d57418b 100644 --- a/src/GridTable.tsx +++ b/src/GridTable.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useRef, useCallback, useMemo, useImperativeHandle } from 'react'; import PropTypes from 'prop-types'; import cn from 'classnames'; import { FixedSizeGrid, VariableSizeGrid } from 'react-window'; @@ -38,109 +38,22 @@ export interface GridTableProps { [key: string]: any; } +export interface GridTableHandle { + resetAfterRowIndex: (rowIndex?: number, shouldForceUpdate?: boolean) => void; + forceUpdateTable: () => void; + scrollToPosition: (args: { scrollLeft?: number; scrollTop?: number }) => void; + scrollToTop: (scrollTop: number) => void; + scrollToLeft: (scrollLeft: number) => void; + scrollToRow: (rowIndex?: number, align?: string) => void; + getTotalRowsHeight: () => number; +} + /** * A wrapper of the Grid for internal only */ -class GridTable extends React.PureComponent { - headerRef: TableHeaderHandle | null = null; - bodyRef: InstanceType | InstanceType | null = null; - innerRef: HTMLElement | null = null; - - _resetColumnWidthCache: (bodyWidth: number) => void; - _getEstimatedTotalRowsHeight: typeof getEstimatedTotalRowsHeight; - - static propTypes = { - containerStyle: PropTypes.object, - classPrefix: PropTypes.string, - className: PropTypes.string, - width: PropTypes.number.isRequired, - height: PropTypes.number.isRequired, - headerHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.arrayOf(PropTypes.number)]).isRequired, - headerWidth: PropTypes.number.isRequired, - bodyWidth: PropTypes.number.isRequired, - rowHeight: PropTypes.number.isRequired, - estimatedRowHeight: PropTypes.oneOfType([PropTypes.func, PropTypes.number]), - getRowHeight: PropTypes.func, - columns: PropTypes.arrayOf(PropTypes.object).isRequired, - data: PropTypes.array.isRequired, - frozenData: PropTypes.array, - rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - useIsScrolling: PropTypes.bool, - overscanRowCount: PropTypes.number, - hoveredRowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - style: PropTypes.object, - onScrollbarPresenceChange: PropTypes.func, - onScroll: PropTypes.func, - onRowsRendered: PropTypes.func, - headerRenderer: PropTypes.func.isRequired, - rowRenderer: PropTypes.func.isRequired, - }; - - constructor(props: GridTableProps) { - super(props); - - this._setHeaderRef = this._setHeaderRef.bind(this); - this._setBodyRef = this._setBodyRef.bind(this); - this._setInnerRef = this._setInnerRef.bind(this); - this._itemKey = this._itemKey.bind(this); - this._getBodyWidth = this._getBodyWidth.bind(this); - this._handleItemsRendered = this._handleItemsRendered.bind(this); - this._resetColumnWidthCache = memoize((_bodyWidth: number) => { - if (!this.props.estimatedRowHeight) return; - this.bodyRef && (this.bodyRef as any).resetAfterColumnIndex(0, false); - }); - this._getEstimatedTotalRowsHeight = memoize(getEstimatedTotalRowsHeight); - - this.renderRow = this.renderRow.bind(this); - } - - resetAfterRowIndex(rowIndex: number = 0, shouldForceUpdate?: boolean) { - if (!this.props.estimatedRowHeight) return; - this.bodyRef && (this.bodyRef as any).resetAfterRowIndex(rowIndex, shouldForceUpdate); - } - - forceUpdateTable() { - this.headerRef && this.headerRef.forceUpdate(); - this.bodyRef && this.bodyRef.forceUpdate(); - } - - scrollToPosition(args: { scrollLeft?: number; scrollTop?: number }) { - this.headerRef && this.headerRef.scrollTo(args.scrollLeft || 0); - this.bodyRef && this.bodyRef.scrollTo(args as any); - } - - scrollToTop(scrollTop: number) { - this.bodyRef && this.bodyRef.scrollTo({ scrollTop } as any); - } - - scrollToLeft(scrollLeft: number) { - this.headerRef && this.headerRef.scrollTo(scrollLeft); - this.bodyRef && (this.bodyRef as any).scrollToPosition({ scrollLeft }); - } - - scrollToRow(rowIndex: number = 0, align: string = 'auto') { - this.bodyRef && (this.bodyRef as any).scrollToItem({ rowIndex, align }); - } - - getTotalRowsHeight(): number { - const { data, rowHeight, estimatedRowHeight } = this.props; - - if (estimatedRowHeight) { - return ( - (this.innerRef && this.innerRef.clientHeight) || this._getEstimatedTotalRowsHeight(data, estimatedRowHeight) - ); - } - return data.length * rowHeight; - } - - renderRow(args: any) { - const { data, columns, rowRenderer } = this.props; - const rowData = data[args.rowIndex]; - return rowRenderer({ ...args, columns, rowData }); - } - - render() { - const { +const InnerGridTable = React.forwardRef( + ( + { containerStyle, classPrefix, className, @@ -161,23 +74,114 @@ class GridTable extends React.PureComponent { style, onScrollbarPresenceChange, ...rest - } = this.props; - const headerHeight = this._getHeaderHeight(); + }, + ref, + ) => { + const headerRef = useRef(null); + const bodyRef = useRef | InstanceType | null>(null); + const innerRef = useRef(null); + + // Memoized helpers — stable across renders + const resetColumnWidthCache = useMemo( + () => + memoize((_bodyWidth: number) => { + if (!estimatedRowHeight) return; + bodyRef.current && (bodyRef.current as any).resetAfterColumnIndex(0, false); + }), + [estimatedRowHeight], + ); + + const getEstimatedTotalRowsHeightMemo = useMemo(() => memoize(getEstimatedTotalRowsHeight), []); + + const getHeaderHeight = useCallback((): number => { + const { headerHeight } = rest; + if (Array.isArray(headerHeight)) { + return headerHeight.reduce((sum: number, h: number) => sum + h, 0); + } + return headerHeight; + }, [rest.headerHeight]); + + const getBodyWidth = useCallback((): number => { + return bodyWidth; + }, [bodyWidth]); + + const itemKey = useCallback( + ({ rowIndex }: { rowIndex: number }) => { + return data[rowIndex][rest.rowKey as string]; + }, + [data, rest.rowKey], + ); + + const handleItemsRendered = useCallback( + ({ overscanRowStartIndex, overscanRowStopIndex, visibleRowStartIndex, visibleRowStopIndex }: any) => { + rest.onRowsRendered!({ + overscanStartIndex: overscanRowStartIndex, + overscanStopIndex: overscanRowStopIndex, + startIndex: visibleRowStartIndex, + stopIndex: visibleRowStopIndex, + }); + }, + [rest.onRowsRendered], + ); + + const renderRow = useCallback( + (args: any) => { + const rowData = data[args.rowIndex]; + return rest.rowRenderer({ ...args, columns: rest.columns, rowData }); + }, + [data, rest.columns, rest.rowRenderer], + ); + + useImperativeHandle(ref, () => ({ + resetAfterRowIndex(rowIndex: number = 0, shouldForceUpdate?: boolean) { + if (!estimatedRowHeight) return; + bodyRef.current && (bodyRef.current as any).resetAfterRowIndex(rowIndex, shouldForceUpdate); + }, + forceUpdateTable() { + headerRef.current && headerRef.current.forceUpdate(); + bodyRef.current && bodyRef.current.forceUpdate(); + }, + scrollToPosition(args: { scrollLeft?: number; scrollTop?: number }) { + headerRef.current && headerRef.current.scrollTo(args.scrollLeft || 0); + bodyRef.current && bodyRef.current.scrollTo(args as any); + }, + scrollToTop(scrollTop: number) { + bodyRef.current && bodyRef.current.scrollTo({ scrollTop } as any); + }, + scrollToLeft(scrollLeft: number) { + headerRef.current && headerRef.current.scrollTo(scrollLeft); + bodyRef.current && (bodyRef.current as any).scrollToPosition({ scrollLeft }); + }, + scrollToRow(rowIndex: number = 0, align: string = 'auto') { + bodyRef.current && (bodyRef.current as any).scrollToItem({ rowIndex, align }); + }, + getTotalRowsHeight(): number { + if (estimatedRowHeight) { + return ( + (innerRef.current && innerRef.current.clientHeight) || + getEstimatedTotalRowsHeightMemo(data, estimatedRowHeight) + ); + } + return data.length * rowHeight; + }, + })); + + const headerHeight = getHeaderHeight(); const frozenRowCount = frozenData ? frozenData.length : 0; const frozenRowsHeight = rowHeight * frozenRowCount; const cls = cn(`${classPrefix}__table`, className); const containerProps = containerStyle ? { style: containerStyle } : null; const Grid: any = estimatedRowHeight ? VariableSizeGrid : FixedSizeGrid; - this._resetColumnWidthCache(bodyWidth); + resetColumnWidthCache(bodyWidth); return (
{ estimatedRowHeight={typeof estimatedRowHeight === 'function' ? undefined : estimatedRowHeight} rowCount={data.length} overscanRowCount={overscanRowCount} - columnWidth={estimatedRowHeight ? this._getBodyWidth : bodyWidth} + columnWidth={estimatedRowHeight ? getBodyWidth : bodyWidth} columnCount={1} overscanColumnCount={0} useIsScrolling={useIsScrolling} hoveredRowKey={hoveredRowKey} onScroll={onScroll} - onItemsRendered={this._handleItemsRendered} - children={this.renderRow} + onItemsRendered={handleItemsRendered} + children={renderRow} /> {headerHeight + frozenRowsHeight > 0 && (
0 ? hoveredRowKey : null} /> )}
); - } - - _setHeaderRef(ref: TableHeaderHandle | null) { - this.headerRef = ref; - } - - _setBodyRef(ref: any) { - this.bodyRef = ref; - } - - _setInnerRef(ref: HTMLElement | null) { - this.innerRef = ref; - } - - _itemKey({ rowIndex }: { rowIndex: number }) { - const { data, rowKey } = this.props; - return data[rowIndex][rowKey as string]; - } - - _getHeaderHeight(): number { - const { headerHeight } = this.props; - if (Array.isArray(headerHeight)) { - return headerHeight.reduce((sum, height) => sum + height, 0); - } - return headerHeight; - } - - _getBodyWidth(): number { - return this.props.bodyWidth; - } - - _handleItemsRendered({ - overscanRowStartIndex, - overscanRowStopIndex, - visibleRowStartIndex, - visibleRowStopIndex, - }: any) { - this.props.onRowsRendered!({ - overscanStartIndex: overscanRowStartIndex, - overscanStopIndex: overscanRowStopIndex, - startIndex: visibleRowStartIndex, - stopIndex: visibleRowStopIndex, - }); - } -} + }, +); + +InnerGridTable.propTypes = { + containerStyle: PropTypes.object, + classPrefix: PropTypes.string, + className: PropTypes.string, + width: PropTypes.number.isRequired, + height: PropTypes.number.isRequired, + headerHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.arrayOf(PropTypes.number)]).isRequired, + headerWidth: PropTypes.number.isRequired, + bodyWidth: PropTypes.number.isRequired, + rowHeight: PropTypes.number.isRequired, + estimatedRowHeight: PropTypes.oneOfType([PropTypes.func, PropTypes.number]), + getRowHeight: PropTypes.func, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + data: PropTypes.array.isRequired, + frozenData: PropTypes.array, + rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + useIsScrolling: PropTypes.bool, + overscanRowCount: PropTypes.number, + hoveredRowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + style: PropTypes.object, + onScrollbarPresenceChange: PropTypes.func, + onScroll: PropTypes.func, + onRowsRendered: PropTypes.func, + headerRenderer: PropTypes.func.isRequired, + rowRenderer: PropTypes.func.isRequired, +}; + +const GridTable = React.memo(InnerGridTable); export default GridTable; From 0c8510619346114225f00490773f5e79aeb26b23 Mon Sep 17 00:00:00 2001 From: Nahrin Date: Fri, 19 Jun 2026 12:37:09 -0400 Subject: [PATCH 6/9] refactor: convert Column from class to function component (#431) - Replace class with function component returning null - Move static properties (Alignment, FrozenDirection) to direct assignments Co-Authored-By: Claude Opus 4.6 --- src/Column.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Column.tsx b/src/Column.tsx index b786d846..3ab857ba 100644 --- a/src/Column.tsx +++ b/src/Column.tsx @@ -19,10 +19,13 @@ export const FrozenDirection: Record = { /** * Column for BaseTable */ -class Column extends React.Component { - static Alignment = Alignment; - static FrozenDirection = FrozenDirection; -} +const Column: React.FC & { + Alignment: typeof Alignment; + FrozenDirection: typeof FrozenDirection; +} = () => null; + +Column.Alignment = Alignment; +Column.FrozenDirection = FrozenDirection; Column.propTypes = { /** From 5c681c1e4edb0b993c34d567a0e73895b559b4b3 Mon Sep 17 00:00:00 2001 From: Nahrin Date: Fri, 19 Jun 2026 12:47:13 -0400 Subject: [PATCH 7/9] refactor: convert BaseTable from class to function component (#431) - Replace class with forwardRef + React.memo function component - Convert state to useState hooks (scrollbarSize, hoveredRowKey, resizingKey, resizingWidth, expandedRowKeys) - Convert instance variables to useRef (scroll position, row height maps, scrollbar sizes, data tracking, column manager) - Convert memoized helpers to useMemo with memoize-one - Convert throttled column resize handler to stable useMemo callback - Convert debounced row height update to stable useMemo callback - Convert lifecycle methods to useEffect hooks - Expose public API via useImperativeHandle (getDOMNode, getColumnManager, scroll methods, expand methods, etc.) - Add BaseTableHandle interface for imperative ref type - Maintain static properties (Column, PlaceholderKey) on memo wrapper Co-Authored-By: Claude Opus 4.6 --- src/BaseTable.tsx | 2112 ++++++++++++++++++++++----------------------- 1 file changed, 1051 insertions(+), 1061 deletions(-) diff --git a/src/BaseTable.tsx b/src/BaseTable.tsx index fd8cd0b9..8dce0302 100644 --- a/src/BaseTable.tsx +++ b/src/BaseTable.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState, useRef, useCallback, useMemo, useEffect, useReducer, useImperativeHandle } from 'react'; import PropTypes from 'prop-types'; import cn from 'classnames'; import memoize from 'memoize-one'; @@ -146,567 +146,509 @@ export interface BaseTableProps { components?: TableComponents; } -interface BaseTableState { - scrollbarSize: number; - hoveredRowKey: RowKey | null; - resizingKey: string | null; - resizingWidth: number; - expandedRowKeys: RowKey[]; +export interface BaseTableHandle { + getDOMNode: () => HTMLDivElement | null; + getColumnManager: () => ColumnManager; + getExpandedRowKeys: () => RowKey[]; + getExpandedState: () => { + expandedData: RowData[]; + expandedRowKeys: RowKey[]; + expandedDepthMap: Record; + }; + getTotalRowsHeight: () => number; + getTotalColumnsWidth: () => number; + forceUpdateTable: () => void; + resetAfterRowIndex: (rowIndex?: number, shouldForceUpdate?: boolean) => void; + resetRowHeightCache: () => void; + scrollToPosition: (offset: { scrollLeft: number; scrollTop: number }) => void; + scrollToTop: (scrollTop: number) => void; + scrollToLeft: (scrollLeft: number) => void; + scrollToRow: (rowIndex?: number, align?: string) => void; + setExpandedRowKeys: (expandedRowKeys: RowKey[]) => void; } +const DEFAULT_PROPS = { + classPrefix: 'BaseTable', + rowKey: 'id' as string | number, + data: [] as RowData[], + frozenData: [] as RowData[], + fixed: false, + headerHeight: 50 as number | number[], + rowHeight: 50, + footerHeight: 0, + defaultExpandedRowKeys: [] as RowKey[], + sortBy: {} as SortByShape, + useIsScrolling: false, + overscanRowCount: 1, + onEndReachedThreshold: 500, + getScrollbarSize: defaultGetScrollbarSize, + ignoreFunctionInColumnCompare: true, + onScroll: noop as (...args: any[]) => void, + onRowsRendered: noop as (...args: any[]) => void, + onScrollbarPresenceChange: noop as (...args: any[]) => void, + onRowExpand: noop as (...args: any[]) => void, + onExpandedRowsChange: noop as (...args: any[]) => void, + onColumnSort: noop as (...args: any[]) => void, + onColumnResize: noop as (...args: any[]) => void, + onColumnResizeEnd: noop as (...args: any[]) => void, +}; + /** * React table component */ -class BaseTable extends React.PureComponent { - static Column = Column; - static PlaceholderKey = ColumnManager.PlaceholderKey; - - static defaultProps = { - classPrefix: 'BaseTable', - rowKey: 'id', - data: [], - frozenData: [], - fixed: false, - headerHeight: 50, - rowHeight: 50, - footerHeight: 0, - defaultExpandedRowKeys: [], - sortBy: {}, - useIsScrolling: false, - overscanRowCount: 1, - onEndReachedThreshold: 500, - getScrollbarSize: defaultGetScrollbarSize, - ignoreFunctionInColumnCompare: true, - - onScroll: noop, - onRowsRendered: noop, - onScrollbarPresenceChange: noop, - onRowExpand: noop, - onExpandedRowsChange: noop, - onColumnSort: noop, - onColumnResize: noop, - onColumnResizeEnd: noop, +const InnerBaseTable = React.forwardRef((rawProps, ref) => { + // Merge with defaults (equivalent to static defaultProps) + const props = { ...DEFAULT_PROPS, ...rawProps }; + const { + classPrefix, + className, + style, + children, + columns, + data, + frozenData, + rowKey, + width, + height, + maxHeight, + rowHeight, + estimatedRowHeight, + headerHeight, + footerHeight, + fixed, + disabled, + overlayRenderer, + emptyRenderer, + footerRenderer, + headerRenderer, + rowRenderer, + headerClassName, + rowClassName, + rowProps: rowPropsProp, + headerProps: headerPropsProp, + headerCellProps: headerCellPropsProp, + cellProps: cellPropsProp, + expandIconProps, + expandColumnKey, + defaultExpandedRowKeys, + expandedRowKeys: expandedRowKeysProp, + onRowExpand, + onExpandedRowsChange, + sortBy, + sortState, + onColumnSort, + onColumnResize, + onColumnResizeEnd, + useIsScrolling, + overscanRowCount, + getScrollbarSize, + onScroll, + onEndReached, + onEndReachedThreshold, + onRowsRendered, + onScrollbarPresenceChange, + rowEventHandlers, + ignoreFunctionInColumnCompare, + components, + } = props; + + // State + const [scrollbarSize, setScrollbarSize] = useState(0); + const [hoveredRowKey, setHoveredRowKey] = useState(null); + const [resizingKey, setResizingKey] = useState(null); + const [resizingWidth, setResizingWidth] = useState(0); + const [expandedRowKeysState, setExpandedRowKeysState] = useState(() => + cloneArray(rawProps.defaultExpandedRowKeys || []), + ); + const [, forceRender] = useReducer((x: number) => x + 1, 0); + + // DOM/component refs + const tableNodeRef = useRef(null); + const tableRef = useRef(null); + const leftTableRef = useRef(null); + const rightTableRef = useRef(null); + + // Instance variable refs + const isResettingRef = useRef(false); + const resetIndexRef = useRef(null); + const rowHeightMapRef = useRef>({}); + const rowHeightMapBufferRef = useRef>({}); + const mainRowHeightMapRef = useRef>({}); + const leftRowHeightMapRef = useRef>({}); + const rightRowHeightMapRef = useRef>({}); + const scrollRef = useRef({ scrollLeft: 0, scrollTop: 0 }); + const scrollHeightRef = useRef(0); + const lastScannedRowIndexRef = useRef(-1); + const hasDataChangedSinceEndReachedRef = useRef(true); + const dataRef = useRef(data); + const depthMapRef = useRef>({}); + const horizontalScrollbarSizeRef = useRef(0); + const verticalScrollbarSizeRef = useRef(0); + const scrollbarPresenceChangedRef = useRef(false); + const totalRowsHeightRef = useRef(0); + + // ColumnManager — initialized once + const columnManagerRef = useRef(null); + if (!columnManagerRef.current) { + columnManagerRef.current = new ColumnManager(getColumns(columns, children), fixed); + } + const columnManager = columnManagerRef.current; + + // Refs to hold latest prop values for use in stable callbacks + const onColumnResizeRef = useRef(onColumnResize); + onColumnResizeRef.current = onColumnResize; + const estimatedRowHeightRef = useRef(estimatedRowHeight); + estimatedRowHeightRef.current = estimatedRowHeight; + const ignoreFunctionInColumnCompareRef = useRef(ignoreFunctionInColumnCompare); + ignoreFunctionInColumnCompareRef.current = ignoreFunctionInColumnCompare; + + // Memoized helpers (stable across renders) + const _getLeftTableContainerStyle = useMemo(() => memoize(getContainerStyle), []); + const _getRightTableContainerStyle = useMemo(() => memoize(getContainerStyle), []); + + const _flattenOnKeys = useMemo( + () => + memoize((tree: RowData[], keys: RowKey[], dataKey: string | number) => { + depthMapRef.current = {}; + return flattenOnKeys(tree, keys, depthMapRef.current, dataKey as string); + }), + [], + ); + + const _getEstimatedTotalRowsHeight = useMemo(() => memoize(getEstimatedTotalRowsHeight), []); + + const _resetColumnManager = useMemo( + () => + memoize( + (cols: ColumnShape[], isFixed: boolean) => { + columnManager.reset(cols, isFixed); + if (estimatedRowHeightRef.current && isFixed) { + if (!columnManager.hasLeftFrozenColumns()) { + leftRowHeightMapRef.current = {}; + } + if (!columnManager.hasRightFrozenColumns()) { + rightRowHeightMapRef.current = {}; + } + } + }, + (newArgs: any, lastArgs: any) => isObjectEqual(newArgs, lastArgs, ignoreFunctionInColumnCompareRef.current), + ), + [columnManager], + ); + + // Helper methods + const _prefixClass = (cls: string): string => `${classPrefix}__${cls}`; + + const _getComponent = (name: keyof TableComponents): React.ComponentType => { + if (components && components[name]) return components[name] as React.ComponentType; + return DEFAULT_COMPONENTS[name]; }; - static propTypes = { - /** - * Prefix for table's inner className - */ - classPrefix: PropTypes.string, - /** - * Class name for the table - */ - className: PropTypes.string, - /** - * Custom style for the table - */ - style: PropTypes.object, - /** - * A collection of Column - */ - children: PropTypes.node, - /** - * Columns for the table - */ - columns: PropTypes.arrayOf(PropTypes.shape(Column.propTypes as any)), - /** - * The data for the table - */ - data: PropTypes.array.isRequired, - /** - * The data be frozen to top, `rowIndex` is negative and started from `-1` - */ - frozenData: PropTypes.array, - /** - * The key field of each data item - */ - rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - /** - * The width of the table - */ - width: PropTypes.number.isRequired, - /** - * The height of the table, will be ignored if `maxHeight` is set - */ - height: PropTypes.number, - /** - * The max height of the table, the table's height will auto change when data changes, - * will turns to vertical scroll if reaches the max height - */ - maxHeight: PropTypes.number, - /** - * The height of each table row, will be only used by frozen rows if `estimatedRowHeight` is set - */ - rowHeight: PropTypes.number, - /** - * Estimated row height, the real height will be measure dynamically according to the content - * The callback is of the shape of `({ rowData, rowIndex }) => number` - */ - estimatedRowHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), - /** - * The height of the table header, set to 0 to hide the header, could be an array to render multi headers. - */ - headerHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.arrayOf(PropTypes.number)]).isRequired, - /** - * The height of the table footer - */ - footerHeight: PropTypes.number, - /** - * Whether the width of the columns are fixed or flexible - */ - fixed: PropTypes.bool, - /** - * Whether the table is disabled - */ - disabled: PropTypes.bool, - /** - * Custom renderer on top of the table component - */ - overlayRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), - /** - * Custom renderer when the length of data is 0 - */ - emptyRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), - /** - * Custom footer renderer, available only if `footerHeight` is larger then 0 - */ - footerRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), - /** - * Custom header renderer - * The renderer receives props `{ cells, columns, headerIndex }` - */ - headerRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), - /** - * Custom row renderer - * The renderer receives props `{ isScrolling, cells, columns, rowData, rowIndex, depth }` - */ - rowRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), - /** - * Class name for the table header, could be a callback to return the class name - * The callback is of the shape of `({ columns, headerIndex }) => string` - */ - headerClassName: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), - /** - * Class name for the table row, could be a callback to return the class name - * The callback is of the shape of `({ columns, rowData, rowIndex }) => string` - */ - rowClassName: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), - /** - * Extra props applied to header element - * The handler is of the shape of `({ columns, headerIndex }) object` - */ - headerProps: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), - /** - * Extra props applied to header cell element - * The handler is of the shape of `({ columns, column, columnIndex, headerIndex }) => object` - */ - headerCellProps: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), - /** - * Extra props applied to row element - * The handler is of the shape of `({ columns, rowData, rowIndex }) => object` - */ - rowProps: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), - /** - * Extra props applied to row cell element - * The handler is of the shape of `({ columns, column, columnIndex, rowData, rowIndex }) => object` - */ - cellProps: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), - /** - * Extra props applied to ExpandIcon component - * The handler is of the shape of `({ rowData, rowIndex, depth, expandable, expanded }) => object` - */ - expandIconProps: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), - /** - * The key for the expand column which render the expand icon if the data is a tree - */ - expandColumnKey: PropTypes.string, - /** - * Default expanded row keys when initialize the table - */ - defaultExpandedRowKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), - /** - * Controlled expanded row keys - */ - expandedRowKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), - /** - * A callback function when expand or collapse a tree node - * The handler is of the shape of `({ expanded, rowData, rowIndex, rowKey }) => *` - */ - onRowExpand: PropTypes.func, - /** - * A callback function when the expanded row keys changed - * The handler is of the shape of `(expandedRowKeys) => *` - */ - onExpandedRowsChange: PropTypes.func, - /** - * The sort state for the table, will be ignored if `sortState` is set - */ - sortBy: PropTypes.shape({ - /** - * Sort key - */ - key: PropTypes.string, - /** - * Sort order - */ - order: PropTypes.oneOf([SortOrder.ASC, SortOrder.DESC]), - }), - /** - * Multiple columns sort state for the table - * - * example: - * ```js - * { - * 'column-0': SortOrder.ASC, - * 'column-1': SortOrder.DESC, - * } - * ``` - */ - sortState: PropTypes.object, - /** - * A callback function for the header cell click event - * The handler is of the shape of `({ column, key, order }) => *` - */ - onColumnSort: PropTypes.func, - /** - * A callback function when resizing the column width - * The handler is of the shape of `({ column, width }) => *` - */ - onColumnResize: PropTypes.func, - /** - * A callback function when resizing the column width ends - * The handler is of the shape of `({ column, width }) => *` - */ - onColumnResizeEnd: PropTypes.func, - /** - * Adds an additional isScrolling parameter to the row renderer. - * This parameter can be used to show a placeholder row while scrolling. - */ - useIsScrolling: PropTypes.bool, - /** - * Number of rows to render above/below the visible bounds of the list - */ - overscanRowCount: PropTypes.number, - /** - * Custom scrollbar size measurement - */ - getScrollbarSize: PropTypes.func, - /** - * A callback function when scrolling the table - * The handler is of the shape of `({ scrollLeft, scrollTop, horizontalScrollDirection, verticalScrollDirection, scrollUpdateWasRequested }) => *` - * - * `scrollLeft` and `scrollTop` are numbers. - * - * `horizontalDirection` and `verticalDirection` are either `forward` or `backward`. - * - * `scrollUpdateWasRequested` is a boolean. This value is true if the scroll was caused by `scrollTo*`, - * and false if it was the result of a user interaction in the browser. - */ - onScroll: PropTypes.func, - /** - * A callback function when scrolling the table within `onEndReachedThreshold` of the bottom - * The handler is of the shape of `({ distanceFromEnd }) => *` - */ - onEndReached: PropTypes.func, - /** - * Threshold in pixels for calling `onEndReached`. - */ - onEndReachedThreshold: PropTypes.number, - /** - * A callback function with information about the slice of rows that were just rendered - * The handler is of the shape of `({ overscanStartIndex, overscanStopIndex, startIndex, stopIndex }) => *` - */ - onRowsRendered: PropTypes.func, - /** - * A callback function when the scrollbar presence state changed - * The handler is of the shape of `({ size, vertical, horizontal }) => *` - */ - onScrollbarPresenceChange: PropTypes.func, - /** - * A object for the row event handlers - * Each of the keys is row event name, like `onClick`, `onDoubleClick` and etc. - * Each of the handlers is of the shape of `({ rowData, rowIndex, rowKey, event }) => *` - */ - rowEventHandlers: PropTypes.object, - /** - * whether to ignore function properties while comparing column definition - */ - ignoreFunctionInColumnCompare: PropTypes.bool, - /** - * A object for the custom components, like `ExpandIcon` and `SortIndicator` - */ - components: PropTypes.shape({ - TableCell: PropTypes.elementType, - TableHeaderCell: PropTypes.elementType, - ExpandIcon: PropTypes.elementType, - SortIndicator: PropTypes.elementType, - }), + const getExpandedRowKeys = (): RowKey[] => { + return expandedRowKeysProp !== undefined ? expandedRowKeysProp || EMPTY_ARRAY : expandedRowKeysState; }; - columnManager: ColumnManager; - tableNode: HTMLDivElement | null = null; - table: GridTableHandle | null = null; - leftTable: GridTableHandle | null = null; - rightTable: GridTableHandle | null = null; - - _isResetting: boolean; - _resetIndex: number | null; - _rowHeightMap: Record; - _rowHeightMapBuffer: Record; - _mainRowHeightMap: Record; - _leftRowHeightMap: Record; - _rightRowHeightMap: Record; - _scroll: { scrollLeft: number; scrollTop: number }; - _scrollHeight: number; - _lastScannedRowIndex: number; - _hasDataChangedSinceEndReached: boolean; - _data: RowData[]; - _depthMap: Record; - _horizontalScrollbarSize: number; - _verticalScrollbarSize: number; - _scrollbarPresenceChanged: boolean; - _totalRowsHeight: number; - - _getLeftTableContainerStyle: typeof getContainerStyle; - _getRightTableContainerStyle: typeof getContainerStyle; - _flattenOnKeys: (tree: RowData[], keys: RowKey[], dataKey: string | number) => RowData[]; - _resetColumnManager: (columns: ColumnShape[], fixed: boolean) => void; - _getEstimatedTotalRowsHeight: typeof getEstimatedTotalRowsHeight; - _getRowHeight: (rowIndex: number) => number; - _updateRowHeights: () => void; - - constructor(props: BaseTableProps) { - super(props); - - const { columns, children, defaultExpandedRowKeys } = props; - this.state = { - scrollbarSize: 0, - hoveredRowKey: null, - resizingKey: null, - resizingWidth: 0, - expandedRowKeys: cloneArray(defaultExpandedRowKeys || []), - }; - this.columnManager = new ColumnManager(getColumns(columns, children), props.fixed!); - - this._setContainerRef = this._setContainerRef.bind(this); - this._setMainTableRef = this._setMainTableRef.bind(this); - this._setLeftTableRef = this._setLeftTableRef.bind(this); - this._setRightTableRef = this._setRightTableRef.bind(this); - - this.renderExpandIcon = this.renderExpandIcon.bind(this); - this.renderRow = this.renderRow.bind(this); - this.renderRowCell = this.renderRowCell.bind(this); - this.renderHeader = this.renderHeader.bind(this); - this.renderHeaderCell = this.renderHeaderCell.bind(this); - - this._handleScroll = this._handleScroll.bind(this); - this._handleVerticalScroll = this._handleVerticalScroll.bind(this); - this._handleRowsRendered = this._handleRowsRendered.bind(this); - this._handleRowHover = this._handleRowHover.bind(this); - this._handleRowExpand = this._handleRowExpand.bind(this); - this._handleColumnResize = throttle(this._handleColumnResize.bind(this), RESIZE_THROTTLE_WAIT); - this._handleColumnResizeStart = this._handleColumnResizeStart.bind(this); - this._handleColumnResizeStop = this._handleColumnResizeStop.bind(this); - this._handleColumnSort = this._handleColumnSort.bind(this); - this._handleFrozenRowHeightChange = this._handleFrozenRowHeightChange.bind(this); - this._handleRowHeightChange = this._handleRowHeightChange.bind(this); - - this._getLeftTableContainerStyle = memoize(getContainerStyle); - this._getRightTableContainerStyle = memoize(getContainerStyle); - this._flattenOnKeys = memoize((tree: RowData[], keys: RowKey[], dataKey: string | number) => { - this._depthMap = {}; - return flattenOnKeys(tree, keys, this._depthMap, dataKey as string); - }); - this._resetColumnManager = memoize( - (columns: ColumnShape[], fixed: boolean) => { - this.columnManager.reset(columns, fixed); + const _getHeaderHeight = (): number => { + if (Array.isArray(headerHeight)) { + return headerHeight.reduce((sum, h) => sum + h, 0); + } + return headerHeight; + }; - if (this.props.estimatedRowHeight && fixed) { - if (!this.columnManager.hasLeftFrozenColumns()) { - this._leftRowHeightMap = {}; - } - if (!this.columnManager.hasRightFrozenColumns()) { - this._rightRowHeightMap = {}; + const _getFrozenRowsHeight = (): number => { + return frozenData.length * rowHeight; + }; + + const getTotalRowsHeight = (): number => { + if (estimatedRowHeight) { + return tableRef.current + ? tableRef.current.getTotalRowsHeight() + : _getEstimatedTotalRowsHeight(dataRef.current, estimatedRowHeight); + } + return dataRef.current.length * rowHeight; + }; + + const _getTableHeight = (): number => { + let tableHeight = height! - footerHeight; + + if (maxHeight! > 0) { + const frozenRowsHeight = _getFrozenRowsHeight(); + const totalRowsHeight = getTotalRowsHeight(); + const hHeight = _getHeaderHeight(); + const totalHeight = hHeight + frozenRowsHeight + totalRowsHeight + horizontalScrollbarSizeRef.current; + tableHeight = Math.min(totalHeight, maxHeight! - footerHeight); + } + + return tableHeight; + }; + + const _getBodyHeight = (): number => { + return _getTableHeight() - _getHeaderHeight() - _getFrozenRowsHeight(); + }; + + const _getFrozenContainerHeight = (): number => { + const tableHeight = _getTableHeight() - (dataRef.current.length > 0 ? horizontalScrollbarSizeRef.current : 0); + if (maxHeight! > 0) return tableHeight; + + const totalHeight = getTotalRowsHeight() + _getHeaderHeight() + _getFrozenRowsHeight(); + return Math.min(tableHeight, totalHeight); + }; + + const _calcScrollbarSizes = () => { + const totalRowsHeight = getTotalRowsHeight(); + const totalColumnsWidth = columnManager.getColumnsWidth(); + + const prevHorizontalScrollbarSize = horizontalScrollbarSizeRef.current; + const prevVerticalScrollbarSize = verticalScrollbarSizeRef.current; + + if (scrollbarSize === 0) { + horizontalScrollbarSizeRef.current = 0; + verticalScrollbarSizeRef.current = 0; + } else { + if (!fixed || totalColumnsWidth <= width - scrollbarSize) { + horizontalScrollbarSizeRef.current = 0; + verticalScrollbarSizeRef.current = totalRowsHeight > _getBodyHeight() ? scrollbarSize : 0; + } else { + if (totalColumnsWidth > width) { + horizontalScrollbarSizeRef.current = scrollbarSize; + verticalScrollbarSizeRef.current = + totalRowsHeight > _getBodyHeight() - horizontalScrollbarSizeRef.current ? scrollbarSize : 0; + } else { + horizontalScrollbarSizeRef.current = 0; + verticalScrollbarSizeRef.current = 0; + if (totalRowsHeight > _getBodyHeight()) { + horizontalScrollbarSizeRef.current = scrollbarSize; + verticalScrollbarSizeRef.current = scrollbarSize; } } - }, - (newArgs: any, lastArgs: any) => isObjectEqual(newArgs, lastArgs, this.props.ignoreFunctionInColumnCompare), - ); + } + } - this._isResetting = false; - this._resetIndex = null; - this._rowHeightMap = {}; - this._rowHeightMapBuffer = {}; - this._mainRowHeightMap = {}; - this._leftRowHeightMap = {}; - this._rightRowHeightMap = {}; - this._getEstimatedTotalRowsHeight = memoize(getEstimatedTotalRowsHeight); - this._getRowHeight = this.__getRowHeight.bind(this); - this._updateRowHeights = debounce(() => { - this._isResetting = true; - this._rowHeightMap = { ...this._rowHeightMap, ...this._rowHeightMapBuffer }; - this.resetAfterRowIndex(this._resetIndex!, false); - this._rowHeightMapBuffer = {}; - this._resetIndex = null; - this.forceUpdateTable(); - this.forceUpdate(); - this._isResetting = false; - }, 0); - - this._scroll = { scrollLeft: 0, scrollTop: 0 }; - this._scrollHeight = 0; - this._lastScannedRowIndex = -1; - this._hasDataChangedSinceEndReached = true; - - this._data = props.data; - this._depthMap = {}; - - this._horizontalScrollbarSize = 0; - this._verticalScrollbarSize = 0; - this._scrollbarPresenceChanged = false; - this._totalRowsHeight = 0; - } + if ( + prevHorizontalScrollbarSize !== horizontalScrollbarSizeRef.current || + prevVerticalScrollbarSize !== verticalScrollbarSizeRef.current + ) { + scrollbarPresenceChangedRef.current = true; + } + }; - /** - * Get the DOM node of the table - */ - getDOMNode(): HTMLDivElement | null { - return this.tableNode; - } + const _maybeScrollbarPresenceChange = () => { + if (scrollbarPresenceChangedRef.current) { + scrollbarPresenceChangedRef.current = false; + onScrollbarPresenceChange({ + size: scrollbarSize, + horizontal: horizontalScrollbarSizeRef.current > 0, + vertical: verticalScrollbarSizeRef.current > 0, + }); + } + }; - /** - * Get the column manager - */ - getColumnManager(): ColumnManager { - return this.columnManager; - } + const _maybeCallOnEndReached = () => { + const { scrollTop } = scrollRef.current; + const scrollHeight = getTotalRowsHeight(); + const clientHeight = _getBodyHeight(); - /** - * Get internal `expandedRowKeys` state - */ - getExpandedRowKeys(): RowKey[] { - const { expandedRowKeys } = this.props; - return expandedRowKeys !== undefined ? expandedRowKeys || EMPTY_ARRAY : this.state.expandedRowKeys; - } + if (!onEndReached || !clientHeight || !scrollHeight) return; + const distanceFromEnd = scrollHeight - scrollTop - clientHeight + horizontalScrollbarSizeRef.current; + if ( + lastScannedRowIndexRef.current >= 0 && + distanceFromEnd <= onEndReachedThreshold && + (hasDataChangedSinceEndReachedRef.current || scrollHeight !== scrollHeightRef.current) + ) { + hasDataChangedSinceEndReachedRef.current = false; + scrollHeightRef.current = scrollHeight; + onEndReached({ distanceFromEnd }); + } + }; - /** - * Get the expanded state, fallback to normal state if not expandable. - */ - getExpandedState() { - return { - expandedData: this._data, - expandedRowKeys: this.getExpandedRowKeys(), - expandedDepthMap: this._depthMap, - }; - } + // Imperative scroll methods (used internally and exposed via ref) + const _forceUpdateTable = () => { + tableRef.current && tableRef.current.forceUpdateTable(); + leftTableRef.current && leftTableRef.current.forceUpdateTable(); + rightTableRef.current && rightTableRef.current.forceUpdateTable(); + }; - /** - * Get the total height of all rows, including expanded rows. - */ - getTotalRowsHeight(): number { - const { rowHeight, estimatedRowHeight } = this.props; + const _resetAfterRowIndex = (rowIndex: number = 0, shouldForceUpdate: boolean = true) => { + if (!estimatedRowHeightRef.current) return; + tableRef.current && tableRef.current.resetAfterRowIndex(rowIndex, shouldForceUpdate); + leftTableRef.current && leftTableRef.current.resetAfterRowIndex(rowIndex, shouldForceUpdate); + rightTableRef.current && rightTableRef.current.resetAfterRowIndex(rowIndex, shouldForceUpdate); + }; - if (estimatedRowHeight) { - return this.table - ? this.table.getTotalRowsHeight() - : this._getEstimatedTotalRowsHeight(this._data, estimatedRowHeight); - } - return this._data.length * rowHeight!; - } + const _scrollToPosition = (offset: { scrollLeft: number; scrollTop: number }) => { + scrollRef.current = offset; + tableRef.current && tableRef.current.scrollToPosition(offset); + leftTableRef.current && leftTableRef.current.scrollToTop(offset.scrollTop); + rightTableRef.current && rightTableRef.current.scrollToTop(offset.scrollTop); + }; - /** - * Get the total width of all columns. - */ - getTotalColumnsWidth(): number { - return this.columnManager.getColumnsWidth(); - } + const _scrollToTop = (scrollTop: number) => { + scrollRef.current.scrollTop = scrollTop; + tableRef.current && tableRef.current.scrollToPosition(scrollRef.current); + leftTableRef.current && leftTableRef.current.scrollToTop(scrollTop); + rightTableRef.current && rightTableRef.current.scrollToTop(scrollTop); + }; - /** - * Forcefully re-render the inner Grid component. - */ - forceUpdateTable() { - this.table && this.table.forceUpdateTable(); - this.leftTable && this.leftTable.forceUpdateTable(); - this.rightTable && this.rightTable.forceUpdateTable(); - } + // Stable callbacks (throttled/debounced — must have stable identity) + const _updateRowHeights = useMemo( + () => + debounce(() => { + isResettingRef.current = true; + rowHeightMapRef.current = { ...rowHeightMapRef.current, ...rowHeightMapBufferRef.current }; + _resetAfterRowIndex(resetIndexRef.current!, false); + rowHeightMapBufferRef.current = {}; + resetIndexRef.current = null; + _forceUpdateTable(); + forceRender(); + isResettingRef.current = false; + }, 0), + [], + ); + + const _handleColumnResize = useMemo( + () => + throttle((column: { key: string }, w: number) => { + columnManager.setColumnWidth(column.key, w); + setResizingWidth(w); + const col = columnManager.getColumn(column.key); + onColumnResizeRef.current({ column: col, width: w }); + }, RESIZE_THROTTLE_WAIT), + [columnManager], + ); + + // Stable callback for _getIsResetting (reads only from ref) + const _getIsResetting = useCallback((): boolean => isResettingRef.current, []); + + const _getRowHeight = (rowIndex: number): number => { + return ( + rowHeightMapRef.current[dataRef.current[rowIndex][rowKey as string]] || + callOrReturn(estimatedRowHeight!, { rowData: dataRef.current[rowIndex], rowIndex }) + ); + }; - /** - * Reset cached offsets for positioning after a specific rowIndex - */ - resetAfterRowIndex(rowIndex: number = 0, shouldForceUpdate: boolean = true) { - if (!this.props.estimatedRowHeight) return; + // Ref setters + const _setContainerRef = useCallback((r: HTMLDivElement | null) => { + tableNodeRef.current = r; + }, []); + const _setMainTableRef = useCallback((r: GridTableHandle | null) => { + tableRef.current = r; + }, []); + const _setLeftTableRef = useCallback((r: GridTableHandle | null) => { + leftTableRef.current = r; + }, []); + const _setRightTableRef = useCallback((r: GridTableHandle | null) => { + rightTableRef.current = r; + }, []); + + // Event handlers + const _handleScroll = (args: any) => { + const lastScrollTop = scrollRef.current.scrollTop; + _scrollToPosition(args); + onScroll(args); + if (args.scrollTop > lastScrollTop) _maybeCallOnEndReached(); + }; - this.table && this.table.resetAfterRowIndex(rowIndex, shouldForceUpdate); - this.leftTable && this.leftTable.resetAfterRowIndex(rowIndex, shouldForceUpdate); - this.rightTable && this.rightTable.resetAfterRowIndex(rowIndex, shouldForceUpdate); - } + const _handleVerticalScroll = ({ scrollTop }: { scrollTop: number }) => { + const lastScrollTop = scrollRef.current.scrollTop; + if (scrollTop !== lastScrollTop) _scrollToTop(scrollTop); + if (scrollTop > lastScrollTop) _maybeCallOnEndReached(); + }; - /** - * Reset row height cache - */ - resetRowHeightCache() { - if (!this.props.estimatedRowHeight) return; - - this._resetIndex = null; - this._rowHeightMapBuffer = {}; - this._rowHeightMap = {}; - this._mainRowHeightMap = {}; - this._leftRowHeightMap = {}; - this._rightRowHeightMap = {}; - } + const _handleRowsRendered = (args: RowsRenderedArgs) => { + onRowsRendered(args); + if (args.overscanStopIndex > lastScannedRowIndexRef.current) { + lastScannedRowIndexRef.current = args.overscanStopIndex; + _maybeCallOnEndReached(); + } + }; - /** - * Scroll to the specified offset. - */ - scrollToPosition(offset: { scrollLeft: number; scrollTop: number }) { - this._scroll = offset; + const _handleRowHover = ({ hovered, rowKey: rk }: { hovered: boolean; rowKey: RowKey }) => { + setHoveredRowKey(hovered ? rk : null); + }; - this.table && this.table.scrollToPosition(offset); - this.leftTable && this.leftTable.scrollToTop(offset.scrollTop); - this.rightTable && this.rightTable.scrollToTop(offset.scrollTop); - } + const _handleRowExpand = ({ + expanded, + rowData, + rowIndex, + rowKey: rk, + }: { + expanded: boolean; + rowData: RowData; + rowIndex: number; + rowKey: RowKey; + }) => { + const keys = cloneArray(getExpandedRowKeys()); + if (expanded) { + if (keys.indexOf(rk) < 0) keys.push(rk); + } else { + const index = keys.indexOf(rk); + if (index > -1) keys.splice(index, 1); + } + if (expandedRowKeysProp === undefined) { + setExpandedRowKeysState(keys); + } + onRowExpand({ expanded, rowData, rowIndex, rowKey: rk }); + onExpandedRowsChange(keys); + }; - /** - * Scroll to the specified offset vertically. - */ - scrollToTop(scrollTop: number) { - this._scroll.scrollTop = scrollTop; + const _handleColumnResizeStart = ({ key }: { key: string }) => { + setResizingKey(key); + }; - this.table && this.table.scrollToPosition(this._scroll); - this.leftTable && this.leftTable.scrollToTop(scrollTop); - this.rightTable && this.rightTable.scrollToTop(scrollTop); - } + const _handleColumnResizeStop = () => { + const rk = resizingKey; + const rw = resizingWidth; + setResizingKey(null); + setResizingWidth(0); + if (!rk || !rw) return; + const column = columnManager.getColumn(rk); + onColumnResizeEnd({ column, width: rw }); + }; - /** - * Scroll to the specified offset horizontally. - */ - scrollToLeft(scrollLeft: number) { - this._scroll.scrollLeft = scrollLeft; + const _handleColumnSort = (event: React.MouseEvent) => { + const key = (event.currentTarget as HTMLElement).dataset.key!; + let order: string = SortOrder.ASC; - this.table && this.table.scrollToPosition(this._scroll); - } + if (sortState) { + order = sortState[key] === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC; + } else if (key === sortBy.key) { + order = sortBy.order === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC; + } - /** - * Scroll to the specified row. - */ - scrollToRow(rowIndex: number = 0, align: string = 'auto') { - this.table && this.table.scrollToRow(rowIndex, align); - this.leftTable && this.leftTable.scrollToRow(rowIndex, align); - this.rightTable && this.rightTable.scrollToRow(rowIndex, align); - } + const column = columnManager.getColumn(key); + onColumnSort({ column, key, order }); + }; - /** - * Set `expandedRowKeys` manually. - */ - setExpandedRowKeys(expandedRowKeys: RowKey[]) { - if (this.props.expandedRowKeys !== undefined) return; + const _handleFrozenRowHeightChange = (rk: RowKey, size: number, rowIndex: number, frozen: any) => { + if (!frozen) { + mainRowHeightMapRef.current[rk] = size; + } else if (frozen === FrozenDirection.RIGHT) { + rightRowHeightMapRef.current[rk] = size; + } else { + leftRowHeightMapRef.current[rk] = size; + } - this.setState({ - expandedRowKeys: cloneArray(expandedRowKeys), - }); - } + const h = Math.max( + mainRowHeightMapRef.current[rk] || 0, + leftRowHeightMapRef.current[rk] || 0, + rightRowHeightMapRef.current[rk] || 0, + ); - renderExpandIcon({ + if (rowHeightMapRef.current[rk] !== h) { + _handleRowHeightChange(rk, h, rowIndex); + } + }; + + const _handleRowHeightChange = (rk: RowKey, size: number, rowIndex: number) => { + if (resetIndexRef.current === null) resetIndexRef.current = rowIndex; + else if (resetIndexRef.current > rowIndex) resetIndexRef.current = rowIndex; + + rowHeightMapBufferRef.current[rk] = size; + _updateRowHeights(); + }; + + // Render methods + const renderExpandIcon = ({ rowData, rowIndex, depth, @@ -716,177 +658,177 @@ class BaseTable extends React.PureComponent { rowIndex: number; depth: number; onExpand: (expanded: boolean) => void; - }) { - const { rowKey, expandColumnKey, expandIconProps } = this.props; + }) => { if (!expandColumnKey) return null; const expandable = rowIndex >= 0 && hasChildren(rowData); - const expanded = rowIndex >= 0 && this.getExpandedRowKeys().indexOf(rowData[rowKey as string]) >= 0; + const expanded = rowIndex >= 0 && getExpandedRowKeys().indexOf(rowData[rowKey as string]) >= 0; const extraProps = callOrReturn(expandIconProps, { rowData, rowIndex, depth, expandable, expanded }); - const ExpandIconComp = this._getComponent('ExpandIcon'); + const ExpandIconComp = _getComponent('ExpandIcon'); return ( ); - } + }; - renderRow({ + const renderRow = ({ isScrolling, - columns, + columns: cols, rowData, rowIndex, - style, + style: rowStyle, }: { isScrolling: boolean; columns: ColumnShape[]; rowData: RowData; rowIndex: number; style: React.CSSProperties; - }) { - const { rowClassName, rowRenderer, rowEventHandlers, expandColumnKey, estimatedRowHeight } = this.props; - - const rowClass = callOrReturn(rowClassName, { columns, rowData, rowIndex }); - const extraProps = callOrReturn(this.props.rowProps, { columns, rowData, rowIndex }); - const rowKey = rowData[this.props.rowKey as string]; - const depth = this._depthMap[rowKey] || 0; - - const className = cn(this._prefixClass('row'), rowClass, { - [this._prefixClass(`row--depth-${depth}`)]: !!expandColumnKey && rowIndex >= 0, - [this._prefixClass('row--expanded')]: !!expandColumnKey && this.getExpandedRowKeys().indexOf(rowKey) >= 0, - [this._prefixClass('row--hovered')]: !isScrolling && rowKey === this.state.hoveredRowKey, - [this._prefixClass('row--frozen')]: depth === 0 && rowIndex < 0, - [this._prefixClass('row--customized')]: rowRenderer, + }) => { + const rowClass = callOrReturn(rowClassName, { columns: cols, rowData, rowIndex }); + const extraProps = callOrReturn(rowPropsProp, { columns: cols, rowData, rowIndex }); + const rk = rowData[rowKey as string]; + const depth = depthMapRef.current[rk] || 0; + + const cls = cn(_prefixClass('row'), rowClass, { + [_prefixClass(`row--depth-${depth}`)]: !!expandColumnKey && rowIndex >= 0, + [_prefixClass('row--expanded')]: !!expandColumnKey && getExpandedRowKeys().indexOf(rk) >= 0, + [_prefixClass('row--hovered')]: !isScrolling && rk === hoveredRowKey, + [_prefixClass('row--frozen')]: depth === 0 && rowIndex < 0, + [_prefixClass('row--customized')]: rowRenderer, }); - const hasFrozenColumns = this.columnManager.hasFrozenColumns(); + const hasFrozenColumns = columnManager.hasFrozenColumns(); const rowProps = { ...extraProps, role: 'row', - key: `row-${rowKey}`, + key: `row-${rk}`, isScrolling, - className, - style, - columns, + className: cls, + style: rowStyle, + columns: cols, rowIndex, rowData, - rowKey, + rowKey: rk, expandColumnKey, depth, rowEventHandlers, rowRenderer, estimatedRowHeight: rowIndex >= 0 ? estimatedRowHeight : undefined, - getIsResetting: this._getIsResetting, - cellRenderer: this.renderRowCell, - expandIconRenderer: this.renderExpandIcon, - onRowExpand: this._handleRowExpand, - onRowHover: hasFrozenColumns ? this._handleRowHover : undefined, - onRowHeightChange: hasFrozenColumns ? this._handleFrozenRowHeightChange : this._handleRowHeightChange, + getIsResetting: _getIsResetting, + cellRenderer: renderRowCell, + expandIconRenderer: renderExpandIcon, + onRowExpand: _handleRowExpand, + onRowHover: hasFrozenColumns ? _handleRowHover : undefined, + onRowHeightChange: hasFrozenColumns ? _handleFrozenRowHeightChange : _handleRowHeightChange, }; return ; - } + }; - renderRowCell({ isScrolling, columns, column, columnIndex, rowData, rowIndex, expandIcon }: any) { + const renderRowCell = ({ isScrolling, columns: cols, column, columnIndex, rowData, rowIndex, expandIcon }: any) => { if (column[ColumnManager.PlaceholderKey]) { return (
); } - const { className, dataKey, dataGetter, cellRenderer } = column; - const TableCellComp = this._getComponent('TableCell'); + const { className: colClassName, dataKey, dataGetter, cellRenderer } = column; + const TableCellComp = _getComponent('TableCell'); const cellData = dataGetter - ? dataGetter({ columns, column, columnIndex, rowData, rowIndex }) + ? dataGetter({ columns: cols, column, columnIndex, rowData, rowIndex }) : getValue(rowData, dataKey); - const cellProps = { isScrolling, cellData, columns, column, columnIndex, rowData, rowIndex, container: this }; - const cell = renderElement( - cellRenderer || , - cellProps, - ); + const cellProps = { + isScrolling, + cellData, + columns: cols, + column, + columnIndex, + rowData, + rowIndex, + container: containerRef, + }; + const cell = renderElement(cellRenderer || , cellProps); - const cellCls = callOrReturn(className, { cellData, columns, column, columnIndex, rowData, rowIndex }); - const cls = cn(this._prefixClass('row-cell'), cellCls, { - [this._prefixClass('row-cell--align-center')]: column.align === Alignment.CENTER, - [this._prefixClass('row-cell--align-right')]: column.align === Alignment.RIGHT, + const cellCls = callOrReturn(colClassName, { cellData, columns: cols, column, columnIndex, rowData, rowIndex }); + const cls = cn(_prefixClass('row-cell'), cellCls, { + [_prefixClass('row-cell--align-center')]: column.align === Alignment.CENTER, + [_prefixClass('row-cell--align-right')]: column.align === Alignment.RIGHT, }); - const extraProps = callOrReturn(this.props.cellProps, { columns, column, columnIndex, rowData, rowIndex }); + const extraProps = callOrReturn(cellPropsProp, { columns: cols, column, columnIndex, rowData, rowIndex }); const { tagName, ...rest } = extraProps || {}; const Tag = tagName || 'div'; return ( {expandIcon} {cell} ); - } + }; - renderHeader({ - columns, + const renderHeader = ({ + columns: cols, headerIndex, - style, + style: headerStyle, }: { columns: ColumnShape[]; headerIndex: number; style: React.CSSProperties; - }) { - const { headerClassName, headerRenderer } = this.props; + }) => { + const headerClass = callOrReturn(headerClassName, { columns: cols, headerIndex }); + const extraProps = callOrReturn(headerPropsProp, { columns: cols, headerIndex }); - const headerClass = callOrReturn(headerClassName, { columns, headerIndex }); - const extraProps = callOrReturn(this.props.headerProps, { columns, headerIndex }); - - const className = cn(this._prefixClass('header-row'), headerClass, { - [this._prefixClass('header-row--resizing')]: !!this.state.resizingKey, - [this._prefixClass('header-row--customized')]: headerRenderer, + const cls = cn(_prefixClass('header-row'), headerClass, { + [_prefixClass('header-row--resizing')]: !!resizingKey, + [_prefixClass('header-row--customized')]: headerRenderer, }); - const headerProps = { + const hProps = { ...extraProps, role: 'row', key: `header-${headerIndex}`, - className, - style, - columns, + className: cls, + style: headerStyle, + columns: cols, headerIndex, headerRenderer, - cellRenderer: this.renderHeaderCell, - expandColumnKey: this.props.expandColumnKey, - expandIcon: this._getComponent('ExpandIcon'), + cellRenderer: renderHeaderCell, + expandColumnKey, + expandIcon: _getComponent('ExpandIcon'), }; - const TableHeaderRowComp = this._getComponent('TableHeaderRow'); - return ; - } + const TableHeaderRowComp = _getComponent('TableHeaderRow'); + return ; + }; - renderHeaderCell({ columns, column, columnIndex, headerIndex, expandIcon }: any) { + const renderHeaderCell = ({ columns: cols, column, columnIndex, headerIndex, expandIcon }: any) => { if (column[ColumnManager.PlaceholderKey]) { return (
); } - const { headerClassName, headerRenderer } = column; - const { sortBy, sortState, headerCellProps } = this.props; - const TableHeaderCellComp = this._getComponent('TableHeaderCell'); - const SortIndicatorComp = this._getComponent('SortIndicator'); + const { headerClassName: colHeaderClassName, headerRenderer: colHeaderRenderer } = column; + const TableHeaderCellComp = _getComponent('TableHeaderCell'); + const SortIndicatorComp = _getComponent('SortIndicator'); - const cellProps = { columns, column, columnIndex, headerIndex, container: this }; + const cellProps = { columns: cols, column, columnIndex, headerIndex, container: containerRef }; const cell = renderElement( - headerRenderer || , + colHeaderRenderer || , cellProps, ); @@ -897,29 +839,29 @@ class BaseTable extends React.PureComponent { sorting = order === SortOrder.ASC || order === SortOrder.DESC; sortOrder = sorting ? order : SortOrder.ASC; } else { - sorting = column.key === sortBy!.key; - sortOrder = sorting ? sortBy!.order! : SortOrder.ASC; + sorting = column.key === sortBy.key; + sortOrder = sorting ? sortBy.order! : SortOrder.ASC; } - const cellCls = callOrReturn(headerClassName, { columns, column, columnIndex, headerIndex }); - const cls = cn(this._prefixClass('header-cell'), cellCls, { - [this._prefixClass('header-cell--align-center')]: column.align === Alignment.CENTER, - [this._prefixClass('header-cell--align-right')]: column.align === Alignment.RIGHT, - [this._prefixClass('header-cell--sortable')]: column.sortable, - [this._prefixClass('header-cell--sorting')]: sorting, - [this._prefixClass('header-cell--resizing')]: column.key === this.state.resizingKey, + const cellCls = callOrReturn(colHeaderClassName, { columns: cols, column, columnIndex, headerIndex }); + const cls = cn(_prefixClass('header-cell'), cellCls, { + [_prefixClass('header-cell--align-center')]: column.align === Alignment.CENTER, + [_prefixClass('header-cell--align-right')]: column.align === Alignment.RIGHT, + [_prefixClass('header-cell--sortable')]: column.sortable, + [_prefixClass('header-cell--sorting')]: sorting, + [_prefixClass('header-cell--resizing')]: column.key === resizingKey, }); - const extraProps = callOrReturn(headerCellProps, { columns, column, columnIndex, headerIndex }); + const extraProps = callOrReturn(headerCellPropsProp, { columns: cols, column, columnIndex, headerIndex }); const { tagName, ...rest } = extraProps || {}; const Tag = tagName || 'div'; return ( {expandIcon} @@ -928,534 +870,582 @@ class BaseTable extends React.PureComponent { )} {column.resizable && ( )} ); - } + }; - renderMainTable() { - const { width, headerHeight, rowHeight, fixed, estimatedRowHeight, ...rest } = this.props; - const height = this._getTableHeight(); + const renderMainTable = () => { + const tableHeight = _getTableHeight(); - let tableWidth = width - this._verticalScrollbarSize; + let tableWidth = width - verticalScrollbarSizeRef.current; if (fixed) { - const columnsWidth = this.columnManager.getColumnsWidth(); + const columnsWidth = columnManager.getColumnsWidth(); tableWidth = Math.max(Math.round(columnsWidth), tableWidth); } return ( ); - } - - renderLeftTable() { - if (!this.columnManager.hasLeftFrozenColumns()) return null; + }; - const { width, headerHeight, rowHeight, estimatedRowHeight, ...rest } = this.props; + const renderLeftTable = () => { + if (!columnManager.hasLeftFrozenColumns()) return null; - const containerHeight = this._getFrozenContainerHeight(); - const offset = this._verticalScrollbarSize || 20; - const columnsWidth = this.columnManager.getLeftFrozenColumnsWidth(); + const containerHeight = _getFrozenContainerHeight(); + const offset = verticalScrollbarSizeRef.current || 20; + const columnsWidth = columnManager.getLeftFrozenColumnsWidth(); return ( ); - } - - renderRightTable() { - if (!this.columnManager.hasRightFrozenColumns()) return null; + }; - const { width, headerHeight, rowHeight, estimatedRowHeight, ...rest } = this.props; + const renderRightTable = () => { + if (!columnManager.hasRightFrozenColumns()) return null; - const containerHeight = this._getFrozenContainerHeight(); - const columnsWidth = this.columnManager.getRightFrozenColumnsWidth(); - const scrollbarWidth = this._verticalScrollbarSize; + const containerHeight = _getFrozenContainerHeight(); + const columnsWidth = columnManager.getRightFrozenColumnsWidth(); + const scrollbarWidth = verticalScrollbarSizeRef.current; return ( ); - } + }; - renderResizingLine() { - const { width, fixed } = this.props; - const { resizingKey } = this.state; + const renderResizingLine = () => { if (!fixed || !resizingKey) return null; - const columns = this.columnManager.getMainColumns(); - const idx = columns.findIndex((column) => column.key === resizingKey); - const column = columns[idx]; + const cols = columnManager.getMainColumns(); + const idx = cols.findIndex((column) => column.key === resizingKey); + const column = cols[idx]; const { width: columnWidth, frozen } = column; - const leftWidth = this.columnManager.recomputeColumnsWidth(columns.slice(0, idx)); + const leftWidth = columnManager.recomputeColumnsWidth(cols.slice(0, idx)); let left = leftWidth + columnWidth; if (!frozen) { - left -= this._scroll.scrollLeft; + left -= scrollRef.current.scrollLeft; } else if (frozen === FrozenDirection.RIGHT) { - const rightWidth = this.columnManager.recomputeColumnsWidth(columns.slice(idx + 1)); - if (rightWidth + columnWidth > width - this._verticalScrollbarSize) { + const rightWidth = columnManager.recomputeColumnsWidth(cols.slice(idx + 1)); + if (rightWidth + columnWidth > width - verticalScrollbarSizeRef.current) { left = columnWidth; } else { - left = width - this._verticalScrollbarSize - rightWidth; + left = width - verticalScrollbarSizeRef.current - rightWidth; } } - const style = { + const lineStyle = { left, - height: this._getTableHeight() - this._horizontalScrollbarSize, + height: _getTableHeight() - horizontalScrollbarSizeRef.current, }; - return
; - } + return
; + }; - renderFooter() { - const { footerHeight, footerRenderer } = this.props; + const renderFooter = () => { if (footerHeight === 0) return null; return ( -
+
{renderElement(footerRenderer)}
); - } - - renderEmptyLayer() { - const { data, frozenData, footerHeight, emptyRenderer } = this.props; + }; + const renderEmptyLayer = () => { if ((data && data.length) || (frozenData && frozenData.length)) return null; - const headerHeight = this._getHeaderHeight(); + const hHeight = _getHeaderHeight(); return ( -
+
{renderElement(emptyRenderer)}
); - } - - renderOverlay() { - const { overlayRenderer } = this.props; + }; - return
{!!overlayRenderer && renderElement(overlayRenderer)}
; - } + const renderOverlay = () => { + return
{!!overlayRenderer && renderElement(overlayRenderer)}
; + }; - render() { - const { - columns, - children, - width, - fixed, - data, - frozenData, - expandColumnKey, - disabled, - className, - style, - footerHeight, - classPrefix, - estimatedRowHeight, - } = this.props; - this._resetColumnManager(getColumns(columns, children), fixed!); - - const _data = expandColumnKey ? this._flattenOnKeys(data, this.getExpandedRowKeys(), this.props.rowKey) : data; - if (this._data !== _data) { - this.resetAfterRowIndex(0, false); - this._data = _data; - } - this._calcScrollbarSizes(); - this._totalRowsHeight = this.getTotalRowsHeight(); - - const containerStyle: React.CSSProperties = { - ...style, - width, - height: this._getTableHeight() + footerHeight!, - position: 'relative', + // Container ref for imperative handle (passed as `container` to cell renderers) + const containerRef = useRef(null); + + // useImperativeHandle + useImperativeHandle(ref, () => { + const handle: BaseTableHandle = { + getDOMNode: () => tableNodeRef.current, + getColumnManager: () => columnManager, + getExpandedRowKeys, + getExpandedState: () => ({ + expandedData: dataRef.current, + expandedRowKeys: getExpandedRowKeys(), + expandedDepthMap: depthMapRef.current, + }), + getTotalRowsHeight, + getTotalColumnsWidth: () => columnManager.getColumnsWidth(), + forceUpdateTable: _forceUpdateTable, + resetAfterRowIndex: _resetAfterRowIndex, + resetRowHeightCache: () => { + if (!estimatedRowHeight) return; + resetIndexRef.current = null; + rowHeightMapBufferRef.current = {}; + rowHeightMapRef.current = {}; + mainRowHeightMapRef.current = {}; + leftRowHeightMapRef.current = {}; + rightRowHeightMapRef.current = {}; + }, + scrollToPosition: _scrollToPosition, + scrollToTop: _scrollToTop, + scrollToLeft: (scrollLeft: number) => { + scrollRef.current.scrollLeft = scrollLeft; + tableRef.current && tableRef.current.scrollToPosition(scrollRef.current); + }, + scrollToRow: (rowIndex: number = 0, align: string = 'auto') => { + tableRef.current && tableRef.current.scrollToRow(rowIndex, align); + leftTableRef.current && leftTableRef.current.scrollToRow(rowIndex, align); + rightTableRef.current && rightTableRef.current.scrollToRow(rowIndex, align); + }, + setExpandedRowKeys: (keys: RowKey[]) => { + if (expandedRowKeysProp !== undefined) return; + setExpandedRowKeysState(cloneArray(keys)); + }, }; - const cls = cn(classPrefix, className, { - [`${classPrefix}--fixed`]: fixed, - [`${classPrefix}--expandable`]: !!expandColumnKey, - [`${classPrefix}--empty`]: data.length === 0, - [`${classPrefix}--has-frozen-rows`]: frozenData!.length > 0, - [`${classPrefix}--has-frozen-columns`]: this.columnManager.hasFrozenColumns(), - [`${classPrefix}--disabled`]: disabled, - [`${classPrefix}--dynamic`]: !!estimatedRowHeight, - }); - return ( -
- {this.renderFooter()} - {this.renderMainTable()} - {this.renderLeftTable()} - {this.renderRightTable()} - {this.renderResizingLine()} - {this.renderEmptyLayer()} - {this.renderOverlay()} -
- ); - } - - componentDidMount() { - const scrollbarSize = this.props.getScrollbarSize!(); - if (scrollbarSize > 0) { - this.setState({ scrollbarSize }); - } - } - - componentDidUpdate(prevProps: BaseTableProps, prevState: BaseTableState) { - const { data, height, maxHeight, estimatedRowHeight } = this.props; - if (data !== prevProps.data) { - this._lastScannedRowIndex = -1; - this._hasDataChangedSinceEndReached = true; - } - - if (maxHeight !== prevProps.maxHeight || height !== prevProps.height) { - this._maybeCallOnEndReached(); - } - this._maybeScrollbarPresenceChange(); - - if (estimatedRowHeight) { - if (this.getTotalRowsHeight() !== this._totalRowsHeight) { - this.forceUpdate(); - } - } - } - - _prefixClass(className: string): string { - return `${this.props.classPrefix}__${className}`; - } - - _setContainerRef(ref: HTMLDivElement | null) { - this.tableNode = ref; - } - - _setMainTableRef(ref: GridTableHandle | null) { - this.table = ref; - } - - _setLeftTableRef(ref: GridTableHandle | null) { - this.leftTable = ref; - } - - _setRightTableRef(ref: GridTableHandle | null) { - this.rightTable = ref; - } - - _getComponent(name: keyof TableComponents): React.ComponentType { - if (this.props.components && this.props.components[name]) - return this.props.components[name] as React.ComponentType; - return DEFAULT_COMPONENTS[name]; - } - - __getRowHeight(rowIndex: number): number { - const { estimatedRowHeight, rowKey } = this.props; - return ( - this._rowHeightMap[this._data[rowIndex][rowKey as string]] || - callOrReturn(estimatedRowHeight!, { rowData: this._data[rowIndex], rowIndex }) - ); - } - - _getIsResetting(): boolean { - return this._isResetting; - } - - _getHeaderHeight(): number { - const { headerHeight } = this.props; - if (Array.isArray(headerHeight)) { - return headerHeight.reduce((sum, height) => sum + height, 0); + containerRef.current = handle; + return handle; + }); + + // Lifecycle: componentDidMount + useEffect(() => { + const size = getScrollbarSize(); + if (size > 0) { + setScrollbarSize(size); } - return headerHeight; - } - - _getFrozenRowsHeight(): number { - const { frozenData, rowHeight } = this.props; - return frozenData!.length * rowHeight!; - } - - _getTableHeight(): number { - const { height, maxHeight, footerHeight } = this.props; - let tableHeight = height! - footerHeight!; - - if (maxHeight! > 0) { - const frozenRowsHeight = this._getFrozenRowsHeight(); - const totalRowsHeight = this.getTotalRowsHeight(); - const headerHeight = this._getHeaderHeight(); - const totalHeight = headerHeight + frozenRowsHeight + totalRowsHeight + this._horizontalScrollbarSize; - tableHeight = Math.min(totalHeight, maxHeight! - footerHeight!); + }, []); + + // Lifecycle: componentDidUpdate — track data changes + const prevDataRef = useRef(data); + const prevHeightRef = useRef(height); + const prevMaxHeightRef = useRef(maxHeight); + useEffect(() => { + if (data !== prevDataRef.current) { + lastScannedRowIndexRef.current = -1; + hasDataChangedSinceEndReachedRef.current = true; } + prevDataRef.current = data; - return tableHeight; - } - - _getBodyHeight(): number { - return this._getTableHeight() - this._getHeaderHeight() - this._getFrozenRowsHeight(); - } - - _getFrozenContainerHeight(): number { - const { maxHeight } = this.props; - - const tableHeight = this._getTableHeight() - (this._data.length > 0 ? this._horizontalScrollbarSize : 0); - if (maxHeight! > 0) return tableHeight; - - const totalHeight = this.getTotalRowsHeight() + this._getHeaderHeight() + this._getFrozenRowsHeight(); - return Math.min(tableHeight, totalHeight); - } - - _calcScrollbarSizes() { - const { fixed, width } = this.props; - const { scrollbarSize } = this.state; - - const totalRowsHeight = this.getTotalRowsHeight(); - const totalColumnsWidth = this.getTotalColumnsWidth(); - - const prevHorizontalScrollbarSize = this._horizontalScrollbarSize; - const prevVerticalScrollbarSize = this._verticalScrollbarSize; - - if (scrollbarSize === 0) { - this._horizontalScrollbarSize = 0; - this._verticalScrollbarSize = 0; - } else { - if (!fixed || totalColumnsWidth <= width - scrollbarSize) { - this._horizontalScrollbarSize = 0; - this._verticalScrollbarSize = totalRowsHeight > this._getBodyHeight() ? scrollbarSize : 0; - } else { - if (totalColumnsWidth > width) { - this._horizontalScrollbarSize = scrollbarSize; - this._verticalScrollbarSize = - totalRowsHeight > this._getBodyHeight() - this._horizontalScrollbarSize ? scrollbarSize : 0; - } else { - this._horizontalScrollbarSize = 0; - this._verticalScrollbarSize = 0; - if (totalRowsHeight > this._getBodyHeight()) { - this._horizontalScrollbarSize = scrollbarSize; - this._verticalScrollbarSize = scrollbarSize; - } - } - } - } - - if ( - prevHorizontalScrollbarSize !== this._horizontalScrollbarSize || - prevVerticalScrollbarSize !== this._verticalScrollbarSize - ) { - this._scrollbarPresenceChanged = true; - } - } - - _maybeScrollbarPresenceChange() { - if (this._scrollbarPresenceChanged) { - const { onScrollbarPresenceChange } = this.props; - this._scrollbarPresenceChanged = false; - - onScrollbarPresenceChange!({ - size: this.state.scrollbarSize, - horizontal: this._horizontalScrollbarSize > 0, - vertical: this._verticalScrollbarSize > 0, - }); - } - } - - _maybeCallOnEndReached() { - const { onEndReached, onEndReachedThreshold } = this.props; - const { scrollTop } = this._scroll; - const scrollHeight = this.getTotalRowsHeight(); - const clientHeight = this._getBodyHeight(); - - if (!onEndReached || !clientHeight || !scrollHeight) return; - const distanceFromEnd = scrollHeight - scrollTop - clientHeight + this._horizontalScrollbarSize; - if ( - this._lastScannedRowIndex >= 0 && - distanceFromEnd <= onEndReachedThreshold! && - (this._hasDataChangedSinceEndReached || scrollHeight !== this._scrollHeight) - ) { - this._hasDataChangedSinceEndReached = false; - this._scrollHeight = scrollHeight; - onEndReached({ distanceFromEnd }); - } - } - - _handleScroll(args: any) { - const lastScrollTop = this._scroll.scrollTop; - this.scrollToPosition(args); - this.props.onScroll!(args); - - if (args.scrollTop > lastScrollTop) this._maybeCallOnEndReached(); - } - - _handleVerticalScroll({ scrollTop }: { scrollTop: number }) { - const lastScrollTop = this._scroll.scrollTop; - - if (scrollTop !== lastScrollTop) this.scrollToTop(scrollTop); - if (scrollTop > lastScrollTop) this._maybeCallOnEndReached(); - } - - _handleRowsRendered(args: RowsRenderedArgs) { - this.props.onRowsRendered!(args); - - if (args.overscanStopIndex > this._lastScannedRowIndex) { - this._lastScannedRowIndex = args.overscanStopIndex; - this._maybeCallOnEndReached(); + if (maxHeight !== prevMaxHeightRef.current || height !== prevHeightRef.current) { + _maybeCallOnEndReached(); } - } + prevHeightRef.current = height; + prevMaxHeightRef.current = maxHeight; - _handleRowHover({ hovered, rowKey }: { hovered: boolean; rowKey: RowKey }) { - this.setState({ hoveredRowKey: hovered ? rowKey : null }); - } + _maybeScrollbarPresenceChange(); - _handleRowExpand({ - expanded, - rowData, - rowIndex, - rowKey, - }: { - expanded: boolean; - rowData: RowData; - rowIndex: number; - rowKey: RowKey; - }) { - const expandedRowKeys = cloneArray(this.getExpandedRowKeys()); - if (expanded) { - if (expandedRowKeys.indexOf(rowKey) < 0) expandedRowKeys.push(rowKey); - } else { - const index = expandedRowKeys.indexOf(rowKey); - if (index > -1) { - expandedRowKeys.splice(index, 1); + if (estimatedRowHeight) { + if (getTotalRowsHeight() !== totalRowsHeightRef.current) { + forceRender(); } } - if (this.props.expandedRowKeys === undefined) { - this.setState({ expandedRowKeys }); - } - this.props.onRowExpand!({ expanded, rowData, rowIndex, rowKey }); - this.props.onExpandedRowsChange!(expandedRowKeys); - } + }); - _handleColumnResize({ key }: { key: string }, width: number) { - this.columnManager.setColumnWidth(key, width); - this.setState({ resizingWidth: width }); + // === Render logic (equivalent to class render() method) === - const column = this.columnManager.getColumn(key); - this.props.onColumnResize!({ column, width }); - } + _resetColumnManager(getColumns(columns, children), fixed); - _handleColumnResizeStart({ key }: { key: string }) { - this.setState({ resizingKey: key }); + const _data = expandColumnKey ? _flattenOnKeys(data, getExpandedRowKeys(), rowKey) : data; + if (dataRef.current !== _data) { + _resetAfterRowIndex(0, false); + dataRef.current = _data; } + _calcScrollbarSizes(); + totalRowsHeightRef.current = getTotalRowsHeight(); + + const containerStyle: React.CSSProperties = { + ...style, + width, + height: _getTableHeight() + footerHeight, + position: 'relative', + }; + const cls = cn(classPrefix, className, { + [`${classPrefix}--fixed`]: fixed, + [`${classPrefix}--expandable`]: !!expandColumnKey, + [`${classPrefix}--empty`]: data.length === 0, + [`${classPrefix}--has-frozen-rows`]: frozenData.length > 0, + [`${classPrefix}--has-frozen-columns`]: columnManager.hasFrozenColumns(), + [`${classPrefix}--disabled`]: disabled, + [`${classPrefix}--dynamic`]: !!estimatedRowHeight, + }); + + return ( +
+ {renderFooter()} + {renderMainTable()} + {renderLeftTable()} + {renderRightTable()} + {renderResizingLine()} + {renderEmptyLayer()} + {renderOverlay()} +
+ ); +}); - _handleColumnResizeStop() { - const { resizingKey, resizingWidth } = this.state; - this.setState({ resizingKey: null, resizingWidth: 0 }); - - if (!resizingKey || !resizingWidth) return; - - const column = this.columnManager.getColumn(resizingKey); - this.props.onColumnResizeEnd!({ column, width: resizingWidth }); - } - - _handleColumnSort(event: React.MouseEvent) { - const key = (event.currentTarget as HTMLElement).dataset.key!; - const { sortBy, sortState, onColumnSort } = this.props; - let order: string = SortOrder.ASC; - - if (sortState) { - order = sortState[key] === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC; - } else if (key === sortBy!.key) { - order = sortBy!.order === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC; - } - - const column = this.columnManager.getColumn(key); - onColumnSort!({ column, key, order }); - } - - _handleFrozenRowHeightChange(rowKey: RowKey, size: number, rowIndex: number, frozen: any) { - if (!frozen) { - this._mainRowHeightMap[rowKey] = size; - } else if (frozen === FrozenDirection.RIGHT) { - this._rightRowHeightMap[rowKey] = size; - } else { - this._leftRowHeightMap[rowKey] = size; - } - - const height = Math.max( - this._mainRowHeightMap[rowKey] || 0, - this._leftRowHeightMap[rowKey] || 0, - this._rightRowHeightMap[rowKey] || 0, - ); - - if (this._rowHeightMap[rowKey] !== height) { - this._handleRowHeightChange(rowKey, height, rowIndex); - } - } +InnerBaseTable.propTypes = { + /** + * Prefix for table's inner className + */ + classPrefix: PropTypes.string, + /** + * Class name for the table + */ + className: PropTypes.string, + /** + * Custom style for the table + */ + style: PropTypes.object, + /** + * A collection of Column + */ + children: PropTypes.node, + /** + * Columns for the table + */ + columns: PropTypes.arrayOf(PropTypes.shape(Column.propTypes as any)), + /** + * The data for the table + */ + data: PropTypes.array.isRequired, + /** + * The data be frozen to top, `rowIndex` is negative and started from `-1` + */ + frozenData: PropTypes.array, + /** + * The key field of each data item + */ + rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + /** + * The width of the table + */ + width: PropTypes.number.isRequired, + /** + * The height of the table, will be ignored if `maxHeight` is set + */ + height: PropTypes.number, + /** + * The max height of the table, the table's height will auto change when data changes, + * will turns to vertical scroll if reaches the max height + */ + maxHeight: PropTypes.number, + /** + * The height of each table row, will be only used by frozen rows if `estimatedRowHeight` is set + */ + rowHeight: PropTypes.number, + /** + * Estimated row height, the real height will be measure dynamically according to the content + * The callback is of the shape of `({ rowData, rowIndex }) => number` + */ + estimatedRowHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), + /** + * The height of the table header, set to 0 to hide the header, could be an array to render multi headers. + */ + headerHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.arrayOf(PropTypes.number)]).isRequired, + /** + * The height of the table footer + */ + footerHeight: PropTypes.number, + /** + * Whether the width of the columns are fixed or flexible + */ + fixed: PropTypes.bool, + /** + * Whether the table is disabled + */ + disabled: PropTypes.bool, + /** + * Custom renderer on top of the table component + */ + overlayRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), + /** + * Custom renderer when the length of data is 0 + */ + emptyRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), + /** + * Custom footer renderer, available only if `footerHeight` is larger then 0 + */ + footerRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), + /** + * Custom header renderer + * The renderer receives props `{ cells, columns, headerIndex }` + */ + headerRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), + /** + * Custom row renderer + * The renderer receives props `{ isScrolling, cells, columns, rowData, rowIndex, depth }` + */ + rowRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), + /** + * Class name for the table header, could be a callback to return the class name + * The callback is of the shape of `({ columns, headerIndex }) => string` + */ + headerClassName: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), + /** + * Class name for the table row, could be a callback to return the class name + * The callback is of the shape of `({ columns, rowData, rowIndex }) => string` + */ + rowClassName: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), + /** + * Extra props applied to header element + * The handler is of the shape of `({ columns, headerIndex }) object` + */ + headerProps: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), + /** + * Extra props applied to header cell element + * The handler is of the shape of `({ columns, column, columnIndex, headerIndex }) => object` + */ + headerCellProps: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), + /** + * Extra props applied to row element + * The handler is of the shape of `({ columns, rowData, rowIndex }) => object` + */ + rowProps: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), + /** + * Extra props applied to row cell element + * The handler is of the shape of `({ columns, column, columnIndex, rowData, rowIndex }) => object` + */ + cellProps: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), + /** + * Extra props applied to ExpandIcon component + * The handler is of the shape of `({ rowData, rowIndex, depth, expandable, expanded }) => object` + */ + expandIconProps: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), + /** + * The key for the expand column which render the expand icon if the data is a tree + */ + expandColumnKey: PropTypes.string, + /** + * Default expanded row keys when initialize the table + */ + defaultExpandedRowKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), + /** + * Controlled expanded row keys + */ + expandedRowKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), + /** + * A callback function when expand or collapse a tree node + * The handler is of the shape of `({ expanded, rowData, rowIndex, rowKey }) => *` + */ + onRowExpand: PropTypes.func, + /** + * A callback function when the expanded row keys changed + * The handler is of the shape of `(expandedRowKeys) => *` + */ + onExpandedRowsChange: PropTypes.func, + /** + * The sort state for the table, will be ignored if `sortState` is set + */ + sortBy: PropTypes.shape({ + /** + * Sort key + */ + key: PropTypes.string, + /** + * Sort order + */ + order: PropTypes.oneOf([SortOrder.ASC, SortOrder.DESC]), + }), + /** + * Multiple columns sort state for the table + * + * example: + * ```js + * { + * 'column-0': SortOrder.ASC, + * 'column-1': SortOrder.DESC, + * } + * ``` + */ + sortState: PropTypes.object, + /** + * A callback function for the header cell click event + * The handler is of the shape of `({ column, key, order }) => *` + */ + onColumnSort: PropTypes.func, + /** + * A callback function when resizing the column width + * The handler is of the shape of `({ column, width }) => *` + */ + onColumnResize: PropTypes.func, + /** + * A callback function when resizing the column width ends + * The handler is of the shape of `({ column, width }) => *` + */ + onColumnResizeEnd: PropTypes.func, + /** + * Adds an additional isScrolling parameter to the row renderer. + * This parameter can be used to show a placeholder row while scrolling. + */ + useIsScrolling: PropTypes.bool, + /** + * Number of rows to render above/below the visible bounds of the list + */ + overscanRowCount: PropTypes.number, + /** + * Custom scrollbar size measurement + */ + getScrollbarSize: PropTypes.func, + /** + * A callback function when scrolling the table + * The handler is of the shape of `({ scrollLeft, scrollTop, horizontalScrollDirection, verticalScrollDirection, scrollUpdateWasRequested }) => *` + * + * `scrollLeft` and `scrollTop` are numbers. + * + * `horizontalDirection` and `verticalDirection` are either `forward` or `backward`. + * + * `scrollUpdateWasRequested` is a boolean. This value is true if the scroll was caused by `scrollTo*`, + * and false if it was the result of a user interaction in the browser. + */ + onScroll: PropTypes.func, + /** + * A callback function when scrolling the table within `onEndReachedThreshold` of the bottom + * The handler is of the shape of `({ distanceFromEnd }) => *` + */ + onEndReached: PropTypes.func, + /** + * Threshold in pixels for calling `onEndReached`. + */ + onEndReachedThreshold: PropTypes.number, + /** + * A callback function with information about the slice of rows that were just rendered + * The handler is of the shape of `({ overscanStartIndex, overscanStopIndex, startIndex, stopIndex }) => *` + */ + onRowsRendered: PropTypes.func, + /** + * A callback function when the scrollbar presence state changed + * The handler is of the shape of `({ size, vertical, horizontal }) => *` + */ + onScrollbarPresenceChange: PropTypes.func, + /** + * A object for the row event handlers + * Each of the keys is row event name, like `onClick`, `onDoubleClick` and etc. + * Each of the handlers is of the shape of `({ rowData, rowIndex, rowKey, event }) => *` + */ + rowEventHandlers: PropTypes.object, + /** + * whether to ignore function properties while comparing column definition + */ + ignoreFunctionInColumnCompare: PropTypes.bool, + /** + * A object for the custom components, like `ExpandIcon` and `SortIndicator` + */ + components: PropTypes.shape({ + TableCell: PropTypes.elementType, + TableHeaderCell: PropTypes.elementType, + ExpandIcon: PropTypes.elementType, + SortIndicator: PropTypes.elementType, + }), +}; - _handleRowHeightChange(rowKey: RowKey, size: number, rowIndex: number) { - if (this._resetIndex === null) this._resetIndex = rowIndex; - else if (this._resetIndex > rowIndex) this._resetIndex = rowIndex; +const BaseTable = React.memo(InnerBaseTable) as React.MemoExoticComponent< + React.ForwardRefExoticComponent> +> & { + Column: typeof Column; + PlaceholderKey: string; +}; - this._rowHeightMapBuffer[rowKey] = size; - this._updateRowHeights(); - } -} +(BaseTable as any).Column = Column; +(BaseTable as any).PlaceholderKey = ColumnManager.PlaceholderKey; export default BaseTable; From dce78c19de97f3ef8484decac6f8dadf452f2890 Mon Sep 17 00:00:00 2001 From: Nahrin Date: Fri, 19 Jun 2026 12:54:46 -0400 Subject: [PATCH 8/9] refactor: consolidate all type definitions into types.ts (#431) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move all component prop interfaces and handle types from individual component files into the central src/types.ts - Components now import their types from types.ts instead of defining them locally - Re-export all type definitions from index.ts for consumer access - No behavioral changes — only import paths changed Co-Authored-By: Claude Opus 4.6 --- src/AutoResizer.tsx | 8 +- src/BaseTable.tsx | 103 +-------------- src/ColumnResizer.tsx | 13 +- src/ExpandIcon.tsx | 9 +- src/GridTable.tsx | 41 +----- src/SortIndicator.tsx | 9 +- src/TableCell.tsx | 11 +- src/TableHeader.tsx | 31 +---- src/TableHeaderCell.tsx | 8 +- src/TableHeaderRow.tsx | 16 +-- src/TableRow.tsx | 31 +---- src/index.ts | 14 ++ src/types.ts | 279 ++++++++++++++++++++++++++++++++++++++++ 13 files changed, 306 insertions(+), 267 deletions(-) diff --git a/src/AutoResizer.tsx b/src/AutoResizer.tsx index 7b7c3779..d6680bde 100644 --- a/src/AutoResizer.tsx +++ b/src/AutoResizer.tsx @@ -2,13 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import AutoSizer from 'react-virtualized-auto-sizer'; -export interface AutoResizerProps { - className?: string; - width?: number; - height?: number; - children: (size: { width: number; height: number }) => React.ReactNode; - onResize?: (size: { width: number; height: number }) => void; -} +import type { AutoResizerProps } from './types'; /** * Decorator component that automatically adjusts the width and height of a single child diff --git a/src/BaseTable.tsx b/src/BaseTable.tsx index 8dce0302..108d9afd 100644 --- a/src/BaseTable.tsx +++ b/src/BaseTable.tsx @@ -4,7 +4,7 @@ import cn from 'classnames'; import memoize from 'memoize-one'; import GridTable from './GridTable'; -import type { GridTableHandle } from './GridTable'; +import type { GridTableHandle } from './types'; import TableHeaderRow from './TableHeaderRow'; import TableRow from './TableRow'; import TableHeaderCell from './TableHeaderCell'; @@ -33,6 +33,8 @@ import { } from './utils'; import type { + BaseTableProps, + BaseTableHandle, ColumnShape, RowData, RowKey, @@ -68,105 +70,6 @@ const RESIZE_THROTTLE_WAIT = 50; const EMPTY_ARRAY: any[] = []; -export interface BaseTableProps { - classPrefix?: string; - className?: string; - style?: React.CSSProperties; - children?: React.ReactNode; - columns?: ColumnShape[]; - data: RowData[]; - frozenData?: RowData[]; - rowKey: string | number; - width: number; - height?: number; - maxHeight?: number; - rowHeight?: number; - estimatedRowHeight?: number | ((args: { rowData: RowData; rowIndex: number }) => number); - headerHeight: number | number[]; - footerHeight?: number; - fixed?: boolean; - disabled?: boolean; - overlayRenderer?: React.ComponentType | React.ReactElement; - emptyRenderer?: React.ComponentType | React.ReactElement; - footerRenderer?: React.ComponentType | React.ReactElement; - headerRenderer?: React.ComponentType | React.ReactElement; - rowRenderer?: React.ComponentType | React.ReactElement; - headerClassName?: string | ((args: { columns: ColumnShape[]; headerIndex: number }) => string); - rowClassName?: string | ((args: { columns: ColumnShape[]; rowData: RowData; rowIndex: number }) => string); - headerProps?: Record | ((args: { columns: ColumnShape[]; headerIndex: number }) => Record); - headerCellProps?: - | Record - | ((args: { - columns: ColumnShape[]; - column: ColumnShape; - columnIndex: number; - headerIndex: number; - }) => Record); - rowProps?: - | Record - | ((args: { columns: ColumnShape[]; rowData: RowData; rowIndex: number }) => Record); - cellProps?: - | Record - | ((args: { - columns: ColumnShape[]; - column: ColumnShape; - columnIndex: number; - rowData: RowData; - rowIndex: number; - }) => Record); - expandIconProps?: - | Record - | ((args: { - rowData: RowData; - rowIndex: number; - depth: number; - expandable: boolean; - expanded: boolean; - }) => Record); - expandColumnKey?: string; - defaultExpandedRowKeys?: RowKey[]; - expandedRowKeys?: RowKey[]; - onRowExpand?: (args: { expanded: boolean; rowData: RowData; rowIndex: number; rowKey: RowKey }) => void; - onExpandedRowsChange?: (expandedRowKeys: RowKey[]) => void; - sortBy?: SortByShape; - sortState?: SortState; - onColumnSort?: (args: { column: ColumnShape; key: string; order: string }) => void; - onColumnResize?: (args: { column: ColumnShape; width: number }) => void; - onColumnResizeEnd?: (args: { column: ColumnShape; width: number }) => void; - useIsScrolling?: boolean; - overscanRowCount?: number; - getScrollbarSize?: () => number; - onScroll?: (args: ScrollArgs) => void; - onEndReached?: (args: { distanceFromEnd: number }) => void; - onEndReachedThreshold?: number; - onRowsRendered?: (args: RowsRenderedArgs) => void; - onScrollbarPresenceChange?: (args: { size: number; horizontal: boolean; vertical: boolean }) => void; - rowEventHandlers?: RowEventHandlers; - ignoreFunctionInColumnCompare?: boolean; - components?: TableComponents; -} - -export interface BaseTableHandle { - getDOMNode: () => HTMLDivElement | null; - getColumnManager: () => ColumnManager; - getExpandedRowKeys: () => RowKey[]; - getExpandedState: () => { - expandedData: RowData[]; - expandedRowKeys: RowKey[]; - expandedDepthMap: Record; - }; - getTotalRowsHeight: () => number; - getTotalColumnsWidth: () => number; - forceUpdateTable: () => void; - resetAfterRowIndex: (rowIndex?: number, shouldForceUpdate?: boolean) => void; - resetRowHeightCache: () => void; - scrollToPosition: (offset: { scrollLeft: number; scrollTop: number }) => void; - scrollToTop: (scrollTop: number) => void; - scrollToLeft: (scrollLeft: number) => void; - scrollToRow: (rowIndex?: number, align?: string) => void; - setExpandedRowKeys: (expandedRowKeys: RowKey[]) => void; -} - const DEFAULT_PROPS = { classPrefix: 'BaseTable', rowKey: 'id' as string | number, diff --git a/src/ColumnResizer.tsx b/src/ColumnResizer.tsx index 027d74e3..52248380 100644 --- a/src/ColumnResizer.tsx +++ b/src/ColumnResizer.tsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { noop, addClassName, removeClassName } from './utils'; -import type { ColumnShape } from './types'; +import type { ColumnResizerProps } from './types'; const INVALID_VALUE = null; @@ -54,17 +54,6 @@ const eventsFor = { let dragEventFor = eventsFor.mouse; -export interface ColumnResizerProps { - style?: React.CSSProperties; - column?: ColumnShape; - onResizeStart?: (column: ColumnShape) => void; - onResize?: (column: ColumnShape, width: number) => void; - onResizeStop?: (column: ColumnShape) => void; - minWidth?: number; - className?: string; - [key: string]: any; -} - /** * ColumnResizer for BaseTable */ diff --git a/src/ExpandIcon.tsx b/src/ExpandIcon.tsx index d8da7b8a..0a7d2b90 100644 --- a/src/ExpandIcon.tsx +++ b/src/ExpandIcon.tsx @@ -2,14 +2,7 @@ import React, { useCallback } from 'react'; import PropTypes from 'prop-types'; import cn from 'classnames'; -export interface ExpandIconProps { - expandable?: boolean; - expanded?: boolean; - indentSize?: number; - depth?: number; - onExpand?: (expanded: boolean) => void; - [key: string]: any; -} +import type { ExpandIconProps } from './types'; /** * default ExpandIcon for BaseTable diff --git a/src/GridTable.tsx b/src/GridTable.tsx index 7d57418b..b493f232 100644 --- a/src/GridTable.tsx +++ b/src/GridTable.tsx @@ -5,48 +5,9 @@ import { FixedSizeGrid, VariableSizeGrid } from 'react-window'; import memoize from 'memoize-one'; import Header from './TableHeader'; -import type { TableHeaderHandle } from './TableHeader'; import { getEstimatedTotalRowsHeight } from './utils'; -import type { ColumnShape, RowData, RowKey } from './types'; - -export interface GridTableProps { - containerStyle?: React.CSSProperties; - classPrefix?: string; - className?: string; - width: number; - height: number; - headerHeight: number | number[]; - headerWidth: number; - bodyWidth: number; - rowHeight: number; - estimatedRowHeight?: number | ((args: { rowData: RowData; rowIndex: number }) => number); - getRowHeight?: (rowIndex: number) => number; - columns: ColumnShape[]; - data: RowData[]; - frozenData?: RowData[]; - rowKey: string | number; - useIsScrolling?: boolean; - overscanRowCount?: number; - hoveredRowKey?: RowKey | null; - style?: React.CSSProperties; - onScrollbarPresenceChange?: (...args: any[]) => void; - onScroll?: (...args: any[]) => void; - onRowsRendered?: (args: any) => void; - headerRenderer: (args: any) => React.ReactNode; - rowRenderer: (args: any) => React.ReactNode; - [key: string]: any; -} - -export interface GridTableHandle { - resetAfterRowIndex: (rowIndex?: number, shouldForceUpdate?: boolean) => void; - forceUpdateTable: () => void; - scrollToPosition: (args: { scrollLeft?: number; scrollTop?: number }) => void; - scrollToTop: (scrollTop: number) => void; - scrollToLeft: (scrollLeft: number) => void; - scrollToRow: (rowIndex?: number, align?: string) => void; - getTotalRowsHeight: () => number; -} +import type { GridTableProps, GridTableHandle, TableHeaderHandle } from './types'; /** * A wrapper of the Grid for internal only diff --git a/src/SortIndicator.tsx b/src/SortIndicator.tsx index 8a52f9de..ae6d31e7 100644 --- a/src/SortIndicator.tsx +++ b/src/SortIndicator.tsx @@ -4,14 +4,7 @@ import cn from 'classnames'; import SortOrder from './SortOrder'; -import type { SortOrderValue } from './types'; - -export interface SortIndicatorProps { - sortOrder?: SortOrderValue; - className?: string; - style?: React.CSSProperties; - [key: string]: any; -} +import type { SortIndicatorProps } from './types'; /** * default SortIndicator for BaseTable diff --git a/src/TableCell.tsx b/src/TableCell.tsx index feb15bea..8c0f932f 100644 --- a/src/TableCell.tsx +++ b/src/TableCell.tsx @@ -2,16 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { toString } from './utils'; -import type { ColumnShape, RowData } from './types'; - -export interface TableCellProps { - className?: string; - cellData?: any; - column?: ColumnShape; - columnIndex?: number; - rowData?: RowData; - rowIndex?: number; -} +import type { TableCellProps } from './types'; /** * Cell component for BaseTable diff --git a/src/TableHeader.tsx b/src/TableHeader.tsx index 4408b8a1..77040508 100644 --- a/src/TableHeader.tsx +++ b/src/TableHeader.tsx @@ -1,36 +1,7 @@ import React, { useRef, useCallback, useImperativeHandle, useReducer } from 'react'; import PropTypes from 'prop-types'; -import type { ColumnShape, RowData } from './types'; - -export interface TableHeaderProps { - className?: string; - width: number; - height: number; - headerHeight: number | number[]; - rowWidth: number; - rowHeight: number; - columns: ColumnShape[]; - data: RowData[]; - frozenData?: RowData[]; - headerRenderer: (args: { - style: React.CSSProperties; - columns: ColumnShape[]; - headerIndex: number; - }) => React.ReactNode; - rowRenderer: (args: { - style: React.CSSProperties; - columns: ColumnShape[]; - rowData: RowData; - rowIndex: number; - }) => React.ReactNode; - hoveredRowKey?: string | number | null; -} - -export interface TableHeaderHandle { - scrollTo: (offset: number) => void; - forceUpdate: () => void; -} +import type { TableHeaderProps, TableHeaderHandle, RowData } from './types'; const InnerTableHeader = React.forwardRef( ( diff --git a/src/TableHeaderCell.tsx b/src/TableHeaderCell.tsx index a27bbe81..b9778a0d 100644 --- a/src/TableHeaderCell.tsx +++ b/src/TableHeaderCell.tsx @@ -1,13 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import type { ColumnShape } from './types'; - -export interface TableHeaderCellProps { - className?: string; - column?: ColumnShape; - columnIndex?: number; -} +import type { TableHeaderCellProps } from './types'; /** * HeaderCell component for BaseTable diff --git a/src/TableHeaderRow.tsx b/src/TableHeaderRow.tsx index b1b0e695..377e43be 100644 --- a/src/TableHeaderRow.tsx +++ b/src/TableHeaderRow.tsx @@ -3,21 +3,7 @@ import PropTypes from 'prop-types'; import { renderElement } from './utils'; -import type { ColumnShape } from './types'; - -export interface TableHeaderRowProps { - isScrolling?: boolean; - className?: string; - style?: React.CSSProperties; - columns: ColumnShape[]; - headerIndex?: number; - cellRenderer?: (args: any) => React.ReactNode; - headerRenderer?: React.ComponentType | React.ReactElement | ((props: any) => React.ReactNode); - expandColumnKey?: string; - expandIcon?: React.ComponentType; - tagName?: React.ElementType; - [key: string]: any; -} +import type { TableHeaderRowProps } from './types'; /** * HeaderRow component for BaseTable diff --git a/src/TableRow.tsx b/src/TableRow.tsx index f96bd7dc..9886648b 100644 --- a/src/TableRow.tsx +++ b/src/TableRow.tsx @@ -3,36 +3,7 @@ import PropTypes from 'prop-types'; import { renderElement } from './utils'; -import type { ColumnShape, RowData, RowKey, RowEventHandlers } from './types'; - -export interface TableRowProps { - isScrolling?: boolean; - className?: string; - style?: React.CSSProperties; - columns: ColumnShape[]; - rowData: RowData; - rowIndex: number; - rowKey?: RowKey; - expandColumnKey?: string; - depth?: number; - rowEventHandlers?: RowEventHandlers; - rowRenderer?: React.ComponentType | React.ReactElement | ((props: any) => React.ReactNode); - cellRenderer?: (args: any) => React.ReactNode; - expandIconRenderer?: (args: any) => React.ReactNode; - estimatedRowHeight?: number | ((args: { rowData: RowData; rowIndex: number }) => number); - getIsResetting?: () => boolean; - onRowHover?: (args: { - hovered: boolean; - rowData: RowData; - rowIndex: number; - rowKey: RowKey; - event: React.SyntheticEvent; - }) => void; - onRowExpand?: (args: { expanded: boolean; rowData: RowData; rowIndex: number; rowKey: RowKey }) => void; - onRowHeightChange?: (rowKey: RowKey, height: number, rowIndex: number, frozen?: any) => void; - tagName?: React.ElementType; - [key: string]: any; -} +import type { TableRowProps } from './types'; /** * Row component for BaseTable diff --git a/src/index.ts b/src/index.ts index 7112bede..a285eee7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,4 +37,18 @@ export type { SortByShape, SortState, TableComponents, + AutoResizerProps, + BaseTableProps, + BaseTableHandle, + ColumnResizerProps, + ExpandIconProps, + GridTableProps, + GridTableHandle, + SortIndicatorProps, + TableCellProps, + TableHeaderProps, + TableHeaderHandle, + TableHeaderCellProps, + TableHeaderRowProps, + TableRowProps, } from './types'; diff --git a/src/types.ts b/src/types.ts index 7a2aba31..1de60ddb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,7 @@ import React from 'react'; +import type ColumnManager from './ColumnManager'; + /** Enum-like constants */ export type SortOrderValue = 'asc' | 'desc'; export type AlignmentValue = 'left' | 'center' | 'right'; @@ -139,3 +141,280 @@ export interface TableComponents { ExpandIcon?: React.ElementType; SortIndicator?: React.ElementType; } + +// --------------------------------------------------------------------------- +// Component prop interfaces +// --------------------------------------------------------------------------- + +/** AutoResizer props */ +export interface AutoResizerProps { + className?: string; + width?: number; + height?: number; + children: (size: { width: number; height: number }) => React.ReactNode; + onResize?: (size: { width: number; height: number }) => void; +} + +/** BaseTable props */ +export interface BaseTableProps { + classPrefix?: string; + className?: string; + style?: React.CSSProperties; + children?: React.ReactNode; + columns?: ColumnShape[]; + data: RowData[]; + frozenData?: RowData[]; + rowKey: string | number; + width: number; + height?: number; + maxHeight?: number; + rowHeight?: number; + estimatedRowHeight?: number | ((args: { rowData: RowData; rowIndex: number }) => number); + headerHeight: number | number[]; + footerHeight?: number; + fixed?: boolean; + disabled?: boolean; + overlayRenderer?: React.ComponentType | React.ReactElement; + emptyRenderer?: React.ComponentType | React.ReactElement; + footerRenderer?: React.ComponentType | React.ReactElement; + headerRenderer?: React.ComponentType | React.ReactElement; + rowRenderer?: React.ComponentType | React.ReactElement; + headerClassName?: string | ((args: { columns: ColumnShape[]; headerIndex: number }) => string); + rowClassName?: string | ((args: { columns: ColumnShape[]; rowData: RowData; rowIndex: number }) => string); + headerProps?: Record | ((args: { columns: ColumnShape[]; headerIndex: number }) => Record); + headerCellProps?: + | Record + | ((args: { + columns: ColumnShape[]; + column: ColumnShape; + columnIndex: number; + headerIndex: number; + }) => Record); + rowProps?: + | Record + | ((args: { columns: ColumnShape[]; rowData: RowData; rowIndex: number }) => Record); + cellProps?: + | Record + | ((args: { + columns: ColumnShape[]; + column: ColumnShape; + columnIndex: number; + rowData: RowData; + rowIndex: number; + }) => Record); + expandIconProps?: + | Record + | ((args: { + rowData: RowData; + rowIndex: number; + depth: number; + expandable: boolean; + expanded: boolean; + }) => Record); + expandColumnKey?: string; + defaultExpandedRowKeys?: RowKey[]; + expandedRowKeys?: RowKey[]; + onRowExpand?: (args: { expanded: boolean; rowData: RowData; rowIndex: number; rowKey: RowKey }) => void; + onExpandedRowsChange?: (expandedRowKeys: RowKey[]) => void; + sortBy?: SortByShape; + sortState?: SortState; + onColumnSort?: (args: { column: ColumnShape; key: string; order: string }) => void; + onColumnResize?: (args: { column: ColumnShape; width: number }) => void; + onColumnResizeEnd?: (args: { column: ColumnShape; width: number }) => void; + useIsScrolling?: boolean; + overscanRowCount?: number; + getScrollbarSize?: () => number; + onScroll?: (args: ScrollArgs) => void; + onEndReached?: (args: { distanceFromEnd: number }) => void; + onEndReachedThreshold?: number; + onRowsRendered?: (args: RowsRenderedArgs) => void; + onScrollbarPresenceChange?: (args: { size: number; horizontal: boolean; vertical: boolean }) => void; + rowEventHandlers?: RowEventHandlers; + ignoreFunctionInColumnCompare?: boolean; + components?: TableComponents; +} + +/** BaseTable imperative handle */ +export interface BaseTableHandle { + getDOMNode: () => HTMLDivElement | null; + getColumnManager: () => ColumnManager; + getExpandedRowKeys: () => RowKey[]; + getExpandedState: () => { + expandedData: RowData[]; + expandedRowKeys: RowKey[]; + expandedDepthMap: Record; + }; + getTotalRowsHeight: () => number; + getTotalColumnsWidth: () => number; + forceUpdateTable: () => void; + resetAfterRowIndex: (rowIndex?: number, shouldForceUpdate?: boolean) => void; + resetRowHeightCache: () => void; + scrollToPosition: (offset: { scrollLeft: number; scrollTop: number }) => void; + scrollToTop: (scrollTop: number) => void; + scrollToLeft: (scrollLeft: number) => void; + scrollToRow: (rowIndex?: number, align?: string) => void; + setExpandedRowKeys: (expandedRowKeys: RowKey[]) => void; +} + +/** ColumnResizer props */ +export interface ColumnResizerProps { + style?: React.CSSProperties; + column?: ColumnShape; + onResizeStart?: (column: ColumnShape) => void; + onResize?: (column: ColumnShape, width: number) => void; + onResizeStop?: (column: ColumnShape) => void; + minWidth?: number; + className?: string; + [key: string]: any; +} + +/** ExpandIcon props */ +export interface ExpandIconProps { + expandable?: boolean; + expanded?: boolean; + indentSize?: number; + depth?: number; + onExpand?: (expanded: boolean) => void; + [key: string]: any; +} + +/** GridTable props */ +export interface GridTableProps { + containerStyle?: React.CSSProperties; + classPrefix?: string; + className?: string; + width: number; + height: number; + headerHeight: number | number[]; + headerWidth: number; + bodyWidth: number; + rowHeight: number; + estimatedRowHeight?: number | ((args: { rowData: RowData; rowIndex: number }) => number); + getRowHeight?: (rowIndex: number) => number; + columns: ColumnShape[]; + data: RowData[]; + frozenData?: RowData[]; + rowKey: string | number; + useIsScrolling?: boolean; + overscanRowCount?: number; + hoveredRowKey?: RowKey | null; + style?: React.CSSProperties; + onScrollbarPresenceChange?: (...args: any[]) => void; + onScroll?: (...args: any[]) => void; + onRowsRendered?: (args: any) => void; + headerRenderer: (args: any) => React.ReactNode; + rowRenderer: (args: any) => React.ReactNode; + [key: string]: any; +} + +/** GridTable imperative handle */ +export interface GridTableHandle { + resetAfterRowIndex: (rowIndex?: number, shouldForceUpdate?: boolean) => void; + forceUpdateTable: () => void; + scrollToPosition: (args: { scrollLeft?: number; scrollTop?: number }) => void; + scrollToTop: (scrollTop: number) => void; + scrollToLeft: (scrollLeft: number) => void; + scrollToRow: (rowIndex?: number, align?: string) => void; + getTotalRowsHeight: () => number; +} + +/** SortIndicator props */ +export interface SortIndicatorProps { + sortOrder?: SortOrderValue; + className?: string; + style?: React.CSSProperties; + [key: string]: any; +} + +/** TableCell props */ +export interface TableCellProps { + className?: string; + cellData?: any; + column?: ColumnShape; + columnIndex?: number; + rowData?: RowData; + rowIndex?: number; +} + +/** TableHeader props */ +export interface TableHeaderProps { + className?: string; + width: number; + height: number; + headerHeight: number | number[]; + rowWidth: number; + rowHeight: number; + columns: ColumnShape[]; + data: RowData[]; + frozenData?: RowData[]; + headerRenderer: (args: { + style: React.CSSProperties; + columns: ColumnShape[]; + headerIndex: number; + }) => React.ReactNode; + rowRenderer: (args: { + style: React.CSSProperties; + columns: ColumnShape[]; + rowData: RowData; + rowIndex: number; + }) => React.ReactNode; + hoveredRowKey?: string | number | null; +} + +/** TableHeader imperative handle */ +export interface TableHeaderHandle { + scrollTo: (offset: number) => void; + forceUpdate: () => void; +} + +/** TableHeaderCell props */ +export interface TableHeaderCellProps { + className?: string; + column?: ColumnShape; + columnIndex?: number; +} + +/** TableHeaderRow props */ +export interface TableHeaderRowProps { + isScrolling?: boolean; + className?: string; + style?: React.CSSProperties; + columns: ColumnShape[]; + headerIndex?: number; + cellRenderer?: (args: any) => React.ReactNode; + headerRenderer?: React.ComponentType | React.ReactElement | ((props: any) => React.ReactNode); + expandColumnKey?: string; + expandIcon?: React.ComponentType; + tagName?: React.ElementType; + [key: string]: any; +} + +/** TableRow props */ +export interface TableRowProps { + isScrolling?: boolean; + className?: string; + style?: React.CSSProperties; + columns: ColumnShape[]; + rowData: RowData; + rowIndex: number; + rowKey?: RowKey; + expandColumnKey?: string; + depth?: number; + rowEventHandlers?: RowEventHandlers; + rowRenderer?: React.ComponentType | React.ReactElement | ((props: any) => React.ReactNode); + cellRenderer?: (args: any) => React.ReactNode; + expandIconRenderer?: (args: any) => React.ReactNode; + estimatedRowHeight?: number | ((args: { rowData: RowData; rowIndex: number }) => number); + getIsResetting?: () => boolean; + onRowHover?: (args: { + hovered: boolean; + rowData: RowData; + rowIndex: number; + rowKey: RowKey; + event: React.SyntheticEvent; + }) => void; + onRowExpand?: (args: { expanded: boolean; rowData: RowData; rowIndex: number; rowKey: RowKey }) => void; + onRowHeightChange?: (rowKey: RowKey, height: number, rowIndex: number, frozen?: any) => void; + tagName?: React.ElementType; + [key: string]: any; +} From 7b6d61f1be14764c8594585b393e047906e2ace3 Mon Sep 17 00:00:00 2001 From: Nahrin Date: Fri, 19 Jun 2026 14:35:59 -0400 Subject: [PATCH 9/9] refactor: remove PropTypes and improve type safety (#431) - Remove PropTypes from all 12 component files (TypeScript interfaces supersede them) - Disable ESLint react/prop-types rule - Replace [key: string]: any with React.HTMLAttributes in ColumnResizerProps, ExpandIconProps, and SortIndicatorProps - Add explanatory comments for remaining index signatures in pass-through components - Remove prop-types import from all source files Co-Authored-By: Claude Opus 4.6 --- eslint.config.mjs | 2 +- src/AutoResizer.tsx | 26 ---- src/BaseTable.tsx | 256 ---------------------------------------- src/Column.tsx | 83 ------------- src/ColumnResizer.tsx | 32 ----- src/ExpandIcon.tsx | 9 -- src/GridTable.tsx | 28 ----- src/SortIndicator.tsx | 7 -- src/TableCell.tsx | 10 -- src/TableHeader.tsx | 15 --- src/TableHeaderCell.tsx | 8 -- src/TableHeaderRow.tsx | 15 --- src/TableRow.tsx | 24 ---- src/types.ts | 16 +-- 14 files changed, 7 insertions(+), 524 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 6cec6d06..c4ad5c24 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -42,7 +42,7 @@ export default [ }, }, rules: { - 'react/prop-types': 2, + 'react/prop-types': 0, 'react/jsx-uses-react': 'error', 'react/jsx-uses-vars': 'error', 'prettier/prettier': 'error', diff --git a/src/AutoResizer.tsx b/src/AutoResizer.tsx index d6680bde..e55ef1ed 100644 --- a/src/AutoResizer.tsx +++ b/src/AutoResizer.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import PropTypes from 'prop-types'; import AutoSizer from 'react-virtualized-auto-sizer'; import type { AutoResizerProps } from './types'; @@ -36,29 +35,4 @@ const AutoResizer: React.FC = ({ className, width, height, chi ); }; -AutoResizer.propTypes = { - /** - * Class name for the component - */ - className: PropTypes.string, - /** - * the width of the component, will be the container's width if not set - */ - width: PropTypes.number, - /** - * the height of the component, will be the container's width if not set - */ - height: PropTypes.number, - /** - * A callback function to render the children component - * The handler is of the shape of `({ width, height }) => node` - */ - children: PropTypes.func.isRequired, - /** - * A callback function when the size of the table container changed if the width and height are not set - * The handler is of the shape of `({ width, height }) => *` - */ - onResize: PropTypes.func, -}; - export default AutoResizer; diff --git a/src/BaseTable.tsx b/src/BaseTable.tsx index 108d9afd..8e68d868 100644 --- a/src/BaseTable.tsx +++ b/src/BaseTable.tsx @@ -1,5 +1,4 @@ import React, { useState, useRef, useCallback, useMemo, useEffect, useReducer, useImperativeHandle } from 'react'; -import PropTypes from 'prop-types'; import cn from 'classnames'; import memoize from 'memoize-one'; @@ -1086,261 +1085,6 @@ const InnerBaseTable = React.forwardRef((rawPro ); }); -InnerBaseTable.propTypes = { - /** - * Prefix for table's inner className - */ - classPrefix: PropTypes.string, - /** - * Class name for the table - */ - className: PropTypes.string, - /** - * Custom style for the table - */ - style: PropTypes.object, - /** - * A collection of Column - */ - children: PropTypes.node, - /** - * Columns for the table - */ - columns: PropTypes.arrayOf(PropTypes.shape(Column.propTypes as any)), - /** - * The data for the table - */ - data: PropTypes.array.isRequired, - /** - * The data be frozen to top, `rowIndex` is negative and started from `-1` - */ - frozenData: PropTypes.array, - /** - * The key field of each data item - */ - rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - /** - * The width of the table - */ - width: PropTypes.number.isRequired, - /** - * The height of the table, will be ignored if `maxHeight` is set - */ - height: PropTypes.number, - /** - * The max height of the table, the table's height will auto change when data changes, - * will turns to vertical scroll if reaches the max height - */ - maxHeight: PropTypes.number, - /** - * The height of each table row, will be only used by frozen rows if `estimatedRowHeight` is set - */ - rowHeight: PropTypes.number, - /** - * Estimated row height, the real height will be measure dynamically according to the content - * The callback is of the shape of `({ rowData, rowIndex }) => number` - */ - estimatedRowHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), - /** - * The height of the table header, set to 0 to hide the header, could be an array to render multi headers. - */ - headerHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.arrayOf(PropTypes.number)]).isRequired, - /** - * The height of the table footer - */ - footerHeight: PropTypes.number, - /** - * Whether the width of the columns are fixed or flexible - */ - fixed: PropTypes.bool, - /** - * Whether the table is disabled - */ - disabled: PropTypes.bool, - /** - * Custom renderer on top of the table component - */ - overlayRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), - /** - * Custom renderer when the length of data is 0 - */ - emptyRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), - /** - * Custom footer renderer, available only if `footerHeight` is larger then 0 - */ - footerRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), - /** - * Custom header renderer - * The renderer receives props `{ cells, columns, headerIndex }` - */ - headerRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), - /** - * Custom row renderer - * The renderer receives props `{ isScrolling, cells, columns, rowData, rowIndex, depth }` - */ - rowRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), - /** - * Class name for the table header, could be a callback to return the class name - * The callback is of the shape of `({ columns, headerIndex }) => string` - */ - headerClassName: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), - /** - * Class name for the table row, could be a callback to return the class name - * The callback is of the shape of `({ columns, rowData, rowIndex }) => string` - */ - rowClassName: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), - /** - * Extra props applied to header element - * The handler is of the shape of `({ columns, headerIndex }) object` - */ - headerProps: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), - /** - * Extra props applied to header cell element - * The handler is of the shape of `({ columns, column, columnIndex, headerIndex }) => object` - */ - headerCellProps: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), - /** - * Extra props applied to row element - * The handler is of the shape of `({ columns, rowData, rowIndex }) => object` - */ - rowProps: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), - /** - * Extra props applied to row cell element - * The handler is of the shape of `({ columns, column, columnIndex, rowData, rowIndex }) => object` - */ - cellProps: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), - /** - * Extra props applied to ExpandIcon component - * The handler is of the shape of `({ rowData, rowIndex, depth, expandable, expanded }) => object` - */ - expandIconProps: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), - /** - * The key for the expand column which render the expand icon if the data is a tree - */ - expandColumnKey: PropTypes.string, - /** - * Default expanded row keys when initialize the table - */ - defaultExpandedRowKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), - /** - * Controlled expanded row keys - */ - expandedRowKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), - /** - * A callback function when expand or collapse a tree node - * The handler is of the shape of `({ expanded, rowData, rowIndex, rowKey }) => *` - */ - onRowExpand: PropTypes.func, - /** - * A callback function when the expanded row keys changed - * The handler is of the shape of `(expandedRowKeys) => *` - */ - onExpandedRowsChange: PropTypes.func, - /** - * The sort state for the table, will be ignored if `sortState` is set - */ - sortBy: PropTypes.shape({ - /** - * Sort key - */ - key: PropTypes.string, - /** - * Sort order - */ - order: PropTypes.oneOf([SortOrder.ASC, SortOrder.DESC]), - }), - /** - * Multiple columns sort state for the table - * - * example: - * ```js - * { - * 'column-0': SortOrder.ASC, - * 'column-1': SortOrder.DESC, - * } - * ``` - */ - sortState: PropTypes.object, - /** - * A callback function for the header cell click event - * The handler is of the shape of `({ column, key, order }) => *` - */ - onColumnSort: PropTypes.func, - /** - * A callback function when resizing the column width - * The handler is of the shape of `({ column, width }) => *` - */ - onColumnResize: PropTypes.func, - /** - * A callback function when resizing the column width ends - * The handler is of the shape of `({ column, width }) => *` - */ - onColumnResizeEnd: PropTypes.func, - /** - * Adds an additional isScrolling parameter to the row renderer. - * This parameter can be used to show a placeholder row while scrolling. - */ - useIsScrolling: PropTypes.bool, - /** - * Number of rows to render above/below the visible bounds of the list - */ - overscanRowCount: PropTypes.number, - /** - * Custom scrollbar size measurement - */ - getScrollbarSize: PropTypes.func, - /** - * A callback function when scrolling the table - * The handler is of the shape of `({ scrollLeft, scrollTop, horizontalScrollDirection, verticalScrollDirection, scrollUpdateWasRequested }) => *` - * - * `scrollLeft` and `scrollTop` are numbers. - * - * `horizontalDirection` and `verticalDirection` are either `forward` or `backward`. - * - * `scrollUpdateWasRequested` is a boolean. This value is true if the scroll was caused by `scrollTo*`, - * and false if it was the result of a user interaction in the browser. - */ - onScroll: PropTypes.func, - /** - * A callback function when scrolling the table within `onEndReachedThreshold` of the bottom - * The handler is of the shape of `({ distanceFromEnd }) => *` - */ - onEndReached: PropTypes.func, - /** - * Threshold in pixels for calling `onEndReached`. - */ - onEndReachedThreshold: PropTypes.number, - /** - * A callback function with information about the slice of rows that were just rendered - * The handler is of the shape of `({ overscanStartIndex, overscanStopIndex, startIndex, stopIndex }) => *` - */ - onRowsRendered: PropTypes.func, - /** - * A callback function when the scrollbar presence state changed - * The handler is of the shape of `({ size, vertical, horizontal }) => *` - */ - onScrollbarPresenceChange: PropTypes.func, - /** - * A object for the row event handlers - * Each of the keys is row event name, like `onClick`, `onDoubleClick` and etc. - * Each of the handlers is of the shape of `({ rowData, rowIndex, rowKey, event }) => *` - */ - rowEventHandlers: PropTypes.object, - /** - * whether to ignore function properties while comparing column definition - */ - ignoreFunctionInColumnCompare: PropTypes.bool, - /** - * A object for the custom components, like `ExpandIcon` and `SortIndicator` - */ - components: PropTypes.shape({ - TableCell: PropTypes.elementType, - TableHeaderCell: PropTypes.elementType, - ExpandIcon: PropTypes.elementType, - SortIndicator: PropTypes.elementType, - }), -}; - const BaseTable = React.memo(InnerBaseTable) as React.MemoExoticComponent< React.ForwardRefExoticComponent> > & { diff --git a/src/Column.tsx b/src/Column.tsx index 3ab857ba..7ecf5826 100644 --- a/src/Column.tsx +++ b/src/Column.tsx @@ -1,6 +1,3 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - import type { AlignmentValue, FrozenDirectionValue } from './types'; export const Alignment: Record = { @@ -27,84 +24,4 @@ const Column: React.FC & { Column.Alignment = Alignment; Column.FrozenDirection = FrozenDirection; -Column.propTypes = { - /** - * Class name for the column cell, could be a callback to return the class name - * The callback is of the shape of `({ cellData, columns, column, columnIndex, rowData, rowIndex }) => string` - */ - className: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), - /** - * Class name for the column header, could be a callback to return the class name - * The callback is of the shape of `({ columns, column, columnIndex, headerIndex }) => string` - */ - headerClassName: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), - /** - * Custom style for the column cell, including the header cells - */ - style: PropTypes.object, - /** - * Title for the column header - */ - title: PropTypes.node, - /** - * Data key for the column cell, could be "a.b.c" - */ - dataKey: PropTypes.string, - /** - * Custom cell data getter - * The handler is of the shape of `({ columns, column, columnIndex, rowData, rowIndex }) => node` - */ - dataGetter: PropTypes.func, - /** - * Alignment of the column cell - */ - align: PropTypes.oneOf(['left', 'center', 'right']), - /** - * Flex grow style, defaults to 0 - */ - flexGrow: PropTypes.number, - /** - * Flex shrink style, defaults to 1 for flexible table and 0 for fixed table - */ - flexShrink: PropTypes.number, - /** - * The width of the column, gutter width is not included - */ - width: PropTypes.number.isRequired, - /** - * Maximum width of the column, used if the column is resizable - */ - maxWidth: PropTypes.number, - /** - * Minimum width of the column, used if the column is resizable - */ - minWidth: PropTypes.number, - /** - * Whether the column is frozen and what's the frozen side - */ - frozen: PropTypes.oneOf(['left', 'right', true, false]), - /** - * Whether the column is hidden - */ - hidden: PropTypes.bool, - /** - * Whether the column is resizable, defaults to false - */ - resizable: PropTypes.bool, - /** - * Whether the column is sortable, defaults to false - */ - sortable: PropTypes.bool, - /** - * Custom column cell renderer - * The renderer receives props `{ cellData, columns, column, columnIndex, rowData, rowIndex, container, isScrolling }` - */ - cellRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), - /** - * Custom column header renderer - * The renderer receives props `{ columns, column, columnIndex, headerIndex, container }` - */ - headerRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), -}; - export default Column; diff --git a/src/ColumnResizer.tsx b/src/ColumnResizer.tsx index 52248380..3661799f 100644 --- a/src/ColumnResizer.tsx +++ b/src/ColumnResizer.tsx @@ -1,6 +1,4 @@ import React, { useRef, useCallback, useEffect } from 'react'; -import PropTypes from 'prop-types'; - import { noop, addClassName, removeClassName } from './utils'; import type { ColumnResizerProps } from './types'; @@ -214,34 +212,4 @@ const ColumnResizer: React.FC = React.memo( }, ); -ColumnResizer.propTypes = { - /** - * Custom style for the drag handler - */ - style: PropTypes.object, - /** - * The column object to be dragged - */ - column: PropTypes.object, - /** - * A callback function when resizing started - * The callback is of the shape of `(column) => *` - */ - onResizeStart: PropTypes.func, - /** - * A callback function when resizing the column - * The callback is of the shape of `(column, width) => *` - */ - onResize: PropTypes.func, - /** - * A callback function when resizing stopped - * The callback is of the shape of `(column) => *` - */ - onResizeStop: PropTypes.func, - /** - * Minimum width of the column could be resized to if the column's `minWidth` is not set - */ - minWidth: PropTypes.number, -}; - export default ColumnResizer; diff --git a/src/ExpandIcon.tsx b/src/ExpandIcon.tsx index 0a7d2b90..d12e932b 100644 --- a/src/ExpandIcon.tsx +++ b/src/ExpandIcon.tsx @@ -1,5 +1,4 @@ import React, { useCallback } from 'react'; -import PropTypes from 'prop-types'; import cn from 'classnames'; import type { ExpandIconProps } from './types'; @@ -49,12 +48,4 @@ const ExpandIcon: React.FC = React.memo( }, ); -ExpandIcon.propTypes = { - expandable: PropTypes.bool, - expanded: PropTypes.bool, - indentSize: PropTypes.number, - depth: PropTypes.number, - onExpand: PropTypes.func, -}; - export default ExpandIcon; diff --git a/src/GridTable.tsx b/src/GridTable.tsx index b493f232..fcce9ff2 100644 --- a/src/GridTable.tsx +++ b/src/GridTable.tsx @@ -1,5 +1,4 @@ import React, { useRef, useCallback, useMemo, useImperativeHandle } from 'react'; -import PropTypes from 'prop-types'; import cn from 'classnames'; import { FixedSizeGrid, VariableSizeGrid } from 'react-window'; import memoize from 'memoize-one'; @@ -183,33 +182,6 @@ const InnerGridTable = React.forwardRef( }, ); -InnerGridTable.propTypes = { - containerStyle: PropTypes.object, - classPrefix: PropTypes.string, - className: PropTypes.string, - width: PropTypes.number.isRequired, - height: PropTypes.number.isRequired, - headerHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.arrayOf(PropTypes.number)]).isRequired, - headerWidth: PropTypes.number.isRequired, - bodyWidth: PropTypes.number.isRequired, - rowHeight: PropTypes.number.isRequired, - estimatedRowHeight: PropTypes.oneOfType([PropTypes.func, PropTypes.number]), - getRowHeight: PropTypes.func, - columns: PropTypes.arrayOf(PropTypes.object).isRequired, - data: PropTypes.array.isRequired, - frozenData: PropTypes.array, - rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - useIsScrolling: PropTypes.bool, - overscanRowCount: PropTypes.number, - hoveredRowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - style: PropTypes.object, - onScrollbarPresenceChange: PropTypes.func, - onScroll: PropTypes.func, - onRowsRendered: PropTypes.func, - headerRenderer: PropTypes.func.isRequired, - rowRenderer: PropTypes.func.isRequired, -}; - const GridTable = React.memo(InnerGridTable); export default GridTable; diff --git a/src/SortIndicator.tsx b/src/SortIndicator.tsx index ae6d31e7..ea89ee5d 100644 --- a/src/SortIndicator.tsx +++ b/src/SortIndicator.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import PropTypes from 'prop-types'; import cn from 'classnames'; import SortOrder from './SortOrder'; @@ -30,10 +29,4 @@ const SortIndicator: React.FC = ({ sortOrder, className, sty ); }; -SortIndicator.propTypes = { - sortOrder: PropTypes.oneOf([SortOrder.ASC, SortOrder.DESC]), - className: PropTypes.string, - style: PropTypes.object, -}; - export default SortIndicator; diff --git a/src/TableCell.tsx b/src/TableCell.tsx index 8c0f932f..383c60b1 100644 --- a/src/TableCell.tsx +++ b/src/TableCell.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { toString } from './utils'; import type { TableCellProps } from './types'; @@ -11,13 +10,4 @@ const TableCell: React.FC = ({ className, cellData, column, colu
{React.isValidElement(cellData) ? cellData : toString(cellData)}
); -TableCell.propTypes = { - className: PropTypes.string, - cellData: PropTypes.any, - column: PropTypes.object, - columnIndex: PropTypes.number, - rowData: PropTypes.object, - rowIndex: PropTypes.number, -}; - export default TableCell; diff --git a/src/TableHeader.tsx b/src/TableHeader.tsx index 77040508..8af9150a 100644 --- a/src/TableHeader.tsx +++ b/src/TableHeader.tsx @@ -1,5 +1,4 @@ import React, { useRef, useCallback, useImperativeHandle, useReducer } from 'react'; -import PropTypes from 'prop-types'; import type { TableHeaderProps, TableHeaderHandle, RowData } from './types'; @@ -78,20 +77,6 @@ const InnerTableHeader = React.forwardRef( }, ); -InnerTableHeader.propTypes = { - className: PropTypes.string, - width: PropTypes.number.isRequired, - height: PropTypes.number.isRequired, - headerHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.arrayOf(PropTypes.number)]).isRequired, - rowWidth: PropTypes.number.isRequired, - rowHeight: PropTypes.number.isRequired, - columns: PropTypes.arrayOf(PropTypes.object).isRequired, - data: PropTypes.array.isRequired, - frozenData: PropTypes.array, - headerRenderer: PropTypes.func.isRequired, - rowRenderer: PropTypes.func.isRequired, -}; - const TableHeader = React.memo(InnerTableHeader); export default TableHeader; diff --git a/src/TableHeaderCell.tsx b/src/TableHeaderCell.tsx index b9778a0d..48cf9c86 100644 --- a/src/TableHeaderCell.tsx +++ b/src/TableHeaderCell.tsx @@ -1,6 +1,4 @@ import React from 'react'; -import PropTypes from 'prop-types'; - import type { TableHeaderCellProps } from './types'; /** @@ -10,10 +8,4 @@ const TableHeaderCell: React.FC = ({ className, column, co
{column?.title}
); -TableHeaderCell.propTypes = { - className: PropTypes.string, - column: PropTypes.object, - columnIndex: PropTypes.number, -}; - export default TableHeaderCell; diff --git a/src/TableHeaderRow.tsx b/src/TableHeaderRow.tsx index 377e43be..bccaa25c 100644 --- a/src/TableHeaderRow.tsx +++ b/src/TableHeaderRow.tsx @@ -1,6 +1,4 @@ import React from 'react'; -import PropTypes from 'prop-types'; - import { renderElement } from './utils'; import type { TableHeaderRowProps } from './types'; @@ -41,17 +39,4 @@ const TableHeaderRow: React.FC = ({ ); }; -TableHeaderRow.propTypes = { - isScrolling: PropTypes.bool, - className: PropTypes.string, - style: PropTypes.object, - columns: PropTypes.arrayOf(PropTypes.object).isRequired, - headerIndex: PropTypes.number, - cellRenderer: PropTypes.func, - headerRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), - expandColumnKey: PropTypes.string, - expandIcon: PropTypes.func, - tagName: PropTypes.elementType, -}; - export default TableHeaderRow; diff --git a/src/TableRow.tsx b/src/TableRow.tsx index 9886648b..8cbf6123 100644 --- a/src/TableRow.tsx +++ b/src/TableRow.tsx @@ -1,6 +1,4 @@ import React, { useState, useRef, useCallback, useEffect, useMemo } from 'react'; -import PropTypes from 'prop-types'; - import { renderElement } from './utils'; import type { TableRowProps } from './types'; @@ -165,26 +163,4 @@ const TableRow: React.FC = React.memo( }, ); -TableRow.propTypes = { - isScrolling: PropTypes.bool, - className: PropTypes.string, - style: PropTypes.object, - columns: PropTypes.arrayOf(PropTypes.object).isRequired, - rowData: PropTypes.object.isRequired, - rowIndex: PropTypes.number.isRequired, - rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - expandColumnKey: PropTypes.string, - depth: PropTypes.number, - rowEventHandlers: PropTypes.object, - rowRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), - cellRenderer: PropTypes.func, - expandIconRenderer: PropTypes.func, - estimatedRowHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), - getIsResetting: PropTypes.func, - onRowHover: PropTypes.func, - onRowExpand: PropTypes.func, - onRowHeightChange: PropTypes.func, - tagName: PropTypes.elementType, -}; - export default TableRow; diff --git a/src/types.ts b/src/types.ts index 1de60ddb..9b6b7d16 100644 --- a/src/types.ts +++ b/src/types.ts @@ -257,25 +257,21 @@ export interface BaseTableHandle { } /** ColumnResizer props */ -export interface ColumnResizerProps { - style?: React.CSSProperties; +export interface ColumnResizerProps extends React.HTMLAttributes { column?: ColumnShape; onResizeStart?: (column: ColumnShape) => void; onResize?: (column: ColumnShape, width: number) => void; onResizeStop?: (column: ColumnShape) => void; minWidth?: number; - className?: string; - [key: string]: any; } /** ExpandIcon props */ -export interface ExpandIconProps { +export interface ExpandIconProps extends React.HTMLAttributes { expandable?: boolean; expanded?: boolean; indentSize?: number; depth?: number; onExpand?: (expanded: boolean) => void; - [key: string]: any; } /** GridTable props */ @@ -304,6 +300,7 @@ export interface GridTableProps { onRowsRendered?: (args: any) => void; headerRenderer: (args: any) => React.ReactNode; rowRenderer: (args: any) => React.ReactNode; + /** Allow pass-through props forwarded to the underlying Grid component */ [key: string]: any; } @@ -319,11 +316,8 @@ export interface GridTableHandle { } /** SortIndicator props */ -export interface SortIndicatorProps { +export interface SortIndicatorProps extends React.HTMLAttributes { sortOrder?: SortOrderValue; - className?: string; - style?: React.CSSProperties; - [key: string]: any; } /** TableCell props */ @@ -386,6 +380,7 @@ export interface TableHeaderRowProps { expandColumnKey?: string; expandIcon?: React.ComponentType; tagName?: React.ElementType; + /** Allow pass-through props forwarded to the tag element */ [key: string]: any; } @@ -416,5 +411,6 @@ export interface TableRowProps { onRowExpand?: (args: { expanded: boolean; rowData: RowData; rowIndex: number; rowKey: RowKey }) => void; onRowHeightChange?: (rowKey: RowKey, height: number, rowIndex: number, frozen?: any) => void; tagName?: React.ElementType; + /** Allow pass-through props forwarded to the tag element */ [key: string]: any; }