Skip to content

Commit b1af826

Browse files
committed
fix(ui): 列拖拽相邻增量与键盘微调(REQ-285)
1 parent ddf3cb6 commit b1af826

2 files changed

Lines changed: 87 additions & 4 deletions

File tree

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// @vitest-environment jsdom
2+
/**
3+
* ColumnResizer.test.tsx — 拖拽增量语义回归测试(REQ-285,v0.19.6)。
4+
*
5+
* @ai-context: 覆盖根因「加速」:旧实现每次 move 传距起点累计位移,而调用方
6+
* resizeBy 为增量语义(cur + delta)→ 重复累加。断言 move 回调
7+
* 收到**相邻增量**(50→70→80 = [20, 10]),up 后不再发;side="left"
8+
* 符号反向;键盘 ←/→ 步进(Shift=8px)与双击复位(§2.9 矩阵)。
9+
*/
10+
import { afterEach, describe, expect, it, vi } from "vitest";
11+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
12+
import ColumnResizer from "./ColumnResizer";
13+
14+
afterEach(cleanup);
15+
16+
describe("ColumnResizer 增量拖拽(REQ-285)", () => {
17+
it("move 序列发出相邻增量而非累计位移(右拖宽)", () => {
18+
const onResize = vi.fn();
19+
render(<ColumnResizer onResize={onResize} />);
20+
const handle = screen.getByTestId("column-resizer");
21+
fireEvent.pointerDown(handle, { clientX: 50 });
22+
fireEvent.pointerMove(handle, { clientX: 70 });
23+
fireEvent.pointerMove(handle, { clientX: 80 });
24+
fireEvent.pointerUp(handle);
25+
// 旧实现(累计):[20, 30] → 加速;修复后相邻增量:[20, 10]
26+
expect(onResize.mock.calls.map((c) => c[0])).toEqual([20, 10]);
27+
});
28+
29+
it("pointerUp 后不再响应 move(拖拽结束)", () => {
30+
const onResize = vi.fn();
31+
render(<ColumnResizer onResize={onResize} />);
32+
const handle = screen.getByTestId("column-resizer");
33+
fireEvent.pointerDown(handle, { clientX: 50 });
34+
fireEvent.pointerUp(handle);
35+
fireEvent.pointerMove(handle, { clientX: 90 });
36+
expect(onResize).not.toHaveBeenCalled();
37+
});
38+
39+
it("side=left 方向反向(拖左手柄收窄自身列)", () => {
40+
const onResize = vi.fn();
41+
render(<ColumnResizer side="left" onResize={onResize} />);
42+
const handle = screen.getByTestId("column-resizer");
43+
fireEvent.pointerDown(handle, { clientX: 50 });
44+
fireEvent.pointerMove(handle, { clientX: 70 });
45+
fireEvent.pointerUp(handle);
46+
expect(onResize.mock.calls.map((c) => c[0])).toEqual([-20]);
47+
});
48+
49+
it("键盘 ←/→ 步进 ±16px(Shift=±8px)——键盘可达性(§2.9)", () => {
50+
const onResize = vi.fn();
51+
render(<ColumnResizer onResize={onResize} />);
52+
const handle = screen.getByTestId("column-resizer");
53+
fireEvent.keyDown(handle, { key: "ArrowRight" });
54+
fireEvent.keyDown(handle, { key: "ArrowLeft" });
55+
fireEvent.keyDown(handle, { key: "ArrowRight", shiftKey: true });
56+
expect(onResize.mock.calls.map((c) => c[0])).toEqual([16, -16, 8]);
57+
});
58+
59+
it("双击触发复位(onReset)", () => {
60+
const onReset = vi.fn();
61+
render(<ColumnResizer onResize={vi.fn()} onReset={onReset} />);
62+
fireEvent.doubleClick(screen.getByTestId("column-resizer"));
63+
expect(onReset).toHaveBeenCalledTimes(1);
64+
});
65+
});

‎app/src/components/ColumnResizer.tsx‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,22 @@ interface Props {
2020
export default function ColumnResizer({ side = "right", onResize, onReset }: Props) {
2121
const [hover, setHover] = useState(false);
2222
const [dragging, setDragging] = useState(false);
23-
const startXRef = useRef(0);
23+
// REQ-285(v0.19.6):上次事件 x——move 传**相邻增量**而非距起点累计位移。
24+
// @ai-context: useColumnLayout.resizeBy 为增量语义(cur + delta);旧实现每次
25+
// move 都传全程距离(clientX - startX)导致重复累加 =「拖拽加速」。
26+
const lastXRef = useRef(0);
2427

2528
const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
2629
e.preventDefault();
27-
startXRef.current = e.clientX;
30+
lastXRef.current = e.clientX;
2831
try { e.currentTarget.setPointerCapture(e.pointerId); } catch { /* 旧 WebView 无捕获——move 仍走元素事件 */ }
2932
setDragging(true);
3033
};
3134

3235
const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
3336
if (!dragging) return;
34-
const dx = e.clientX - startXRef.current;
37+
const dx = e.clientX - lastXRef.current;
38+
lastXRef.current = e.clientX;
3539
onResize(side === "right" ? dx : -dx);
3640
};
3741

@@ -41,17 +45,31 @@ export default function ColumnResizer({ side = "right", onResize, onReset }: Pro
4145
setDragging(false);
4246
};
4347

48+
// REQ-285(v0.19.6):键盘可达性(§2.9 交互矩阵)——←/→ 步进 ±16px(Shift=±8px)。
49+
// @ai-context: 与 pointer 拖拽同走 onResize 增量语义;夹取由 useColumnLayout 负责。
50+
const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
51+
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
52+
e.preventDefault();
53+
const step = e.shiftKey ? 8 : 16;
54+
const dx = e.key === "ArrowRight" ? step : -step;
55+
onResize(side === "right" ? dx : -dx);
56+
};
57+
4458
return (
4559
<div
4660
data-testid="column-resizer"
61+
role="separator"
62+
aria-orientation="vertical"
63+
tabIndex={0}
4764
onPointerDown={onPointerDown}
4865
onPointerMove={onPointerMove}
4966
onPointerUp={endDrag}
5067
onPointerCancel={endDrag}
68+
onKeyDown={onKeyDown}
5169
onPointerEnter={() => setHover(true)}
5270
onPointerLeave={() => setHover(false)}
5371
onDoubleClick={() => onReset?.()}
54-
title="拖拽调整宽度(双击恢复默认)"
72+
title="拖拽调整宽度(←/→ 微调;双击恢复默认)"
5573
style={{
5674
width: 5,
5775
flexShrink: 0,

0 commit comments

Comments
 (0)