Skip to content

Commit 0d48916

Browse files
fabhariRich-Harris
andauthored
fix: cursor jumps in input two way binding (#16649)
* fix : remove cursor manipulation for input bindings Old Fix: Restore input binding selection position (#14649) Current Fix: Remove unnecessary cursor manipulation as the presence of runes no longer requires special handling. * fix : add change set to my previous commit * Revert "fix : add change set to my previous commit" This reverts commit 6ca8ef3. * fix: revert previous changeset added new to fix lint errors * chore : resolve lint error to fix pipeline issue * Revert "fix: revert previous changeset added new to fix lint errors" This reverts commit 9109494. * fix: input binding to handle code in a synchronous manner Introduced Promise.resolve to ensure that the 'set' operation completes before the 'get' operation Minimizing update delays. * Fix: resolve cursor jumps and change sets * better fix * test * changeset * simplify * failing test * gah we can't fix the input in an effect, need to do it here, but after a tick so that changes have been flushed through each blocks * add explanatory comment * fix test * this seems to work? --------- Co-authored-by: Hariharan Srinivasan <[email protected]> Co-authored-by: Rich Harris <[email protected]>
1 parent 6534aa0 commit 0d48916

File tree

7 files changed

+91
-7
lines changed

7 files changed

+91
-7
lines changed

.changeset/rare-cups-fold.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: Introduced Promise.resolve to ensure that the 'set' operation completes before the 'get' operation Minimizing update delays.

.changeset/tasty-chicken-care.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: wait until changes propagate before updating input selection state

packages/svelte/src/internal/client/dom/elements/bindings/input.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as e from '../../../errors.js';
66
import { is } from '../../../proxy.js';
77
import { queue_micro_task } from '../../task.js';
88
import { hydrating } from '../../hydration.js';
9-
import { untrack } from '../../../runtime.js';
9+
import { tick, untrack } from '../../../runtime.js';
1010
import { is_runes } from '../../../context.js';
1111
import { current_batch, previous_batch } from '../../../reactivity/batch.js';
1212

@@ -17,11 +17,9 @@ import { current_batch, previous_batch } from '../../../reactivity/batch.js';
1717
* @returns {void}
1818
*/
1919
export function bind_value(input, get, set = get) {
20-
var runes = is_runes();
21-
2220
var batches = new WeakSet();
2321

24-
listen_to_event_and_reset_event(input, 'input', (is_reset) => {
22+
listen_to_event_and_reset_event(input, 'input', async (is_reset) => {
2523
if (DEV && input.type === 'checkbox') {
2624
// TODO should this happen in prod too?
2725
e.bind_invalid_checkbox_value();
@@ -36,9 +34,13 @@ export function bind_value(input, get, set = get) {
3634
batches.add(current_batch);
3735
}
3836

39-
// In runes mode, respect any validation in accessors (doesn't apply in legacy mode,
40-
// because we use mutable state which ensures the render effect always runs)
41-
if (runes && value !== (value = get())) {
37+
// Because `{#each ...}` blocks work by updating sources inside the flush,
38+
// we need to wait a tick before checking to see if we should forcibly
39+
// update the input and reset the selection state
40+
await tick();
41+
42+
// Respect any validation in accessors
43+
if (value !== (value = get())) {
4244
var start = input.selectionStart;
4345
var end = input.selectionEnd;
4446

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
mode: ['client', 'hydrate'],
6+
7+
html: `<input><p>a</a>`,
8+
9+
async test({ assert, target }) {
10+
const [input] = target.querySelectorAll('input');
11+
12+
input.focus();
13+
input.value = 'ab';
14+
input.dispatchEvent(new InputEvent('input', { bubbles: true }));
15+
flushSync();
16+
17+
assert.htmlEqual(target.innerHTML, `<input><p>ab</a>`);
18+
assert.equal(input.value, 'ab');
19+
20+
input.focus();
21+
input.value = 'abc';
22+
input.dispatchEvent(new InputEvent('input', { bubbles: true }));
23+
flushSync();
24+
25+
assert.htmlEqual(target.innerHTML, `<input><p>abc</a>`);
26+
assert.equal(input.value, 'abc');
27+
}
28+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script>
2+
let array = $state([{ value: 'a' }]);
3+
</script>
4+
5+
{#each array as obj}
6+
<input bind:value={() => obj.value, (value) => array = [{ value }]} />
7+
<p>{obj.value}</p>
8+
{/each}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { tick } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
mode: ['client', 'hydrate'],
6+
7+
async test({ assert, target }) {
8+
const [input] = target.querySelectorAll('input');
9+
10+
input.focus();
11+
input.value = 'Ab';
12+
input.dispatchEvent(new InputEvent('input', { bubbles: true }));
13+
14+
await tick();
15+
await tick();
16+
17+
assert.equal(input.value, 'AB');
18+
assert.htmlEqual(target.innerHTML, `<input /><p>AB</p>`);
19+
20+
input.focus();
21+
input.value = 'ABc';
22+
input.dispatchEvent(new InputEvent('input', { bubbles: true }));
23+
24+
await tick();
25+
await tick();
26+
27+
assert.equal(input.value, 'ABC');
28+
assert.htmlEqual(target.innerHTML, `<input /><p>ABC</p>`);
29+
}
30+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<script>
2+
let text = $state('A');
3+
</script>
4+
5+
<input bind:value={() => text, (v) => text = v.toUpperCase()} />
6+
<p>{text}</p>

0 commit comments

Comments
 (0)