fix(tags): tagging works on Android Chrome PWA

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 <form>
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.
This commit is contained in:
Ole-Morten Duesund 2026-05-25 22:19:55 +02:00
commit a94a38071b

View file

@ -30,6 +30,13 @@
let serverHits: { name: string; usage_count: number }[] = $state([]);
let privateHits: { name: string; count: number }[] = $state([]);
let timer: ReturnType<typeof setTimeout> | 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 <form>
// 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}
</div>
<input
type="text"
value={input}
oninput={onType}
onkeydown={onKey}
placeholder="Legg til etikett, trykk Enter"
aria-label="Ny etikett"
/>
<div class="row" style="gap: 0.4rem; align-items: stretch;">
<input
type="text"
value={input}
oninput={onType}
onkeydown={onKey}
oncompositionstart={() => (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;"
/>
<button type="button" onclick={addCurrent} disabled={!input.trim()}
aria-label="Legg til etikett"
style="flex: 0 0 auto;">
Legg til
</button>
</div>
{#if serverHits.length || privateHits.length}
<div class="card" style="margin-top: 0.25rem;" role="listbox" aria-label="Forslag">