From a94a38071b6c75b21d470bf29c22271da43ddc13 Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Mon, 25 May 2026 22:19:55 +0200 Subject: [PATCH 1/2] fix(tags): tagging works on Android Chrome PWA MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TagInput's onkeydown only commits on `e.key === 'Enter' || e.key === ','`. On Android Chrome with predictive text (GBoard et al), the soft keyboard keydown fires with `e.key === 'Unidentified'` and `e.keyCode === 229` during IME composition — the check misses entirely, the Enter never adds the tag, and the press instead bubbles up to the parent
and submits the whole activity. Result: impossible to tag from mobile. Fixes: - Track composition state via oncompositionstart / oncompositionend so we ignore commits while the IME is mid-composition. - Accept Enter via either `e.key === 'Enter'` OR `e.keyCode === 13` (and same for comma). preventDefault on every commit attempt so the parent form never submits accidentally. - Add an explicit "Legg til"-button next to the input. Foolproof mobile path — a tap is a tap, no IME involved. Desktop users still have Enter. - `enterkeyhint="done"` so the soft keyboard shows a sensible label on the action key. autocapitalize/autocorrect/spellcheck off so tags don't get mangled by autocomplete suggestions. Placeholder copy updated to mention the button. --- frontend/src/components/TagInput.svelte | 57 +++++++++++++++++++++---- 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/TagInput.svelte b/frontend/src/components/TagInput.svelte index 6037eae..671e669 100644 --- a/frontend/src/components/TagInput.svelte +++ b/frontend/src/components/TagInput.svelte @@ -30,6 +30,13 @@ let serverHits: { name: string; usage_count: number }[] = $state([]); let privateHits: { name: string; count: number }[] = $state([]); let timer: ReturnType | undefined; + // True while the soft keyboard is composing (IME predictive text on + // Android, hangul/kana/pinyin elsewhere). During composition keydown + // fires with e.key === 'Unidentified' and e.keyCode === 229, so a naive + // `e.key === 'Enter'` check misses the user's commit-and-press-enter + // gesture. We commit on compositionend Enter via the addCurrent button + // instead — see the keydown handler. + let composing = $state(false); function add(name: string) { const n = name.trim().toLowerCase(); @@ -48,7 +55,19 @@ } function onKey(e: KeyboardEvent) { - if (e.key === 'Enter' || e.key === ',') { + // Detect "commit" intent across desktop and mobile soft keyboards. + // Android Chrome with predictive text fires keydown with key + // 'Unidentified' + keyCode 229 during composition; Enter committed + // afterwards still has keyCode 13 (or 'Enter'), so we accept either. + // Comma is the same. + if (composing) return; + const isEnter = e.key === 'Enter' || e.keyCode === 13; + const isComma = e.key === ',' || e.keyCode === 188; + if (isEnter || isComma) { + // Always preventDefault on Enter — without it the parent + // submits the whole activity when the user is just trying to add a + // tag (especially noticeable on mobile where the soft keyboard's + // "Done" key triggers form submit). e.preventDefault(); add(input); } else if (e.key === 'Backspace' && !input && tags.length) { @@ -56,6 +75,12 @@ } } + /** Called by the "Legg til"-button. The button is the foolproof mobile + * path — IME composition can swallow Enter, but a tap is a tap. */ + function addCurrent() { + add(input); + } + function onType(e: Event) { input = (e.target as HTMLInputElement).value; clearTimeout(timer); @@ -87,14 +112,28 @@ {/each} - +
+ (composing = true)} + oncompositionend={() => (composing = false)} + placeholder="Skriv etikett, trykk Enter eller «Legg til»" + aria-label="Ny etikett" + enterkeyhint="done" + autocapitalize="none" + autocorrect="off" + spellcheck="false" + style="flex: 1 1 auto; min-width: 0;" + /> + +
{#if serverHits.length || privateHits.length}
From 4945204b7b37b213a57e2309e7fe30b73997682a Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Mon, 25 May 2026 22:22:22 +0200 Subject: [PATCH 2/2] =?UTF-8?q?fix(tags):=20Firefox=20Android=20=E2=80=94?= =?UTF-8?q?=20Enter=20to=20commit=20now=20works?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prior fix (a94a380) handled Chrome Android via the keydown path. Firefox Android does something different: the soft-keyboard Enter is intercepted at the input layer as "advance to next form field" — no keydown fires for us to intercept, so the user's tag was lost and focus jumped to the next ActivityForm input. Add an onbeforeinput handler. InputEvent.inputType === 'insertLineBreak' fires on every modern browser when the user submits via Enter, even when keydown is suppressed at the IME/keyboard layer. preventDefault'ing here cancels the focus-advance before it happens. Idempotent with the existing onKey: - Chrome: keydown fires with Enter, our preventDefault cancels the line-break, so beforeinput never fires. - Firefox Android: no keydown for Enter, beforeinput fires with insertLineBreak, we commit there. - Android Chrome with IME composition: composing flag short-circuits both paths so we don't commit mid-composition. Comma + button remain as platform-agnostic fallbacks. --- frontend/src/components/TagInput.svelte | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/frontend/src/components/TagInput.svelte b/frontend/src/components/TagInput.svelte index 671e669..5594b5b 100644 --- a/frontend/src/components/TagInput.svelte +++ b/frontend/src/components/TagInput.svelte @@ -81,6 +81,25 @@ add(input); } + /** + * beforeinput fires before the browser commits a value change. We use it + * to catch Enter on platforms where keydown doesn't deliver a usable + * Enter event — notably Firefox on Android, which treats the soft- + * keyboard Enter as "advance to next form field" at the input layer + * (no keydown for us to intercept). InputEvent.inputType === 'insertLineBreak' + * still fires here, so we get to preventDefault before focus moves. + * Idempotent with onKey: keydown's preventDefault on Chrome cancels + * the would-be insertion, so beforeinput never fires there. + */ + function onBeforeInput(e: Event) { + const ie = e as InputEvent; + if (composing) return; + if (ie.inputType === 'insertLineBreak') { + e.preventDefault(); + add(input); + } + } + function onType(e: Event) { input = (e.target as HTMLInputElement).value; clearTimeout(timer); @@ -118,6 +137,7 @@ value={input} oninput={onType} onkeydown={onKey} + onbeforeinput={onBeforeInput} oncompositionstart={() => (composing = true)} oncompositionend={() => (composing = false)} placeholder="Skriv etikett, trykk Enter eller «Legg til»"