diff --git a/README.md b/README.md index 8d23a487f..b0885d23c 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 | | set column to be resizable | | 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 e2810cc23..aabd0afde 100644 --- a/assets/index.less +++ b/assets/index.less @@ -55,6 +55,10 @@ } } + &-column-resizing { + cursor: col-resize; + } + // ================== Cell ================== &-cell { background: #f4f4f4; @@ -64,6 +68,26 @@ z-index: calc(2 + var(--z-offset)); } + &-resize-handle { + position: absolute; + top: 0; + width: 4px; + height: 100%; + cursor: col-resize; + background: red; + z-index: 1; + } + + &-resize-line { + position: absolute; + width: 4px; + background: red; + height: 100%; + top: 0; + transform: translateX(-50%); + z-index: 100; + } + &-fix-start-shadow, &-fix-end-shadow { &::after { @@ -75,6 +99,7 @@ opacity: 0; content: ''; transition: opacity 0.3s; + pointer-events: none; } &-show::after { diff --git a/docs/examples/column-resize.tsx b/docs/examples/column-resize.tsx index a3f15cbbc..8e753c47d 100644 --- a/docs/examples/column-resize.tsx +++ b/docs/examples/column-resize.tsx @@ -1,95 +1,113 @@ -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'; +import { useCheckbox } from './utils/useInput'; -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 [isRtl, isRtlProps] = useCheckbox(false); + + 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', width: 200 }, + { 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', minWidth: 50 }, + ].map(i => ({ + ...i, + resizable: true, + width: widthMap.get(i.key ?? i.dataIndex) ?? i.width ?? 150, + })) as ColumnType[]; return ( - - - +
+ table width: 800px {'columns=[{width: 100, width: 100}]'} 情况 + t + (c.width as number), 0) }} + columns={columns1} + data={data} + onColumnResizeEnd={({ 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; + }} + /> +
+ Fixed Columns + +
t + (c.width as number), 0) }} + columns={columns2} + data={data} + onColumnResizeEnd={({ 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 1dea3059d..a07c00d0d 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,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 b3015c5f2..39c2c0685 100644 --- a/src/Body/MeasureCell.tsx +++ b/src/Body/MeasureCell.tsx @@ -4,15 +4,15 @@ import useLayoutEffect from '@rc-component/util/lib/hooks/useLayoutEffect'; 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(); useLayoutEffect(() => { 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 d5b38b0e0..b3c113d5c 100644 --- a/src/Body/MeasureRow.tsx +++ b/src/Body/MeasureRow.tsx @@ -5,11 +5,15 @@ import isVisible from '@rc-component/util/lib/Dom/isVisible'; 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) { const ref = React.useRef(null); return ( @@ -23,13 +27,17 @@ export default function MeasureRow({ prefixCls, columnsKey, onColumnResize }: Me onBatchResize={infoList => { if (isVisible(ref.current)) { 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 aec12e9d4..6489d9d81 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', @@ -103,7 +103,7 @@ function Body(props: BodyProps) { )} diff --git a/src/FixedHolder/index.tsx b/src/FixedHolder/index.tsx index f2a4a6331..bc75d4435 100644 --- a/src/FixedHolder/index.tsx +++ b/src/FixedHolder/index.tsx @@ -109,7 +109,7 @@ const FixedHolder = React.forwardRef>((pro return () => { scrollRef.current?.removeEventListener('wheel', onWheel); }; - }, []); + }, [direction]); // Check if all flattenColumns has width const allFlattenColumnsWithWidth = React.useMemo( diff --git a/src/Header/HeaderCell.tsx b/src/Header/HeaderCell.tsx new file mode 100644 index 000000000..d7ed13bab --- /dev/null +++ b/src/Header/HeaderCell.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; +import type { CellProps } from '../Cell'; +import Cell from '../Cell'; +import useCellResize from './useCellResize'; + +interface HeaderCellProps extends CellProps { + columnKey?: React.Key; + resizable?: boolean; + minWidth?: number; + isScrollBarPreviousCell?: boolean; +} + +function HeaderCell({ + columnKey, + resizable, + minWidth, + isScrollBarPreviousCell, + ...cellProps +}: HeaderCellProps) { + const resizeHandleNode = useCellResize( + columnKey, + typeof cellProps.fixEnd === 'number', + cellProps.prefixCls, + isScrollBarPreviousCell, + resizable, + minWidth, + ); + + return ; +} + +export default HeaderCell; diff --git a/src/Header/HeaderRow.tsx b/src/Header/HeaderRow.tsx index fe83ac5ed..25f3dd40e 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,6 +10,7 @@ import type { } from '../interface'; import { getCellFixedInfo } from '../utils/fixUtil'; import { getColumnsKey } from '../utils/valueUtil'; +import HeaderCell from './HeaderCell'; export interface RowProps { cells: readonly CellType[]; @@ -59,8 +59,12 @@ const HeaderRow = (props: RowProps) => { additionalProps = cell.column.onHeaderCell(column); } + // Whether this cell is in the previous cell of the scrollbar + const isScrollBarPreviousCell = + cells[cells.length - 1]?.column.scrollbar && cellIndex === cells.length - 2; + return ( - 1 ? 'colgroup' : 'col') : null} ellipsis={column.ellipsis} @@ -71,6 +75,10 @@ const HeaderRow = (props: RowProps) => { {...fixedInfo} additionalProps={additionalProps} rowType="header" + columnKey={columnsKey[cellIndex]} + isScrollBarPreviousCell={isScrollBarPreviousCell} + resizable={column.scrollbar ? false : (column as ColumnType).resizable} + minWidth={(column as ColumnType).minWidth} /> ); })} diff --git a/src/Header/useCellResize.tsx b/src/Header/useCellResize.tsx new file mode 100644 index 000000000..20952522f --- /dev/null +++ b/src/Header/useCellResize.tsx @@ -0,0 +1,155 @@ +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, + isFixEnd: boolean, + prefixCls: string, + isScrollBarPreviousCell?: boolean, + resizable?: boolean, + minWidth: number = 0, +) { + const cellPrefixCls = `${prefixCls}-cell`; + + const { + direction, + colsWidths, + colsKeys, + colWidths, + componentWidth, + fullTableRef, + scrollbarSize, + onColumnResizeEnd, + onResizingChange, + } = useContext(TableContext, [ + 'direction', + 'colWidths', + 'colsKeys', + 'colsWidths', + 'componentWidth', + 'fullTableRef', + 'scrollbarSize', + 'onColumnResizeEnd', + '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); + + const isRtl = direction === 'rtl'; + // inline-end + const isEndHandle = !isFixEnd; + // right + const isRightHandle = isRtl ? isFixEnd : isEndHandle; + + const removeResizeListener = () => { + document.removeEventListener('mousemove', mouseMoveRef.current); + document.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 increaseWidthColumnKey: React.Key; + const isDecreaseWidth = oldWidth - newWidth > 0; + if (smallThanWidth > 0 && isDecreaseWidth) { + const index = colsKeys.findIndex(key => key === columnKey); + increaseWidthColumnKey = colsKeys[index + 1] ?? colsKeys[index - 1]; + } + + const columnWidthsMap = new Map(colsWidths); + columnWidthsMap.set(columnKey, newWidth); + if (increaseWidthColumnKey) { + const addWidthColumnNewWidth = colsWidths.get(increaseWidthColumnKey) + smallThanWidth; + columnWidthsMap.set(increaseWidthColumnKey, addWidthColumnNewWidth); + } + const columnWidths = Array.from(columnWidthsMap).map(([key, width]) => ({ + columnKey: key, + width, + })); + + onColumnResizeEnd?.({ columnKey, width: newWidth, columnWidths }); + } + }); + + const onResizeEnd = (event: MouseEvent) => { + setIsResizing(false); + onResizingChange(false); + removeResizeListener(); + onResize(event, true); + }; + + const onResizeStart = (event: React.MouseEvent) => { + removeResizeListener(); + // Prevent selected text + event.preventDefault(); + const scrollBarOffset = isScrollBarPreviousCell && isEndHandle ? scrollbarSize : 0; + const left = + (event.target as HTMLElement).parentElement.getBoundingClientRect()[ + isRightHandle ? 'right' : 'left' + ] - + fullTableRef.current.getBoundingClientRect().left + + (isRtl ? -scrollBarOffset : scrollBarOffset); + setLineLeft(left); + lineStartLeftRef.current = left; + startRealWidth.current = colsWidths.get(columnKey); + startPageX.current = event.pageX; + document.addEventListener('mousemove', onResize); + document.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 299a48b35..4e20da726 100644 --- a/src/Table.tsx +++ b/src/Table.tsx @@ -126,6 +126,11 @@ export interface TableProps // Events onScroll?: React.UIEventHandler; + onColumnResizeEnd?: (info: { + columnKey: React.Key; + width: number; + columnWidths: { columnKey: React.Key; width: number }[]; + }) => void; // =================================== Internal =================================== /** @@ -212,6 +217,7 @@ function Table( // Events onScroll, + onColumnResizeEnd, // Internal internalHooks, @@ -350,9 +356,11 @@ function Table( const [shadowStart, setShadowStart] = React.useState(false); const [shadowEnd, setShadowEnd] = React.useState(false); const [colsWidths, updateColsWidths] = React.useState(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); @@ -402,7 +410,7 @@ function Table( }; } - const onColumnResize = React.useCallback((columnKey: React.Key, width: number) => { + const onColumnWidthChange = React.useCallback((columnKey: React.Key, width: number) => { updateColsWidths(widths => { if (widths.get(columnKey) !== width) { const newWidths = new Map(widths); @@ -784,6 +792,7 @@ function Table( [`${prefixCls}-scroll-horizontal`]: horizonScroll, [`${prefixCls}-has-fix-start`]: flattenColumns[0]?.fixed, [`${prefixCls}-has-fix-end`]: flattenColumns[flattenColumns.length - 1]?.fixed === 'end', + [`${prefixCls}-column-resizing`]: isResizing, })} style={tableStyle} id={id} @@ -840,7 +849,7 @@ function Table( // Column columns, flattenColumns, - onColumnResize, + onColumnWidthChange, // Row hoverStartRow: startRow, @@ -854,6 +863,12 @@ function Table( childrenColumnName: mergedChildrenColumnName, rowHoverable, + fullTableRef, + colsWidths, + colsKeys, + colWidths, + onColumnResizeEnd, + onResizingChange: setIsResizing, }), [ // Scroll @@ -867,6 +882,7 @@ function Table( direction, fixedInfoList, isSticky, + fullTableRef, componentWidth, fixHeader, @@ -889,7 +905,12 @@ function Table( // Column columns, flattenColumns, - onColumnResize, + onColumnWidthChange, + colsWidths, + colsKeys, + colWidths, + onColumnResizeEnd, + setIsResizing, // Row startRow, diff --git a/src/VirtualTable/BodyGrid.tsx b/src/VirtualTable/BodyGrid.tsx index cc1548cc6..7d4780e25 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, @@ -32,7 +32,7 @@ const Grid = React.forwardRef((props, ref) => { direction, } = useContext(TableContext, [ 'flattenColumns', - 'onColumnResize', + 'onColumnWidthChange', 'getRowKey', 'prefixCls', 'expandedKeys', @@ -70,7 +70,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 cce41f90b..b9c8dee83 100644 --- a/src/context/TableContext.tsx +++ b/src/context/TableContext.tsx @@ -57,7 +57,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; @@ -70,6 +70,16 @@ export interface TableContextProps { childrenColumnName: string; rowHoverable?: boolean; + fullTableRef: React.MutableRefObject; + colsWidths: Map; + colWidths: number[]; + colsKeys: React.Key[]; + onColumnResizeEnd?: (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 54ebfcd97..d8294d578 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -53,7 +53,7 @@ export interface CellType { className?: string; style?: React.CSSProperties; children?: React.ReactNode; - column?: ColumnsType[number]; + column?: ColumnsType[number] & { scrollbar?: boolean }; colSpan?: number; rowSpan?: number; @@ -117,6 +117,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.tsx b/tests/Resizable.spec.tsx new file mode 100644 index 000000000..737a11113 --- /dev/null +++ b/tests/Resizable.spec.tsx @@ -0,0 +1,221 @@ +import Table from '../src'; +import React from 'react'; +import { render, fireEvent, act, createEvent } from '@testing-library/react'; +import { _rs } from '@rc-component/resize-observer'; +import { spyElementPrototype } from '@rc-component/util/lib/test/domHook'; + +async function triggerResize(ele: Element) { + await act(async () => { + _rs([{ target: ele }] as any); + await Promise.resolve(); + }); +} + +function doMouseMove(element: Element, start: number, end: number, fireMouseUp = true) { + const mouseDown = createEvent.mouseDown(element, { + pageX: start, + }); + Object.defineProperties(mouseDown, { + pageX: { get: () => start }, + pageY: { get: () => start }, + }); + + fireEvent(element, mouseDown); + + // Drag + if (start !== end) { + const mouseMove: any = new Event('mousemove'); + mouseMove.pageX = end; + + fireEvent(document, mouseMove); + } + + if (fireMouseUp) { + const mouseUp: any = new Event('mouseup'); + mouseUp.pageX = end; + fireEvent(document, mouseUp); + } +} + +describe('Table.resizable', () => { + let domSpy; + let containerSpy; + let measureCellSpy; + + beforeAll(() => { + domSpy = spyElementPrototype(HTMLElement, 'offsetParent', { + get: () => ({}), + }); + containerSpy = spyElementPrototype(HTMLDivElement, 'offsetWidth', { + get: () => 800, + }); + measureCellSpy = spyElementPrototype(HTMLTableCellElement, 'offsetWidth', { + get: () => 400, + }); + }); + + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterAll(() => { + domSpy.mockRestore(); + containerSpy.mockRestore(); + measureCellSpy.mockRestore(); + }); + + it('change width in onColumnResizeEnd', async () => { + const onColumnResizeEnd = 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) }} + onColumnResizeEnd={info => { + setWidthMap(prev => { + const result = new Map(prev); + info.columnWidths.forEach(i => { + result.set(i.columnKey, i.width); + }); + return result; + }); + onColumnResizeEnd(info); + }} + /> + ); + }; + const { container } = render(); + + await triggerResize(container.querySelector('.rc-table')); + + doMouseMove(container.querySelectorAll('.rc-table-cell-resize-handle')[0], 0, 100); + + expect(onColumnResizeEnd).toHaveBeenCalledWith({ + columnKey: 'a', + width: 500, + columnWidths: [ + { columnKey: 'a', width: 500 }, + { columnKey: 'b', width: 400 }, + ], + }); + + const measureCells = container.querySelectorAll('.rc-table-measure-row td'); + + Object.defineProperty(measureCells[0], 'offsetWidth', { + value: 500, + }); + await triggerResize(measureCells[0]); + + doMouseMove(container.querySelectorAll('.rc-table-cell-resize-handle')[1], 0, 100); + + expect(onColumnResizeEnd).toHaveBeenCalledWith({ + columnKey: 'b', + width: 500, + columnWidths: [ + { columnKey: 'a', width: 500 }, + { columnKey: 'b', width: 500 }, + ], + }); + }); + + it('columns total width < componentWidth', async () => { + const onColumnResizeEnd = 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) }} + onColumnResizeEnd={info => { + setWidthMap(prev => { + const result = new Map(prev); + info.columnWidths.forEach(i => { + result.set(i.columnKey, i.width); + }); + return result; + }); + onColumnResizeEnd(info); + }} + /> + ); + }; + const { container } = render(); + + await triggerResize(container.querySelector('.rc-table')); + + doMouseMove(container.querySelectorAll('.rc-table-cell-resize-handle')[0], 100, 0); + + expect(onColumnResizeEnd).toHaveBeenCalledWith({ + columnKey: 'a', + width: 300, + columnWidths: [ + { columnKey: 'a', width: 300 }, + { columnKey: 'b', width: 485 }, + // scrollbar 15px + ], + }); + }); + + it('minWidth should be worked', async () => { + const onColumnResizeEnd = vi.fn(); + + const App = () => { + const [widthMap, setWidthMap] = React.useState(new Map()); + + const columns = [ + { key: 'a', dataIndex: 'a', width: 400, resizable: true, minWidth: 200 }, + { 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) }} + onColumnResizeEnd={info => { + setWidthMap(prev => { + const result = new Map(prev); + info.columnWidths.forEach(i => { + result.set(i.columnKey, i.width); + }); + return result; + }); + onColumnResizeEnd(info); + }} + /> + ); + }; + const { container } = render(); + + await triggerResize(container.querySelector('.rc-table')); + + doMouseMove(container.querySelectorAll('.rc-table-cell-resize-handle')[0], 300, 0); + + expect(onColumnResizeEnd).toHaveBeenCalledWith({ + columnKey: 'a', + width: 200, + columnWidths: [ + { columnKey: 'a', width: 200 }, + { columnKey: 'b', width: 585 }, + // scrollbar 15px + ], + }); + }); +});