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
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:
1 parent
fcdf44a0ad
commit
3c31a22ba0
4 files changed
+37
-7
No files matched your search
Generated
+2
-2
@@ -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,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();
|
||||||
|
|||||||
Reference in new issue
Block a user