fix: uncontrolled input — stop re-setting value prop on every keystroke, killing focus

This commit is contained in:
Falkan
2026-03-17 19:27:07 -04:00
parent d2045b7a00
commit 78e24017a7
3 changed files with 146 additions and 3 deletions

View File

@@ -1,7 +1,16 @@
<script lang="ts">
/**
* UnitInput — controlled numeric input.
* Phase 2: number input only. NL field added in Phase 3.
* UnitInput — uncontrolled numeric input with one-way push to parent.
*
* The input owns its own DOM value. We only push changes upward via onchange.
* We deliberately do NOT set value={value} on the input element — that would
* cause Svelte to re-render the input on every parent state update, resetting
* the cursor position and stealing focus mid-typing.
*
* The parent can pass a new `value` prop if it needs to imperatively set the
* input (e.g. Ctrl+click adopts a converted value). We detect that via $effect
* and update the DOM node directly, only when the value actually differs from
* what's currently in the input.
*/
let {
@@ -12,6 +21,18 @@
onchange: (v: number) => void;
} = $props();
let inputEl = $state<HTMLInputElement | null>(null);
// Sync parent-driven value changes into the DOM — but only when the parent
// is actually setting a new value (e.g. Ctrl+click), not on every keystroke.
$effect(() => {
if (!inputEl) return;
const current = parseFloat(inputEl.value);
if (isNaN(current) || current !== value) {
inputEl.value = String(value);
}
});
function handleInput(e: Event) {
const input = e.currentTarget as HTMLInputElement;
const parsed = parseFloat(input.value);
@@ -22,9 +43,9 @@
</script>
<input
bind:this={inputEl}
type="number"
step="any"
value={value}
oninput={handleInput}
aria-label="Input value"
/>

View File

@@ -24,6 +24,9 @@ export interface Unit {
/** Group this unit belongs to, or null if orphaned. */
group: string | null;
/** Optional human-readable description shown as a tooltip in the converter. */
description?: string;
}
/** A group of units sharing a base unit and a universal scale factor. */