From 3c31a22ba02dc5e5f28e80fd89d3a6d80bb063df Mon Sep 17 00:00:00 2001 From: Sandro Date: Thu, 29 Oct 2026 17:12:03 +0100 Subject: [PATCH] fix(capture): + button opens the capture sheet again in triage and tasks Hold-to-record opened the sheet on pointerup; the click the browser sends right after landed on the new sheet's backdrop and closed it at once. Lists and notes have no such backdrop and kept working. A short tap now opens on the click itself (as before), and after a hold that click is ignored. Tests send the real pointerup + click sequence. Version 0.5.1. Co-Authored-By: Claude Opus 5.5 --- frontend/package-lock.json | 4 ++-- frontend/package.json | 2 +- .../src/features/capture/HoldToRecordFab.tsx | 17 +++++++++++---- .../__tests__/HoldToRecordFab.test.tsx | 21 +++++++++++++++++++ 4 files changed, 37 insertions(+), 7 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 910ea3d..1cd3571 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mindsift-frontend", - "version": "0.5.0", + "version": "0.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mindsift-frontend", - "version": "0.5.0", + "version": "0.5.1", "license": "Unlicense", "dependencies": { "@types/prismjs": "^1.26.6", diff --git a/frontend/package.json b/frontend/package.json index 80fd9d3..33c9558 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "mindsift-frontend", "private": true, - "version": "0.5.0", + "version": "0.5.1", "license": "Unlicense", "type": "module", "scripts": { diff --git a/frontend/src/features/capture/HoldToRecordFab.tsx b/frontend/src/features/capture/HoldToRecordFab.tsx index 1b82f88..eb4deab 100644 --- a/frontend/src/features/capture/HoldToRecordFab.tsx +++ b/frontend/src/features/capture/HoldToRecordFab.tsx @@ -19,6 +19,8 @@ export const HoldToRecordFab: React.FC = ({ onTap, onTrans const [toastMessage, setToastMessage] = useState(null); const isPressedRef = useRef(false); + // A press that turned into a hold must not also count as a tap when the browser sends its click + const suppressClickRef = useRef(false); const didEnterHoldRef = useRef(false); const fallbackHandledRef = useRef(false); const isCancelZoneRef = useRef(false); @@ -187,6 +189,7 @@ export const HoldToRecordFab: React.FC = ({ onTap, onTrans clearStopTimer(); isPressedRef.current = true; + suppressClickRef.current = false; didEnterHoldRef.current = false; fallbackHandledRef.current = false; isCancelZoneRef.current = false; @@ -196,6 +199,7 @@ export const HoldToRecordFab: React.FC = ({ onTap, onTrans holdTimerRef.current = null; if (!isPressedRef.current) return; didEnterHoldRef.current = true; + suppressClickRef.current = true; startVoiceRecording(); }, HOLD_DELAY_MS); }; @@ -223,7 +227,8 @@ export const HoldToRecordFab: React.FC = ({ onTap, onTrans clearHoldTimer(); if (!didEnterHoldRef.current) { - onTap(); + // A short tap opens on the click that follows. Opening here already would let that click land + // on the freshly opened sheet's backdrop and close it again at once. return; } @@ -340,9 +345,13 @@ export const HoldToRecordFab: React.FC = ({ onTap, onTrans onPointerUp={handlePointerUp} onPointerCancel={handlePointerCancel} onContextMenu={(e) => e.preventDefault()} - // Keyboard, screen readers and switch control produce a click without pointer events (detail 0) - onClick={(e) => { - if (e.detail === 0) onTap(); + // Taps, keyboard, screen readers and switch control all end in a click; after a hold it is ignored + onClick={() => { + if (suppressClickRef.current) { + suppressClickRef.current = false; + return; + } + onTap(); }} className={ isRecording diff --git a/frontend/src/features/capture/__tests__/HoldToRecordFab.test.tsx b/frontend/src/features/capture/__tests__/HoldToRecordFab.test.tsx index 4fe1494..be531f8 100644 --- a/frontend/src/features/capture/__tests__/HoldToRecordFab.test.tsx +++ b/frontend/src/features/capture/__tests__/HoldToRecordFab.test.tsx @@ -74,6 +74,7 @@ describe('HoldToRecordFab', () => { vi.advanceTimersByTime(150); }); fireEvent.pointerUp(btn, { clientY: 600 }); + fireEvent.click(btn); expect(onTap).toHaveBeenCalledTimes(1); expect(onTranscript).not.toHaveBeenCalled(); @@ -109,6 +110,7 @@ describe('HoldToRecordFab', () => { await act(async () => { fireEvent.pointerUp(btn, { clientY: 600 }); + fireEvent.click(btn); }); expect(rec.stopped).toBe(true); @@ -143,6 +145,8 @@ describe('HoldToRecordFab', () => { fireEvent.pointerUp(btn, { clientY: 520 }); + fireEvent.click(btn); + expect(rec.aborted).toBe(true); expect(onTap).not.toHaveBeenCalled(); expect(onTranscript).not.toHaveBeenCalled(); @@ -177,6 +181,7 @@ describe('HoldToRecordFab', () => { await act(async () => { fireEvent.pointerUp(btn, { clientY: 580 }); + fireEvent.click(btn); }); expect(rec.aborted).toBe(false); @@ -198,6 +203,7 @@ describe('HoldToRecordFab', () => { }); await act(async () => { fireEvent.pointerUp(btn, { clientY: 600 }); + fireEvent.click(btn); }); expect(onTap).not.toHaveBeenCalled(); @@ -224,6 +230,7 @@ describe('HoldToRecordFab', () => { rec.emitError('not-allowed'); }); fireEvent.pointerUp(btn, { clientY: 600 }); + fireEvent.click(btn); expect(onTap).toHaveBeenCalledTimes(1); expect(onTranscript).not.toHaveBeenCalled(); @@ -247,6 +254,7 @@ describe('HoldToRecordFab', () => { vi.advanceTimersByTime(350); }); fireEvent.pointerUp(btn, { clientY: 600 }); + fireEvent.click(btn); expect(onTap).toHaveBeenCalledTimes(1); expect(onTranscript).not.toHaveBeenCalled(); @@ -256,6 +264,19 @@ describe('HoldToRecordFab', () => { }); }); +describe('HoldToRecordFab tap timing', () => { + it('opens only on the click after the release, so that click cannot close the new sheet', () => { + const onTap = vi.fn(); + render(); + const btn = screen.getByRole('button'); + fireEvent.pointerDown(btn, { clientY: 600, button: 0 }); + fireEvent.pointerUp(btn, { clientY: 600 }); + expect(onTap).not.toHaveBeenCalled(); + fireEvent.click(btn); + expect(onTap).toHaveBeenCalledTimes(1); + }); +}); + describe('HoldToRecordFab keyboard access', () => { it('opens the capture sheet when activated without a pointer (keyboard, screen reader)', () => { const onTap = vi.fn();