From ba88febc77e3bd223f8bb2387c70f65f6a8bd972 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 29 Apr 2026 16:18:40 +0200 Subject: [PATCH] fix(PersonMentionEditor): guard setEditable effect against re-entry loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The disabled-state effect calls editor.setEditable, which triggers a ProseMirror transaction → onUpdate → bind:value/mentionedPersons writes → host re-render → child prop pass-through → effect re-fires. Without an idempotence check, this exceeds Svelte's effect_update_depth and crashes every consuming spec (TranscriptionBlock 22/22). Compare editor.isEditable against the desired value first; only call setEditable when it actually needs to change. Follow-up to 6ef888a1. Co-Authored-By: Claude Opus 4.7 --- frontend/src/lib/components/PersonMentionEditor.svelte | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/components/PersonMentionEditor.svelte b/frontend/src/lib/components/PersonMentionEditor.svelte index 592fdd7d..1347f545 100644 --- a/frontend/src/lib/components/PersonMentionEditor.svelte +++ b/frontend/src/lib/components/PersonMentionEditor.svelte @@ -241,8 +241,15 @@ onDestroy(() => { // Keep editor in sync with the reactive `disabled` prop. Tiptap's setEditable // flips contenteditable on the inner DOM and stops accepting input — matches // the textarea's old `disabled` semantics for keyboard users (WCAG 2.1.1). +// +// Guard: setEditable triggers a ProseMirror transaction which fires onUpdate; +// onUpdate writes through bind:value / bind:mentionedPersons. Without this +// idempotence check, the effect would loop on every prop pass-through. $effect(() => { - editor?.setEditable(!disabled); + const shouldBeEditable = !disabled; + if (editor && editor.isEditable !== shouldBeEditable) { + editor.setEditable(shouldBeEditable); + } });