diff --git a/README.md b/README.md index 5adb71f89..ec1289528 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,7 @@ React.render(, mountNode); | fixed | String \| Boolean | | this column will be fixed when table scroll horizontally: true or 'left' or 'right' | | align | String | | specify how cell content is aligned | | ellipsis | Boolean | | specify whether cell content be ellipsized | +| resizable | Boolean | | column resize | | rowScope | 'row' \| 'rowgroup' | | Set scope attribute for all cells in this column | | onCell | Function(record, index) | | Set custom props per each cell. | | onHeaderCell | Function(record) | | Set custom props per each header cell. | diff --git a/assets/index.less b/assets/index.less index bfa245b03..64bfe1eef 100644 --- a/assets/index.less +++ b/assets/index.less @@ -63,9 +63,42 @@ } } + &-column-resizing { + cursor: col-resize; + } + &-cell { background: #f4f4f4; + &-resize-handle { + position: absolute; + top: 0; + inset-inline-end: 0; + width: 4px; + height: 100%; + cursor: col-resize; + z-index: 1; + background: red; + } + + &-resize-line { + position: absolute; + width: 4px; + background: red; + height: 100%; + top: 0; + transform: translateX(-50%); + z-index: 2; + } + + &-fix-left &-resize-handle { + right: 0 + } + + &-fix-right &-resize-handle { + left: 0 + } + &-fix-left, &-fix-right { z-index: 2; diff --git a/docs/examples/column-resize.tsx b/docs/examples/column-resize.tsx index a3f15cbbc..81b33bb42 100644 --- a/docs/examples/column-resize.tsx +++ b/docs/examples/column-resize.tsx @@ -1,95 +1,106 @@ -import React from 'react'; -import { Resizable } from 'react-resizable'; -import Table from 'rc-table'; +import React, { useState } from 'react'; +import Table, { INTERNAL_HOOKS } from 'rc-table'; +import type { ColumnType } from 'rc-table'; import '../../assets/index.less'; -import 'react-resizable/css/styles.css'; -import type { ColumnType } from '@/interface'; -const ResizableTitle = props => { - const { onResize, width, ...restProps } = props; +const data = [ + { a: '123', b: 'xxxxxxxx xxxxxxxx', d: 3, key: '1' }, + { a: 'cdd', b: 'edd12221 edd12221', d: 3, key: '2' }, + { a: '133', c: 'edd12221 edd12221', d: 2, key: '3' }, + { a: '133', c: 'edd12221 edd12221', d: 2, key: '4' }, + { a: '133', c: 'edd12221 edd12221', d: 2, key: '5' }, + { a: '133', c: 'edd12221 edd12221', d: 2, key: '6' }, + { a: '133', c: 'edd12221 edd12221', d: 2, key: '7' }, + { a: '133', c: 'edd12221 edd12221', d: 2, key: '8' }, + { a: '133', c: 'edd12221 edd12221', d: 2, key: '9' }, +]; - if (!width) { - return
; - } +const Demo = () => { + const [widthMap, setWidthMap] = useState>(new Map()); + + const columns1 = [ + { title: 'title1', dataIndex: 'aaa', key: 'aaa', width: 100 }, + { title: 'title2', dataIndex: 'bbb', key: 'bbb', width: 100 }, + ].map(i => ({ + ...i, + resizable: true, + width: widthMap.get(i.key ?? i.dataIndex) ?? i.width, + })) as ColumnType[]; + + const columns2 = [ + { title: 'title1', dataIndex: 'a', key: 'a', fixed: 'left' }, + { title: 'title2', dataIndex: 'b', key: 'b', fixed: 'left' }, + { title: 'title3', dataIndex: 'c', key: 'c' }, + { title: 'title4', dataIndex: 'b', key: 'd' }, + { title: 'title5', dataIndex: 'b', key: 'e' }, + { title: 'title6', dataIndex: 'b', key: 'f' }, + { title: 'title7', dataIndex: 'b', key: 'g' }, + { title: 'title8', dataIndex: 'b', key: 'h' }, + { title: 'title9', dataIndex: 'b', key: 'i' }, + { title: 'title10', dataIndex: 'b', key: 'j' }, + { title: 'title11', dataIndex: 'b', key: 'k', fixed: 'right' }, + { title: 'title12', dataIndex: 'b', key: 'l', fixed: 'right' }, + ].map(i => ({ + ...i, + resizable: true, + width: widthMap.get(i.key ?? i.dataIndex) ?? 150, + })) as ColumnType[]; return ( - - - +
+ table width: 800px {'columns=[{width: 100, width: 100}]'} 情况 + t + (c.width as number), 0) }} + columns={columns1} + data={data} + onColumnResizeComplete={({ columnWidths }) => { + setWidthMap(prev => { + const result = new Map(prev); + columnWidths.forEach(i => { + result.set(i.columnKey, i.width); + }); + return result; + }); + }} + internalHooks={INTERNAL_HOOKS} + getContainerWidth={(ele, width) => { + // Minus border + const borderWidth = getComputedStyle( + ele.querySelector('.rc-table-body'), + ).borderInlineStartWidth; + const mergedWidth = width - parseInt(borderWidth, 10); + return mergedWidth; + }} + /> +
+ 大多数情况 +
t + (c.width as number), 0) }} + columns={columns2} + data={data} + onColumnResizeComplete={({ columnWidths }) => { + setWidthMap(prev => { + const result = new Map(prev); + columnWidths.forEach(i => { + result.set(i.columnKey, i.width); + }); + return result; + }); + }} + internalHooks={INTERNAL_HOOKS} + getContainerWidth={(ele, width) => { + // Minus border + const borderWidth = getComputedStyle( + ele.querySelector('.rc-table-body'), + ).borderInlineStartWidth; + const mergedWidth = width - parseInt(borderWidth, 10); + return mergedWidth; + }} + /> + ); }; -interface RecordType { - a: string; - b?: string; - c?: string; - d?: number; - key: string; -} - -interface DemoState { - columns: ColumnType[]; -} - -class Demo extends React.Component<{}, DemoState> { - state: DemoState = { - columns: [ - { title: 'title1', dataIndex: 'a', key: 'a', width: 100 }, - { title: 'title2', dataIndex: 'b', key: 'b', width: 100 }, - { title: 'title3', dataIndex: 'c', key: 'c', width: 200 }, - { - title: 'Operations', - dataIndex: '', - key: 'd', - render() { - return Operations; - }, - }, - ], - }; - - components = { - header: { - cell: ResizableTitle, - }, - }; - - data = [ - { a: '123', key: '1' }, - { a: 'cdd', b: 'edd', key: '2' }, - { a: '1333', c: 'eee', d: 2, key: '3' }, - ]; - - handleResize = - index => - (e, { size }) => { - this.setState(({ columns }) => { - const nextColumns = [...columns]; - nextColumns[index] = { - ...nextColumns[index], - width: size.width, - }; - return { columns: nextColumns }; - }); - }; - - render() { - const columns = this.state.columns.map((col, index) => ({ - ...col, - onHeaderCell: (column: ColumnType) => - ({ - width: column.width, - onResize: this.handleResize(index), - }) as any, - })); - - return ( -
-

Integrate with react-resizable

-
- - ); - } -} - export default Demo; diff --git a/package.json b/package.json index a1a64a73c..264031981 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,6 @@ "react-dnd": "^2.5.4", "react-dnd-html5-backend": "^2.5.4", "react-dom": "^16.0.0", - "react-resizable": "^3.0.5", "react-virtualized": "^9.12.0", "react-window": "^1.8.5", "regenerator-runtime": "^0.14.0", diff --git a/src/Body/MeasureCell.tsx b/src/Body/MeasureCell.tsx index 1fc8d9cdf..a1e482f3a 100644 --- a/src/Body/MeasureCell.tsx +++ b/src/Body/MeasureCell.tsx @@ -3,15 +3,15 @@ import ResizeObserver from 'rc-resize-observer'; export interface MeasureCellProps { columnKey: React.Key; - onColumnResize: (key: React.Key, width: number) => void; + onColumnWidthChange: (key: React.Key, width: number) => void; } -export default function MeasureCell({ columnKey, onColumnResize }: MeasureCellProps) { +export default function MeasureCell({ columnKey, onColumnWidthChange }: MeasureCellProps) { const cellRef = React.useRef(); React.useEffect(() => { if (cellRef.current) { - onColumnResize(columnKey, cellRef.current.offsetWidth); + onColumnWidthChange(columnKey, cellRef.current.offsetWidth); } }, []); diff --git a/src/Body/MeasureRow.tsx b/src/Body/MeasureRow.tsx index 3dbd12113..48d60f40e 100644 --- a/src/Body/MeasureRow.tsx +++ b/src/Body/MeasureRow.tsx @@ -4,11 +4,15 @@ import MeasureCell from './MeasureCell'; export interface MeasureCellProps { prefixCls: string; - onColumnResize: (key: React.Key, width: number) => void; + onColumnWidthChange: (key: React.Key, width: number) => void; columnsKey: React.Key[]; } -export default function MeasureRow({ prefixCls, columnsKey, onColumnResize }: MeasureCellProps) { +export default function MeasureRow({ + prefixCls, + columnsKey, + onColumnWidthChange, +}: MeasureCellProps) { return ( { infoList.forEach(({ data: columnKey, size }) => { - onColumnResize(columnKey, size.offsetWidth); + onColumnWidthChange(columnKey, size.offsetWidth); }); }} > {columnsKey.map(columnKey => ( - + ))} diff --git a/src/Body/index.tsx b/src/Body/index.tsx index 3173fa22d..5a0e0f4a2 100644 --- a/src/Body/index.tsx +++ b/src/Body/index.tsx @@ -25,7 +25,7 @@ function Body(props: BodyProps) { const { prefixCls, getComponent, - onColumnResize, + onColumnWidthChange, flattenColumns, getRowKey, expandedKeys, @@ -34,7 +34,7 @@ function Body(props: BodyProps) { } = useContext(TableContext, [ 'prefixCls', 'getComponent', - 'onColumnResize', + 'onColumnWidthChange', 'flattenColumns', 'getRowKey', 'expandedKeys', @@ -104,7 +104,7 @@ function Body(props: BodyProps) { )} diff --git a/src/Header/HeaderCell.tsx b/src/Header/HeaderCell.tsx new file mode 100644 index 000000000..890320f56 --- /dev/null +++ b/src/Header/HeaderCell.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import type { CellProps } from '../Cell'; +import Cell from '../Cell'; +import useCellResize from './useCellResize'; + +interface HeaderCellProps extends CellProps { + isFixLeft: boolean; + isFixRight: boolean; + columnKey?: React.Key; + resizable?: boolean; + minWidth?: number; +} + +function HeaderCell({ + isFixLeft, + isFixRight, + columnKey, + resizable, + minWidth, + ...cellProps +}: HeaderCellProps) { + const { prefixCls } = cellProps; + + const cellPrefixCls = `${prefixCls}-cell`; + + const resizeHandleNode = useCellResize( + columnKey, + isFixLeft, + isFixRight, + cellPrefixCls, + resizable, + minWidth, + ); + + return ; +} + +export default HeaderCell; diff --git a/src/Header/HeaderRow.tsx b/src/Header/HeaderRow.tsx index fe7629c64..5834f04b9 100644 --- a/src/Header/HeaderRow.tsx +++ b/src/Header/HeaderRow.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import Cell from '../Cell'; import TableContext from '../context/TableContext'; import { useContext } from '@rc-component/context'; import type { @@ -11,9 +10,10 @@ import type { } from '../interface'; import { getCellFixedInfo } from '../utils/fixUtil'; import { getColumnsKey } from '../utils/valueUtil'; +import HeaderCell from './HeaderCell'; export interface RowProps { - cells: readonly CellType[]; + cells: CellType[]; stickyOffsets: StickyOffsets; flattenColumns: readonly ColumnType[]; rowComponent: CustomizeComponent; @@ -32,7 +32,11 @@ const HeaderRow = (props: RowProps) => { onHeaderRow, index, } = props; - const { prefixCls, direction } = useContext(TableContext, ['prefixCls', 'direction']); + const { prefixCls, direction, supportSticky } = useContext(TableContext, [ + 'prefixCls', + 'direction', + 'supportSticky', + ]); let rowProps: React.HTMLAttributes; if (onHeaderRow) { rowProps = onHeaderRow( @@ -59,9 +63,34 @@ const HeaderRow = (props: RowProps) => { if (column && column.onHeaderCell) { additionalProps = cell.column.onHeaderCell(column); } + const isFixLeft = typeof fixedInfo.fixLeft === 'number' && supportSticky; + const isFixRight = typeof fixedInfo.fixRight === 'number' && supportSticky; + // If scrollbar cell is not fixed right, and the previous cell of the scrollbar is resizable, then the scrollbar is resizable + const isScrollBarCellAndResizable = + column.scrollbar && + // if scrollbar fixed right, the resize handle of previous cell is on the left, so there is no need to put the handle inside the scrollbar + (direction === 'rtl' ? !isFixLeft : !isFixRight) && + (cells[cells.length - 2].column as ColumnType).resizable; + + // Whether this cell is in the previous cell of the scrollbar + const isScrollBarPreviousCell = + cells[cells.length - 1].column.scrollbar && cellIndex === cells.length - 2; + + let resizable: boolean; + // ltr: If it is the column before the scrollbar and fixed right, resizable is required. + // rtl: If it is the column before the scrollbar and fixed left, resizable is required. + if (isScrollBarPreviousCell) { + if (direction === 'rtl' ? isFixLeft : isFixRight) { + resizable = (column as ColumnType).resizable; + } else { + resizable = false; + } + } else { + resizable = isScrollBarCellAndResizable || (column as ColumnType).resizable; + } return ( - 1 ? 'colgroup' : 'col') : null} ellipsis={column.ellipsis} @@ -72,6 +101,15 @@ const HeaderRow = (props: RowProps) => { {...fixedInfo} additionalProps={additionalProps} rowType="header" + columnKey={ + isScrollBarCellAndResizable + ? columnsKey[columnsKey.length - 2] + : columnsKey[cellIndex] + } + isFixLeft={isFixLeft} + isFixRight={isFixRight} + resizable={resizable} + minWidth={(column as ColumnType).minWidth} /> ); })} diff --git a/src/Header/useCellResize.tsx b/src/Header/useCellResize.tsx new file mode 100644 index 000000000..9bb8ba0b0 --- /dev/null +++ b/src/Header/useCellResize.tsx @@ -0,0 +1,140 @@ +import TableContext from '../context/TableContext'; +import { useContext } from '@rc-component/context'; +import { useEvent } from 'rc-util'; +import React, { useEffect, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; + +export default function useCellResize( + columnKey: React.Key, + isFixLeft: boolean, + isFixRight: boolean, + cellPrefixCls: string, + resizable?: boolean, + minWidth: number = 0, +) { + const { + direction, + colsWidths, + colsKeys, + colWidths, + componentWidth, + fullTableRef, + scrollbarSize, + onColumnResizeComplete, + onResizingChange, + } = useContext(TableContext, [ + 'direction', + 'colWidths', + 'colsKeys', + 'colsWidths', + 'componentWidth', + 'fullTableRef', + 'scrollbarSize', + 'onColumnResizeComplete', + 'onResizingChange', + ]); + const [isResizing, setIsResizing] = useState(false); + const [lineLeft, setLineLeft] = useState(0); + const lineStartLeftRef = useRef(0); + const startRealWidth = useRef(0); + const startPageX = useRef(0); + const mouseMoveRef = useRef<(event: MouseEvent) => void>(null); + const mouseUpRef = useRef<(event: MouseEvent) => void>(null); + + // handle position + const isRightHandle = direction === 'rtl' ? isFixLeft : !isFixRight; + + const removeResizeListener = () => { + document.body.removeEventListener('mousemove', mouseMoveRef.current); + document.body.removeEventListener('mouseup', mouseUpRef.current); + }; + + useEffect(() => removeResizeListener, []); + + const onResize = useEvent((event: MouseEvent, isResizeEnd?: boolean) => { + const offset = event.pageX - startPageX.current; + const oldWidth = colsWidths.get(columnKey); + let newWidth = startRealWidth.current + (isRightHandle ? offset : -offset); + + if (newWidth < minWidth) { + newWidth = minWidth; + } + setLineLeft( + lineStartLeftRef.current + + (isRightHandle ? newWidth - startRealWidth.current : startRealWidth.current - newWidth), + ); + + if (isResizeEnd) { + const totalWidth = colWidths.reduce((total, width) => total + width, 0); + const smallThanWidth = componentWidth - scrollbarSize - (totalWidth - oldWidth + newWidth); + // If it is less than the width of the table, the remaining width will be allocated to the column on the right. + // If there is no column on the right, it will be allocated to the column on the left. + let addWidthColumnKey: React.Key; + const isDecreasingWidth = oldWidth - newWidth > 0; + if (smallThanWidth > 0 && isDecreasingWidth) { + const index = colsKeys.findIndex(key => key === columnKey); + addWidthColumnKey = colsKeys[index + 1] ?? colsKeys[index - 1]; + } + + const columnWidthsMap = new Map(colsWidths); + columnWidthsMap.set(columnKey, newWidth); + if (addWidthColumnKey) { + const addWidthColumnNewWidth = colsWidths.get(addWidthColumnKey) + smallThanWidth; + columnWidthsMap.set(addWidthColumnKey, addWidthColumnNewWidth); + } + const columnWidths = Array.from(columnWidthsMap).map(([key, width]) => ({ + columnKey: key, + width, + })); + onColumnResizeComplete?.({ columnKey, width: newWidth, columnWidths }); + } + }); + + const onResizeEnd = (event: MouseEvent) => { + setIsResizing(false); + onResizingChange(false); + removeResizeListener(); + onResize(event, true); + }; + + const onResizeStart = (event: React.MouseEvent) => { + // Block selected text + event.preventDefault(); + const left = + (event.target as HTMLElement).parentElement.getBoundingClientRect()[ + isRightHandle ? 'right' : 'left' + ] - fullTableRef.current.getBoundingClientRect().left; + setLineLeft(left); + lineStartLeftRef.current = left; + startRealWidth.current = colsWidths.get(columnKey); + startPageX.current = event.pageX; + document.body.addEventListener('mousemove', onResize); + document.body.addEventListener('mouseup', onResizeEnd); + mouseMoveRef.current = onResize; + mouseUpRef.current = onResizeEnd; + onResizingChange(true); + setIsResizing(true); + }; + + const resizeHandleNode = resizable && ( + <> +
+ {isResizing && + createPortal( +
, + fullTableRef.current, + )} + + ); + + return resizeHandleNode; +} diff --git a/src/Table.tsx b/src/Table.tsx index 54b39833b..ab5584039 100644 --- a/src/Table.tsx +++ b/src/Table.tsx @@ -126,6 +126,11 @@ export interface TableProps // Events onScroll?: React.UIEventHandler; + onColumnResizeComplete?: (info: { + columnKey: React.Key; + width: number; + columnWidths: { columnKey: React.Key; width: number }[]; + }) => void; // =================================== Internal =================================== /** @@ -212,6 +217,7 @@ function Table( // Events onScroll, + onColumnResizeComplete, // Internal internalHooks, @@ -353,9 +359,11 @@ function Table( const [pingedLeft, setPingedLeft] = React.useState(false); const [pingedRight, setPingedRight] = React.useState(false); const [colsWidths, updateColsWidths] = useLayoutState(new Map()); + const [isResizing, setIsResizing] = React.useState(false); // Convert map to number width - const colsKeys = getColumnsKey(flattenColumns); + const pureColsKeys = getColumnsKey(flattenColumns); + const colsKeys = React.useMemo(() => pureColsKeys, [pureColsKeys.join('_')]); const pureColWidths = colsKeys.map(columnKey => colsWidths.get(columnKey)); const colWidths = React.useMemo(() => pureColWidths, [pureColWidths.join('_')]); const stickyOffsets = useStickyOffsets(colWidths, flattenColumns, direction); @@ -405,7 +413,7 @@ function Table( }; } - const onColumnResize = React.useCallback((columnKey: React.Key, width: number) => { + const onColumnWidthChange = React.useCallback((columnKey: React.Key, width: number) => { if (isVisible(fullTableRef.current)) { updateColsWidths(widths => { if (widths.get(columnKey) !== width) { @@ -776,6 +784,7 @@ function Table( [`${prefixCls}-has-fix-right`]: flattenColumns[flattenColumns.length - 1] && flattenColumns[flattenColumns.length - 1].fixed === 'right', + [`${prefixCls}-column-resizing`]: isResizing, })} style={style} id={id} @@ -832,7 +841,7 @@ function Table( // Column columns, flattenColumns, - onColumnResize, + onColumnWidthChange, // Row hoverStartRow: startRow, @@ -846,6 +855,12 @@ function Table( childrenColumnName: mergedChildrenColumnName, rowHoverable, + fullTableRef, + colsWidths, + colsKeys, + colWidths, + onColumnResizeComplete, + onResizingChange: setIsResizing, }), [ // Scroll @@ -859,6 +874,7 @@ function Table( fixedInfoList, isSticky, supportSticky, + fullTableRef, componentWidth, fixHeader, @@ -881,7 +897,12 @@ function Table( // Column columns, flattenColumns, - onColumnResize, + onColumnWidthChange, + colsWidths, + colsKeys, + colWidths, + onColumnResizeComplete, + setIsResizing, // Row startRow, diff --git a/src/VirtualTable/BodyGrid.tsx b/src/VirtualTable/BodyGrid.tsx index 8c57c5dcc..efae064fe 100644 --- a/src/VirtualTable/BodyGrid.tsx +++ b/src/VirtualTable/BodyGrid.tsx @@ -23,7 +23,7 @@ const Grid = React.forwardRef((props, ref) => { const { flattenColumns, - onColumnResize, + onColumnWidthChange, getRowKey, expandedKeys, prefixCls, @@ -31,7 +31,7 @@ const Grid = React.forwardRef((props, ref) => { scrollX, } = useContext(TableContext, [ 'flattenColumns', - 'onColumnResize', + 'onColumnWidthChange', 'getRowKey', 'prefixCls', 'expandedKeys', @@ -68,7 +68,7 @@ const Grid = React.forwardRef((props, ref) => { React.useEffect(() => { columnsWidth.forEach(([key, width]) => { - onColumnResize(key, width); + onColumnWidthChange(key, width); }); }, [columnsWidth]); diff --git a/src/context/TableContext.tsx b/src/context/TableContext.tsx index fd810b378..7fee91193 100644 --- a/src/context/TableContext.tsx +++ b/src/context/TableContext.tsx @@ -55,7 +55,7 @@ export interface TableContextProps { // Column columns: ColumnsType; flattenColumns: readonly ColumnType[]; - onColumnResize: (columnKey: React.Key, width: number) => void; + onColumnWidthChange: (columnKey: React.Key, width: number) => void; // Row hoverStartRow: number; @@ -68,6 +68,16 @@ export interface TableContextProps { childrenColumnName: string; rowHoverable?: boolean; + fullTableRef: React.MutableRefObject; + colsWidths: Map; + colWidths: number[]; + colsKeys: React.Key[]; + onColumnResizeComplete?: (info: { + columnKey: React.Key; + width: number; + columnWidths: { columnKey: React.Key; width: number }[]; + }) => void; + onResizingChange: (value: boolean) => void; } const TableContext = createContext(); diff --git a/src/interface.ts b/src/interface.ts index 051ac57ae..5b20aa1de 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -50,7 +50,7 @@ export interface CellType { className?: string; style?: React.CSSProperties; children?: React.ReactNode; - column?: ColumnsType[number]; + column?: ColumnsType[number] & { scrollbar?: boolean }; colSpan?: number; rowSpan?: number; @@ -114,6 +114,7 @@ export interface ColumnType extends ColumnSharedType { rowSpan?: number; width?: number | string; minWidth?: number; + resizable?: boolean; onCell?: GetComponentProps; /** @deprecated Please use `onCell` instead */ onCellClick?: (record: RecordType, e: React.MouseEvent) => void; diff --git a/tests/Resizable.spec.jsx b/tests/Resizable.spec.jsx new file mode 100644 index 000000000..272bc89bb --- /dev/null +++ b/tests/Resizable.spec.jsx @@ -0,0 +1,335 @@ +import { mount } from 'enzyme'; +import Table from '../src'; +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import RcResizeObserver, { _rs } from 'rc-resize-observer'; +import { safeAct } from './utils'; +import { spyElementPrototype } from 'rc-util/lib/test/domHook'; + +describe('Table.resizable', () => { + let domSpy; + let containerSpy; + + beforeAll(() => { + domSpy = spyElementPrototype(HTMLElement, 'offsetParent', { + get: () => ({}), + }); + containerSpy = spyElementPrototype(HTMLDivElement, 'offsetWidth', { + get: () => 800, + }); + }); + + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterAll(() => { + domSpy.mockRestore(); + containerSpy.mockRestore(); + }); + + it('change width in onColumnResizeComplete', async () => { + const onColumnResizeComplete = vi.fn(); + + const App = () => { + const [widthMap, setWidthMap] = React.useState(new Map()); + + const columns = [ + { key: 'a', dataIndex: 'a', width: 400, resizable: true }, + { key: 'b', dataIndex: 'b', width: 400, resizable: true }, + ].map(col => ({ ...col, width: widthMap.get(col.key ?? col.dataIndex) || col.width })); + + return ( +
t + c.width, 0) }} + onColumnResizeComplete={info => { + setWidthMap(prev => { + const result = new Map(prev); + info.columnWidths.forEach(i => { + result.set(i.columnKey, i.width); + }); + return result; + }); + onColumnResizeComplete(info); + }} + /> + ); + }; + const wrapper = mount(); + + async function triggerResize(resizeList) { + wrapper.find(RcResizeObserver.Collection).first().props().onBatchResize(resizeList); + await safeAct(wrapper); + wrapper.update(); + } + + await triggerResize([ + { + data: wrapper.find('ResizeObserver').at(1).props().data, + size: { width: 400, offsetWidth: 400 }, + }, + { + data: wrapper.find('ResizeObserver').at(2).props().data, + size: { width: 400, offsetWidth: 400 }, + }, + ]); + + wrapper.find('.rc-table-cell-resize-handle').first().simulate('mousedown', { pageX: 0 }); + + const mousemoveEvent = new Event('mousemove'); + mousemoveEvent.pageX = 100; + + await act(async () => { + document.body.dispatchEvent(mousemoveEvent); + await Promise.resolve(); + wrapper.update(); + }); + + const mouseupEvent = new Event('mouseup'); + mouseupEvent.pageX = 100; + + await act(async () => { + document.body.dispatchEvent(mouseupEvent); + await Promise.resolve(); + wrapper.update(); + }); + + expect(onColumnResizeComplete).toHaveBeenCalledWith({ + columnKey: 'a', + width: 500, + columnWidths: [ + { columnKey: 'a', width: 500 }, + { columnKey: 'b', width: 400 }, + ], + }); + + expect(wrapper.find('colgroup col').at(0).props().style.width).toBe(500); + expect(wrapper.find('colgroup col').at(1).props().style.width).toBe(400); + }); + + it('columns total width < componentWidth', async () => { + const onColumnResizeComplete = vi.fn(); + + const App = () => { + const [widthMap, setWidthMap] = React.useState(new Map()); + + const columns = [ + { key: 'a', dataIndex: 'a', width: 100, resizable: true }, + { key: 'b', dataIndex: 'b', width: 100, resizable: true }, + ].map(col => ({ ...col, width: widthMap.get(col.key ?? col.dataIndex) || col.width || 100 })); + + return ( +
t + c.width, 0) }} + onColumnResizeComplete={info => { + setWidthMap(prev => { + const result = new Map(prev); + info.columnWidths.forEach(i => { + result.set(i.columnKey, i.width); + }); + return result; + }); + onColumnResizeComplete(info); + }} + /> + ); + }; + const wrapper = mount(); + + async function triggerResize(resizeList) { + wrapper.find(RcResizeObserver.Collection).first().props().onBatchResize(resizeList); + await safeAct(wrapper); + wrapper.update(); + } + + wrapper.find(RcResizeObserver).first().props().onResize({ width: 800 }); + + await triggerResize([ + { + data: wrapper.find('ResizeObserver').at(1).props().data, + size: { width: 400, offsetWidth: 400 }, + }, + { + data: wrapper.find('ResizeObserver').at(2).props().data, + size: { width: 400, offsetWidth: 400 }, + }, + ]); + + wrapper.find('.rc-table-cell-resize-handle').first().simulate('mousedown', { pageX: 0 }); + + const mousemoveEvent = new Event('mousemove'); + mousemoveEvent.pageX = -100; + + await act(async () => { + document.body.dispatchEvent(mousemoveEvent); + await Promise.resolve(); + wrapper.update(); + }); + + const mouseupEvent = new Event('mouseup'); + mouseupEvent.pageX = -100; + + await act(async () => { + document.body.dispatchEvent(mouseupEvent); + await Promise.resolve(); + wrapper.update(); + }); + + expect(onColumnResizeComplete).toHaveBeenCalledWith({ + columnKey: 'a', + width: 300, + columnWidths: [ + { columnKey: 'a', width: 300 }, + // scrollBarSize = 15px + { columnKey: 'b', width: 485 }, + ], + }); + + expect(wrapper.find('colgroup col').at(0).props().style.width).toBe(300); + expect(wrapper.find('colgroup col').at(1).props().style.width).toBe(485); + }); + + it('minWidth should be worked', async () => { + const onColumnResizeComplete = vi.fn(); + + const App = () => { + const [widthMap, setWidthMap] = React.useState(new Map()); + + const columns = [ + { key: 'a', dataIndex: 'a', width: 800, resizable: true, minWidth: 400 }, + { key: 'b', dataIndex: 'b', width: 800, resizable: true }, + ].map(col => ({ ...col, width: widthMap.get(col.key ?? col.dataIndex) || col.width })); + + return ( +
t + c.width, 0) }} + onColumnResizeComplete={info => { + setWidthMap(prev => { + const result = new Map(prev); + info.columnWidths.forEach(i => { + result.set(i.columnKey, i.width); + }); + return result; + }); + onColumnResizeComplete(info); + }} + /> + ); + }; + const wrapper = mount(); + + async function triggerResize(resizeList) { + wrapper.find(RcResizeObserver.Collection).first().props().onBatchResize(resizeList); + await safeAct(wrapper); + wrapper.update(); + } + + await triggerResize([ + { + data: wrapper.find('ResizeObserver').at(1).props().data, + size: { width: 800, offsetWidth: 800 }, + }, + { + data: wrapper.find('ResizeObserver').at(2).props().data, + size: { width: 800, offsetWidth: 800 }, + }, + ]); + + wrapper.find('.rc-table-cell-resize-handle').first().simulate('mousedown', { pageX: 0 }); + + const mousemoveEvent = new Event('mousemove'); + mousemoveEvent.pageX = -1000; + + await act(async () => { + document.body.dispatchEvent(mousemoveEvent); + await Promise.resolve(); + wrapper.update(); + }); + + const mouseupEvent = new Event('mouseup'); + mouseupEvent.pageX = -1000; + + await act(async () => { + document.body.dispatchEvent(mouseupEvent); + await Promise.resolve(); + wrapper.update(); + }); + + expect(onColumnResizeComplete).toHaveBeenCalledWith({ + columnKey: 'a', + width: 400, + columnWidths: [ + { columnKey: 'a', width: 400 }, + { columnKey: 'b', width: 800 }, + ], + }); + + expect(wrapper.find('colgroup col').at(0).props().style.width).toBe(400); + expect(wrapper.find('colgroup col').at(1).props().style.width).toBe(800); + }); + + it('resize handle should in header right scrollbar when last column is not fixed', async () => { + const App = () => { + const columns = [ + { key: 'a', dataIndex: 'a', width: 800, resizable: true }, + { key: 'b', dataIndex: 'b', width: 800, resizable: true }, + ]; + + return ( +
+ ); + }; + const wrapper = mount(); + + expect( + wrapper.find('.rc-table-cell-scrollbar .rc-table-cell-resize-handle').exists(), + ).toBeTruthy(); + expect( + wrapper + .find('.rc-table-thead .rc-table-cell') + .at(1) + .find('.rc-table-cell-resize-handle') + .exists(), + ).toBeFalsy(); + }); + + it('resize handle should not in header right scrollbar when last column is fixed right', async () => { + const App = () => { + const columns = [ + { key: 'a', dataIndex: 'a', width: 800, resizable: true }, + { key: 'b', dataIndex: 'b', width: 800, resizable: true, fixed: 'right' }, + ]; + + return ( +
+ ); + }; + const wrapper = mount(); + + expect( + wrapper.find('.rc-table-cell-scrollbar .rc-table-cell-resize-handle').exists(), + ).toBeFalsy(); + expect( + wrapper + .find('.rc-table-thead .rc-table-cell') + .at(1) + .find('.rc-table-cell-resize-handle') + .exists(), + ).toBeTruthy(); + }); +});