diff --git a/frontend/src/components/TagInput.svelte b/frontend/src/components/TagInput.svelte index 6037eae..5594b5b 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,31 @@ } } + /** 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); + } + + /** + * 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); @@ -87,14 +131,29 @@ {/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}