From d93f6cb0b9af87a9a460f6044582da07084c6c64 Mon Sep 17 00:00:00 2001 From: xiong Date: Thu, 23 Jul 2026 09:42:33 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=89=8B=E6=9C=BA?= =?UTF-8?q?=E7=AB=AF=E5=8F=8C=E5=87=BB=E7=BC=96=E8=BE=91=E5=8D=95=E5=85=83?= =?UTF-8?q?=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../editable-cell.integration.test.ts | 79 +++++++++++++++++++ .../src/components/EditableCell/index.tsx | 43 +++++++++- 2 files changed, 121 insertions(+), 1 deletion(-) diff --git a/apps/admin/src/components/EditableCell/editable-cell.integration.test.ts b/apps/admin/src/components/EditableCell/editable-cell.integration.test.ts index d1a7099..e97fe18 100644 --- a/apps/admin/src/components/EditableCell/editable-cell.integration.test.ts +++ b/apps/admin/src/components/EditableCell/editable-cell.integration.test.ts @@ -13,6 +13,21 @@ let root: ReturnType | null = null; 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 () => { if (root) { await act(async () => root?.unmount()); @@ -51,6 +66,70 @@ describe('editable cell value mapping', () => { }); 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 () => { const onSave = vi.fn(async () => undefined); container = document.createElement('div'); diff --git a/apps/admin/src/components/EditableCell/index.tsx b/apps/admin/src/components/EditableCell/index.tsx index 7e83f98..58ad4c7 100644 --- a/apps/admin/src/components/EditableCell/index.tsx +++ b/apps/admin/src/components/EditableCell/index.tsx @@ -94,6 +94,8 @@ const EditableCell = ({ const { hasPermission } = usePermission(); const idRef = useRef(crypto.randomUUID()); const rootRef = useRef(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 [saving, setSaving] = useState(false); const [draft, setDraft] = useState(() => @@ -200,6 +202,39 @@ const EditableCell = ({ setEditing(true); }; + const onPointerDown = (event: React.PointerEvent) => { + if (event.pointerType !== 'touch' || editing) return; + touchStartRef.current = { + pointerId: event.pointerId, + x: event.clientX, + y: event.clientY, + }; + }; + + const onPointerUp = (event: React.PointerEvent) => { + 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) => { if (event.key === 'Escape') { event.preventDefault(); @@ -264,11 +299,17 @@ const EditableCell = ({ ref={rootRef} className={`editable-cell${enabled ? ' editable-cell--enabled' : ''}${editing ? ' editable-cell--editing' : ''}`} onDoubleClick={() => void beginEdit()} + onPointerDown={onPointerDown} + onPointerUp={onPointerUp} + onPointerCancel={() => { + touchStartRef.current = null; + lastTouchTapRef.current = null; + }} > {editing ? ( {control} ) : ( - {children} + {children} )} ); From fa7ed8e128bc87bbfee38100ebbb37a55c83e413 Mon Sep 17 00:00:00 2001 From: xiong Date: Thu, 23 Jul 2026 11:22:31 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat=EF=BC=9A=E7=A7=BB=E9=99=A4=E8=A1=A8?= =?UTF-8?q?=E6=A0=BC=E5=86=85=E9=83=A8=E7=9A=84=E6=BB=9A=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/admin/src/index.css | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/admin/src/index.css b/apps/admin/src/index.css index 6c0fd57..8cd0068 100644 --- a/apps/admin/src/index.css +++ b/apps/admin/src/index.css @@ -188,8 +188,7 @@ canvas { max-width: 100%; } -.ant-table-wrapper .ant-table-container { - overflow-x: auto; +.ant-table-wrapper .ant-table-content { overscroll-behavior-inline: contain; -webkit-overflow-scrolling: touch; } From 5bd63846d10a386246d21868b29a013be2e1a0b1 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Thu, 23 Jul 2026 11:22:36 +0800 Subject: [PATCH 3/3] fix: prevent touch double-tap zoom --- apps/admin/src/components/EditableCell/style.css | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/admin/src/components/EditableCell/style.css b/apps/admin/src/components/EditableCell/style.css index e47adf1..9e55823 100644 --- a/apps/admin/src/components/EditableCell/style.css +++ b/apps/admin/src/components/EditableCell/style.css @@ -7,6 +7,7 @@ .editable-cell--enabled { cursor: cell; + touch-action: manipulation; padding: 4px 6px; margin: -4px -6px; border: 1px solid transparent;