Repository: sveltejs/svelte (JavaScript)
Date: 2019-12-28 10:01:30+00:00
Task type: Bug Fix

Issue:
Cursor misbehaviour in  Safari's <input> controls
**Describe the bug**
In Safari (12.1.2), when adding an `<input>` control with manual two-way binding - by using an 'on:input' handler for instance - changing the contents of the `<input>` control at any point other than the end of the word causes the cursor to jump to the end of the control.

This does not happen in Chrome (76.0.3809.100) or Firefox (68.0.2). I have not been able to test Edge yet.

**To Reproduce**
Check out this minimal REPL - https://svelte.dev/repl/db64dcebc4da4ab89100870a9fe7522b?version=3.9.1

In Safari (12.1.2), select the input control in the middle and make any sort of edit. Notice how the cursor jumps to the end of the input control.

**Expected behavior**
Consistent behaviour across all modern browsers

**Severity**
Annoying, however I suspect it'll catch more people out as it's such a simple thing to try. I've worked around it by having the variable updated by on:input be different from the one that is used as the initial {value} of the control. I cannot use two-way binding in this case because I'm using the on:input to handle validation.


Issue discussion:
I've been looking into this for about an hour. No results so far, but there seemed to be a similar problem in Elm for a while: https://github.com/elm/html/issues/174  

Solution patch:
diff --git a/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts b/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts
--- a/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts
+++ b/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts
@@ -76,6 +76,8 @@ export default class AttributeWrapper {
 		const is_select_value_attribute =
 			name === 'value' && element.node.name === 'select';
 
+		const is_input_value = name === 'value' && element.node.name === 'input';
+
 		const should_cache = is_src || this.node.should_cache() || is_select_value_attribute; // TODO is this necessary?
 
 		const last = should_cache && block.get_unique_name(
@@ -147,6 +149,14 @@ export default class AttributeWrapper {
 					: x`${condition} && (${last} !== (${last} = ${value}))`;
 			}
 
+			if (is_input_value) {
+				const type = element.node.get_static_attribute_value('type');
+
+				if (type === null || type === "" || type === "text" || type === "email" || type === "password") {
+					condition = x`${condition} && ${element.var}.${property_name} !== ${should_cache ? last : value}`;
+				}
+			}
+
 			if (block.has_outros) {
 				condition = x`!#current || ${condition}`;
 			}


Test patch:
diff --git a/test/js/samples/input-value/expected.js b/test/js/samples/input-value/expected.js
new file mode 100644
--- /dev/null
+++ b/test/js/samples/input-value/expected.js
@@ -0,0 +1,77 @@
+/* generated by Svelte vX.Y.Z */
+import {
+	SvelteComponent,
+	append,
+	detach,
+	element,
+	init,
+	insert,
+	listen,
+	noop,
+	safe_not_equal,
+	set_data,
+	space,
+	text
+} from "svelte/internal";
+
+function create_fragment(ctx) {
+	let input;
+	let t0;
+	let h1;
+	let t1;
+	let t2;
+	let dispose;
+
+	return {
+		c() {
+			input = element("input");
+			t0 = space();
+			h1 = element("h1");
+			t1 = text(/*name*/ ctx[0]);
+			t2 = text("!");
+			input.value = /*name*/ ctx[0];
+			dispose = listen(input, "input", /*onInput*/ ctx[1]);
+		},
+		m(target, anchor) {
+			insert(target, input, anchor);
+			insert(target, t0, anchor);
+			insert(target, h1, anchor);
+			append(h1, t1);
+			append(h1, t2);
+		},
+		p(ctx, [dirty]) {
+			if (dirty & /*name*/ 1 && input.value !== /*name*/ ctx[0]) {
+				input.value = /*name*/ ctx[0];
+			}
+
+			if (dirty & /*name*/ 1) set_data(t1, /*name*/ ctx[0]);
+		},
+		i: noop,
+		o: noop,
+		d(detaching) {
+			if (detaching) detach(input);
+			if (detaching) detach(t0);
+			if (detaching) detach(h1);
+			dispose();
+		}
+	};
+}
+
+function instance($$self, $$props, $$invalidate) {
+	let name = "change me";
+
+	function onInput(event) {
+		$$invalidate(0, name = event.target.value);
+	}
+
+	return [name, onInput];
+}
+
+class Component extends SvelteComponent {
+	constructor(options) {
+		super();
+		init(this, options, instance, create_fragment, safe_not_equal, {});
+	}
+}
+
+export default Component;
\ No newline at end of file
diff --git a/test/js/samples/input-value/input.svelte b/test/js/samples/input-value/input.svelte
new file mode 100644
--- /dev/null
+++ b/test/js/samples/input-value/input.svelte
@@ -0,0 +1,11 @@
+<script>
+	let name = 'change me';
+	
+	function onInput(event) {
+		name = event.target.value
+	}
+</script>
+
+<input value={name} on:input={onInput}>
+
+<h1>{name}!</h1>
\ No newline at end of file