From d922b5448af73a5daeb64c4b6186d80435c60b5a Mon Sep 17 00:00:00 2001 From: cdevq <124263330+cdevq@users.noreply.github.com> Date: Tue, 10 Nov 2026 10:25:50 +0800 Subject: [PATCH] =?UTF-8?q?fix(Table):=20=E4=BF=AE=E5=A4=8D=E5=88=86?= =?UTF-8?q?=E7=BB=84=E8=A1=A8=E5=A4=B4=E8=BE=B9=E7=95=8C=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E5=88=97=E5=AE=BD=E6=97=B6=E6=8A=A5=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../table/__tests__/column-resize.test.tsx | 211 ++++++++++++++++++ .../table/hooks/useColumnResize.tsx | 13 +- 2 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 packages/components/table/__tests__/column-resize.test.tsx diff --git a/packages/components/table/__tests__/column-resize.test.tsx b/packages/components/table/__tests__/column-resize.test.tsx new file mode 100644 index 0000000000..0804a1fe57 --- /dev/null +++ b/packages/components/table/__tests__/column-resize.test.tsx @@ -0,0 +1,211 @@ +import React from 'react'; +import { createEvent, fireEvent, mockElementSizes, render, vi } from '@test/utils'; + +import { Table } from '..'; + +const columns = [ + { colKey: 'category', title: '普通列', width: 100 }, + { + colKey: 'metrics', + title: '分组表头', + children: [ + { colKey: 'valueA', title: '子列 A', width: 100 }, + { colKey: 'valueB', title: '子列 B', width: 100 }, + ], + }, +]; +const data = [{ id: 1, category: '示例 A', valueA: 10, valueB: 20 }]; + +function fireMouseEvent( + target: Element | Document, + type: 'mouseMove' | 'mouseDown' | 'mouseUp', + x: number, + buttons = 0, +) { + const event = createEvent[type](target, { + button: 0, + buttons, + clientX: x, + clientY: 20, + }); + // jsdom 的 MouseEvent 没有 pageX 和 x,补齐浏览器提供的拖动坐标。 + Object.defineProperties(event, { pageX: { value: x }, x: { value: x } }); + fireEvent(target, event); +} + +describe('Table', () => { + describe('scenarios', () => { + describe('resizable', () => { + let restoreSizes: () => void; + + beforeEach(() => { + vi.useFakeTimers(); + restoreSizes = mockElementSizes([ + { selector: '.t-table', width: 300, height: 200 }, + { selector: '.t-table__content', width: 300, height: 200 }, + { selector: 'th[data-colkey="category"]', width: 100, height: 80 }, + { + selector: 'th[data-colkey="metrics"]', + width: 200, + height: 40, + left: 100, + }, + { + selector: 'th[data-colkey="valueA"]', + width: 100, + height: 40, + left: 100, + top: 40, + }, + { + selector: 'th[data-colkey="valueB"]', + width: 100, + height: 40, + left: 200, + top: 40, + }, + ]); + }); + + afterEach(() => { + vi.clearAllTimers(); + vi.useRealTimers(); + vi.restoreAllMocks(); + restoreSizes(); + }); + + it.each([true, false])('不应在相邻分组表头上启动拖动(mousemove: %s)', (shouldMoveOverGroup) => { + const onColumnResizeChange = vi.fn(); + const { container } = render( + , + ); + const leaf = container.querySelector('th[data-colkey="category"]'); + const group = container.querySelector('th[data-colkey="metrics"]'); + const originalSelectStart = document.onselectstart; + const originalDragStart = document.ondragstart; + + fireMouseEvent(leaf, 'mouseMove', 99); + expect(leaf).toHaveStyle({ cursor: 'col-resize' }); + if (shouldMoveOverGroup) { + fireMouseEvent(group, 'mouseMove', 101); + } + + const addListener = vi.spyOn(document, 'addEventListener'); + try { + fireMouseEvent(group, 'mouseDown', 101, 1); + expect(addListener.mock.calls.filter(([type]) => type === 'mouseup' || type === 'mousemove')).toHaveLength(0); + fireMouseEvent(document, 'mouseUp', 119); + expect(onColumnResizeChange).not.toHaveBeenCalled(); + expect(document.onselectstart).toBe(originalSelectStart); + expect(document.ondragstart).toBe(originalDragStart); + } finally { + // 修复前运行此用例时也清理已注册的拖动事件,避免影响其他用例。 + addListener.mock.calls.forEach(([type, listener, options]) => { + if (type === 'mouseup' || type === 'mousemove') document.removeEventListener(type, listener, options); + }); + addListener.mockRestore(); + document.onselectstart = originalSelectStart; + document.ondragstart = originalDragStart; + } + }); + + it('保留分组表头的自定义光标', () => { + const { container } = render( +
, + ); + const leaf = container.querySelector('th[data-colkey="category"]'); + const group = container.querySelector('th[data-colkey="metrics"]'); + expect(group).toHaveStyle({ cursor: 'pointer' }); + fireMouseEvent(leaf, 'mouseMove', 99); + fireMouseEvent(group, 'mouseMove', 101); + expect(group).toHaveStyle({ cursor: 'pointer' }); + }); + + it.each([ + { + name: '右边缘拖动', + colKey: 'category', + start: 99, + end: 119, + moveOverGroup: false, + widths: { category: 120, metrics: 200, valueA: 80, valueB: 100 }, + }, + { + name: '左边缘拖动', + colKey: 'valueB', + start: 201, + end: 221, + moveOverGroup: false, + widths: { category: 100, metrics: 200, valueA: 120, valueB: 80 }, + }, + { + name: '已开始拖动后跨过分组表头', + colKey: 'category', + start: 99, + end: 119, + moveOverGroup: true, + widths: { category: 120, metrics: 200, valueA: 80, valueB: 100 }, + }, + ])('$name仍可正常调整列宽', ({ colKey, start, end, moveOverGroup, widths }) => { + const onColumnResizeChange = vi.fn(); + const { container } = render( +
, + ); + const leaf = container.querySelector(`th[data-colkey="${colKey}"]`); + const group = container.querySelector('th[data-colkey="metrics"]'); + + fireMouseEvent(leaf, 'mouseMove', start); + fireMouseEvent(leaf, 'mouseDown', start, 1); + fireMouseEvent(moveOverGroup ? group : document, 'mouseMove', end, 1); + fireMouseEvent(document, 'mouseUp', end); + + expect(onColumnResizeChange).toHaveBeenCalledTimes(1); + expect(onColumnResizeChange).toHaveBeenCalledWith({ + columnsWidth: widths, + }); + }); + + it('右侧固定列激活时仍可从左边缘调整自身宽度', () => { + vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockReturnValue(300); + vi.spyOn(HTMLElement.prototype, 'scrollWidth', 'get').mockReturnValue(400); + const onColumnResizeChange = vi.fn(); + const { container } = render( +
, + ); + const fixed = container.querySelector('th[data-colkey="valueB"]'); + + fireMouseEvent(fixed, 'mouseMove', 201); + fireMouseEvent(fixed, 'mouseDown', 201, 1); + fireMouseEvent(document, 'mouseMove', 181, 1); + fireMouseEvent(document, 'mouseUp', 181); + + expect(onColumnResizeChange).toHaveBeenCalledTimes(1); + expect(onColumnResizeChange).toHaveBeenCalledWith({ + columnsWidth: { + category: 100, + metrics: 200, + valueA: 100, + valueB: 120, + }, + }); + }); + }); + }); +}); diff --git a/packages/components/table/hooks/useColumnResize.tsx b/packages/components/table/hooks/useColumnResize.tsx index 194c34cc89..679bdc2ebb 100644 --- a/packages/components/table/hooks/useColumnResize.tsx +++ b/packages/components/table/hooks/useColumnResize.tsx @@ -127,7 +127,11 @@ export default function useColumnResize(params: { if (!target) return; // 判断是否为叶子阶段,仅叶子结点允许拖拽 const colKey = target.getAttribute('data-colkey'); - if (!leafColumns.find((t) => t.colKey === colKey)) return; + if (!leafColumns.find((t) => t.colKey === colKey)) { + resizeLineParams.draggingCol = null; + resizeLineParams.effectCol = null; + return; + } const targetBoundRect = target.getBoundingClientRect(); const thRightCursor = targetBoundRect.right - e.pageX <= distance; const thLeftCursor = e.pageX - targetBoundRect.left <= distance; @@ -230,7 +234,12 @@ export default function useColumnResize(params: { col: BaseTableCol, index: number, ) => { - if (e.button === CONTEXTMENU || !resizeLineParams.draggingCol) return; + if ( + e.button === CONTEXTMENU || + !resizeLineParams.draggingCol || + !leafColumns.some((item) => item.colKey === col.colKey) + ) + return; const target = resizeLineParams.draggingCol; const targetBoundRect = target.getBoundingClientRect(); const tableBoundRect = tableContentRef.current?.getBoundingClientRect();