From 59b7a77f3a3555553c214eadb9245ff6a8e7589c Mon Sep 17 00:00:00 2001 From: Romain LE DISEZ Date: Sat, 11 Jul 2026 10:52:57 +0000 Subject: [PATCH] fix(editor): avoid mobile focus on task checkboxes Intercept touch checkbox changes before TipTap's focusing handler and update the task-item transaction directly. This prevents the mobile keyboard from starting to open while preserving checkbox state changes. Strengthen the mobile regression test to assert that checking and unchecking emit no editor focus events. Co-authored-by: Codex --- src/lib/components/TiptapEditor.svelte | 52 +++++++++++++++++++++++++- tests/e2e/formatting.spec.ts | 48 +++++++++++++++++++++++- 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/src/lib/components/TiptapEditor.svelte b/src/lib/components/TiptapEditor.svelte index 96b560c..c716755 100644 --- a/src/lib/components/TiptapEditor.svelte +++ b/src/lib/components/TiptapEditor.svelte @@ -27,6 +27,54 @@ let element: HTMLDivElement | undefined = $state(); let editor: Editor | undefined = $state(); + function isTaskCheckboxTarget(target: EventTarget | null): boolean { + if (target instanceof Element) { + const checkbox = target.matches('input[type="checkbox"]') + ? target + : target.closest('label')?.querySelector('input[type="checkbox"]'); + return checkbox !== null && checkbox !== undefined; + } + return false; + } + + function isTouchDevice(): boolean { + return window.matchMedia('(pointer: coarse)').matches || navigator.maxTouchPoints > 0; + } + + function handleWrapperPointerDown(event: PointerEvent) { + if (event.pointerType === 'touch' && isTaskCheckboxTarget(event.target)) { + event.preventDefault(); + } + } + + function handleWrapperClick(event: MouseEvent) { + if (isTaskCheckboxTarget(event.target)) { + return; + } + + editor?.commands.focus(); + } + + function handleWrapperChangeCapture(event: Event) { + if (!isTouchDevice() || !(event.target instanceof HTMLInputElement)) return; + if (event.target.type !== 'checkbox' || !editor) return; + + const taskItem = event.target.closest('li[data-checked]'); + if (!taskItem) return; + + const taskItemPosition = editor.view.posAtDOM(taskItem, 0) - 1; + const taskItemNode = editor.state.doc.nodeAt(taskItemPosition); + if (taskItemNode?.type.name !== 'taskItem') return; + + event.stopPropagation(); + editor.view.dispatch( + editor.state.tr.setNodeMarkup(taskItemPosition, undefined, { + ...taskItemNode.attrs, + checked: event.target.checked + }) + ); + } + onMount(() => { editor = new Editor({ element: element!, @@ -77,7 +125,9 @@ role="textbox" aria-multiline="true" tabindex="0" - onclick={() => editor?.commands.focus()} + onpointerdown={handleWrapperPointerDown} + onclick={handleWrapperClick} + onchangecapture={handleWrapperChangeCapture} >