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();