forked from wangziqi/gongxue-base
Merge pull request '修复手机端双击编辑单元格' (#40) from xiongyuxing/gongxue-base:main into main
This commit is contained in:
@@ -13,6 +13,21 @@ let root: ReturnType<typeof createRoot> | null = null;
|
|||||||
|
|
||||||
const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
|
const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
const dispatchPointer = (
|
||||||
|
target: Element,
|
||||||
|
type: 'pointerdown' | 'pointerup',
|
||||||
|
init: { pointerType: 'touch' | 'mouse'; pointerId?: number; clientX?: number; clientY?: number },
|
||||||
|
) => {
|
||||||
|
const event = new Event(type, { bubbles: true });
|
||||||
|
Object.defineProperties(event, {
|
||||||
|
pointerType: { value: init.pointerType },
|
||||||
|
pointerId: { value: init.pointerId ?? 1 },
|
||||||
|
clientX: { value: init.clientX ?? 0 },
|
||||||
|
clientY: { value: init.clientY ?? 0 },
|
||||||
|
});
|
||||||
|
target.dispatchEvent(event);
|
||||||
|
};
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
if (root) {
|
if (root) {
|
||||||
await act(async () => root?.unmount());
|
await act(async () => root?.unmount());
|
||||||
@@ -51,6 +66,70 @@ describe('editable cell value mapping', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('editable cell interactions', () => {
|
describe('editable cell interactions', () => {
|
||||||
|
const renderTextCell = async () => {
|
||||||
|
const onSave = vi.fn(async () => undefined);
|
||||||
|
container = document.createElement('div');
|
||||||
|
document.body.appendChild(container);
|
||||||
|
root = createRoot(container);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root?.render(
|
||||||
|
React.createElement(EditableCell, { value: '张三', onSave }, '张三'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return container.querySelector('.editable-cell') as HTMLElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
it('keeps a single touch tap read-only', async () => {
|
||||||
|
const cell = await renderTextCell();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
dispatchPointer(cell, 'pointerdown', { pointerType: 'touch', clientX: 10, clientY: 10 });
|
||||||
|
dispatchPointer(cell, 'pointerup', { pointerType: 'touch', clientX: 12, clientY: 11 });
|
||||||
|
await flush();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(container?.querySelector('.editable-cell--editing')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('enters edit mode after two nearby touch taps', async () => {
|
||||||
|
const cell = await renderTextCell();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
dispatchPointer(cell, 'pointerdown', { pointerType: 'touch', clientX: 10, clientY: 10 });
|
||||||
|
dispatchPointer(cell, 'pointerup', { pointerType: 'touch', clientX: 11, clientY: 10 });
|
||||||
|
dispatchPointer(cell, 'pointerdown', { pointerType: 'touch', clientX: 12, clientY: 11 });
|
||||||
|
dispatchPointer(cell, 'pointerup', { pointerType: 'touch', clientX: 12, clientY: 11 });
|
||||||
|
await flush();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(container?.querySelector('.editable-cell--editing')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps mouse single-click behavior read-only', async () => {
|
||||||
|
const cell = await renderTextCell();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
dispatchPointer(cell, 'pointerdown', { pointerType: 'mouse' });
|
||||||
|
dispatchPointer(cell, 'pointerup', { pointerType: 'mouse' });
|
||||||
|
await flush();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(container?.querySelector('.editable-cell--editing')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not edit when a touch gesture scrolls the table', async () => {
|
||||||
|
const cell = await renderTextCell();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
dispatchPointer(cell, 'pointerdown', { pointerType: 'touch', clientX: 10, clientY: 10 });
|
||||||
|
dispatchPointer(cell, 'pointerup', { pointerType: 'touch', clientX: 30, clientY: 10 });
|
||||||
|
await flush();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(container?.querySelector('.editable-cell--editing')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it('saves a single-select value immediately when an option is clicked', async () => {
|
it('saves a single-select value immediately when an option is clicked', async () => {
|
||||||
const onSave = vi.fn(async () => undefined);
|
const onSave = vi.fn(async () => undefined);
|
||||||
container = document.createElement('div');
|
container = document.createElement('div');
|
||||||
|
|||||||
@@ -94,6 +94,8 @@ const EditableCell = <Value,>({
|
|||||||
const { hasPermission } = usePermission();
|
const { hasPermission } = usePermission();
|
||||||
const idRef = useRef(crypto.randomUUID());
|
const idRef = useRef(crypto.randomUUID());
|
||||||
const rootRef = useRef<HTMLDivElement>(null);
|
const rootRef = useRef<HTMLDivElement>(null);
|
||||||
|
const touchStartRef = useRef<{ pointerId: number; x: number; y: number } | null>(null);
|
||||||
|
const lastTouchTapRef = useRef<{ time: number; x: number; y: number } | null>(null);
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [draft, setDraft] = useState<unknown>(() =>
|
const [draft, setDraft] = useState<unknown>(() =>
|
||||||
@@ -200,6 +202,39 @@ const EditableCell = <Value,>({
|
|||||||
setEditing(true);
|
setEditing(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onPointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
|
||||||
|
if (event.pointerType !== 'touch' || editing) return;
|
||||||
|
touchStartRef.current = {
|
||||||
|
pointerId: event.pointerId,
|
||||||
|
x: event.clientX,
|
||||||
|
y: event.clientY,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerUp = (event: React.PointerEvent<HTMLDivElement>) => {
|
||||||
|
const touchStart = touchStartRef.current;
|
||||||
|
touchStartRef.current = null;
|
||||||
|
if (!touchStart || event.pointerType !== 'touch' || event.pointerId !== touchStart.pointerId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const moved = Math.hypot(event.clientX - touchStart.x, event.clientY - touchStart.y);
|
||||||
|
if (moved > 8) {
|
||||||
|
lastTouchTapRef.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const lastTap = lastTouchTapRef.current;
|
||||||
|
const isDoubleTap =
|
||||||
|
!!lastTap &&
|
||||||
|
now - lastTap.time <= 450 &&
|
||||||
|
Math.hypot(event.clientX - lastTap.x, event.clientY - lastTap.y) <= 24;
|
||||||
|
lastTouchTapRef.current = isDoubleTap
|
||||||
|
? null
|
||||||
|
: { time: now, x: event.clientX, y: event.clientY };
|
||||||
|
if (isDoubleTap) void beginEdit();
|
||||||
|
};
|
||||||
|
|
||||||
const onKeyDown = async (event: React.KeyboardEvent) => {
|
const onKeyDown = async (event: React.KeyboardEvent) => {
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -264,11 +299,17 @@ const EditableCell = <Value,>({
|
|||||||
ref={rootRef}
|
ref={rootRef}
|
||||||
className={`editable-cell${enabled ? ' editable-cell--enabled' : ''}${editing ? ' editable-cell--editing' : ''}`}
|
className={`editable-cell${enabled ? ' editable-cell--enabled' : ''}${editing ? ' editable-cell--editing' : ''}`}
|
||||||
onDoubleClick={() => void beginEdit()}
|
onDoubleClick={() => void beginEdit()}
|
||||||
|
onPointerDown={onPointerDown}
|
||||||
|
onPointerUp={onPointerUp}
|
||||||
|
onPointerCancel={() => {
|
||||||
|
touchStartRef.current = null;
|
||||||
|
lastTouchTapRef.current = null;
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{editing ? (
|
{editing ? (
|
||||||
<Spin spinning={saving}>{control}</Spin>
|
<Spin spinning={saving}>{control}</Spin>
|
||||||
) : (
|
) : (
|
||||||
<Tooltip title={enabled ? '双击编辑' : undefined}>{children}</Tooltip>
|
<Tooltip title={enabled ? '双击编辑,触屏双击编辑' : undefined}>{children}</Tooltip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -188,8 +188,7 @@ canvas {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-table-wrapper .ant-table-container {
|
.ant-table-wrapper .ant-table-content {
|
||||||
overflow-x: auto;
|
|
||||||
overscroll-behavior-inline: contain;
|
overscroll-behavior-inline: contain;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user