fix(capture): + button opens the capture sheet again in triage and tasks
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m59s

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 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-29 17:12:03 +01:00
1 parent fcdf44a0ad
commit 3c31a22ba0
4 files changed
+37 -7

No files matched your search

+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "mindsift-frontend", "name": "mindsift-frontend",
"version": "0.5.0", "version": "0.5.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mindsift-frontend", "name": "mindsift-frontend",
"version": "0.5.0", "version": "0.5.1",
"license": "Unlicense", "license": "Unlicense",
"dependencies": { "dependencies": {
"@types/prismjs": "^1.26.6", "@types/prismjs": "^1.26.6",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "mindsift-frontend", "name": "mindsift-frontend",
"private": true, "private": true,
"version": "0.5.0", "version": "0.5.1",
"license": "Unlicense", "license": "Unlicense",
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -19,6 +19,8 @@ export const HoldToRecordFab: React.FC<HoldToRecordFabProps> = ({ onTap, onTrans
const [toastMessage, setToastMessage] = useState<string | null>(null); const [toastMessage, setToastMessage] = useState<string | null>(null);
const isPressedRef = useRef(false); 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 didEnterHoldRef = useRef(false);
const fallbackHandledRef = useRef(false); const fallbackHandledRef = useRef(false);
const isCancelZoneRef = useRef(false); const isCancelZoneRef = useRef(false);
@@ -187,6 +189,7 @@ export const HoldToRecordFab: React.FC<HoldToRecordFabProps> = ({ onTap, onTrans
clearStopTimer(); clearStopTimer();
isPressedRef.current = true; isPressedRef.current = true;
suppressClickRef.current = false;
didEnterHoldRef.current = false; didEnterHoldRef.current = false;
fallbackHandledRef.current = false; fallbackHandledRef.current = false;
isCancelZoneRef.current = false; isCancelZoneRef.current = false;
@@ -196,6 +199,7 @@ export const HoldToRecordFab: React.FC<HoldToRecordFabProps> = ({ onTap, onTrans
holdTimerRef.current = null; holdTimerRef.current = null;
if (!isPressedRef.current) return; if (!isPressedRef.current) return;
didEnterHoldRef.current = true; didEnterHoldRef.current = true;
suppressClickRef.current = true;
startVoiceRecording(); startVoiceRecording();
}, HOLD_DELAY_MS); }, HOLD_DELAY_MS);
}; };
@@ -223,7 +227,8 @@ export const HoldToRecordFab: React.FC<HoldToRecordFabProps> = ({ onTap, onTrans
clearHoldTimer(); clearHoldTimer();
if (!didEnterHoldRef.current) { 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; return;
} }
@@ -340,9 +345,13 @@ export const HoldToRecordFab: React.FC<HoldToRecordFabProps> = ({ onTap, onTrans
onPointerUp={handlePointerUp} onPointerUp={handlePointerUp}
onPointerCancel={handlePointerCancel} onPointerCancel={handlePointerCancel}
onContextMenu={(e) => e.preventDefault()} onContextMenu={(e) => e.preventDefault()}
// Keyboard, screen readers and switch control produce a click without pointer events (detail 0) // Taps, keyboard, screen readers and switch control all end in a click; after a hold it is ignored
onClick={(e) => { onClick={() => {
if (e.detail === 0) onTap(); if (suppressClickRef.current) {
suppressClickRef.current = false;
return;
}
onTap();
}} }}
className={ className={
isRecording isRecording
@@ -74,6 +74,7 @@ describe('HoldToRecordFab', () => {
vi.advanceTimersByTime(150); vi.advanceTimersByTime(150);
}); });
fireEvent.pointerUp(btn, { clientY: 600 }); fireEvent.pointerUp(btn, { clientY: 600 });
fireEvent.click(btn);
expect(onTap).toHaveBeenCalledTimes(1); expect(onTap).toHaveBeenCalledTimes(1);
expect(onTranscript).not.toHaveBeenCalled(); expect(onTranscript).not.toHaveBeenCalled();
@@ -109,6 +110,7 @@ describe('HoldToRecordFab', () => {
await act(async () => { await act(async () => {
fireEvent.pointerUp(btn, { clientY: 600 }); fireEvent.pointerUp(btn, { clientY: 600 });
fireEvent.click(btn);
}); });
expect(rec.stopped).toBe(true); expect(rec.stopped).toBe(true);
@@ -143,6 +145,8 @@ describe('HoldToRecordFab', () => {
fireEvent.pointerUp(btn, { clientY: 520 }); fireEvent.pointerUp(btn, { clientY: 520 });
fireEvent.click(btn);
expect(rec.aborted).toBe(true); expect(rec.aborted).toBe(true);
expect(onTap).not.toHaveBeenCalled(); expect(onTap).not.toHaveBeenCalled();
expect(onTranscript).not.toHaveBeenCalled(); expect(onTranscript).not.toHaveBeenCalled();
@@ -177,6 +181,7 @@ describe('HoldToRecordFab', () => {
await act(async () => { await act(async () => {
fireEvent.pointerUp(btn, { clientY: 580 }); fireEvent.pointerUp(btn, { clientY: 580 });
fireEvent.click(btn);
}); });
expect(rec.aborted).toBe(false); expect(rec.aborted).toBe(false);
@@ -198,6 +203,7 @@ describe('HoldToRecordFab', () => {
}); });
await act(async () => { await act(async () => {
fireEvent.pointerUp(btn, { clientY: 600 }); fireEvent.pointerUp(btn, { clientY: 600 });
fireEvent.click(btn);
}); });
expect(onTap).not.toHaveBeenCalled(); expect(onTap).not.toHaveBeenCalled();
@@ -224,6 +230,7 @@ describe('HoldToRecordFab', () => {
rec.emitError('not-allowed'); rec.emitError('not-allowed');
}); });
fireEvent.pointerUp(btn, { clientY: 600 }); fireEvent.pointerUp(btn, { clientY: 600 });
fireEvent.click(btn);
expect(onTap).toHaveBeenCalledTimes(1); expect(onTap).toHaveBeenCalledTimes(1);
expect(onTranscript).not.toHaveBeenCalled(); expect(onTranscript).not.toHaveBeenCalled();
@@ -247,6 +254,7 @@ describe('HoldToRecordFab', () => {
vi.advanceTimersByTime(350); vi.advanceTimersByTime(350);
}); });
fireEvent.pointerUp(btn, { clientY: 600 }); fireEvent.pointerUp(btn, { clientY: 600 });
fireEvent.click(btn);
expect(onTap).toHaveBeenCalledTimes(1); expect(onTap).toHaveBeenCalledTimes(1);
expect(onTranscript).not.toHaveBeenCalled(); 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(<HoldToRecordFab onTap={onTap} onTranscript={vi.fn()} />);
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', () => { describe('HoldToRecordFab keyboard access', () => {
it('opens the capture sheet when activated without a pointer (keyboard, screen reader)', () => { it('opens the capture sheet when activated without a pointer (keyboard, screen reader)', () => {
const onTap = vi.fn(); const onTap = vi.fn();