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 6ca8ef3f97.

* 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 91094949a6.

* 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 <hariharan.srinivasan@kadfire.com>
Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/16672/head
hariharan 2 weeks ago committed by GitHub
parent 6534aa08e3
commit 0d48916e02
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: Introduced Promise.resolve to ensure that the 'set' operation completes before the 'get' operation Minimizing update delays.

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: wait until changes propagate before updating input selection state

@ -6,7 +6,7 @@ import * as e from '../../../errors.js';
import { is } from '../../../proxy.js';
import { queue_micro_task } from '../../task.js';
import { hydrating } from '../../hydration.js';
import { untrack } from '../../../runtime.js';
import { tick, untrack } from '../../../runtime.js';
import { is_runes } from '../../../context.js';
import { current_batch, previous_batch } from '../../../reactivity/batch.js';
@ -17,11 +17,9 @@ import { current_batch, previous_batch } from '../../../reactivity/batch.js';
* @returns {void}
*/
export function bind_value(input, get, set = get) {
var runes = is_runes();
var batches = new WeakSet();
listen_to_event_and_reset_event(input, 'input', (is_reset) => {
listen_to_event_and_reset_event(input, 'input', async (is_reset) => {
if (DEV && input.type === 'checkbox') {
// TODO should this happen in prod too?
e.bind_invalid_checkbox_value();
@ -36,9 +34,13 @@ export function bind_value(input, get, set = get) {
batches.add(current_batch);
}
// In runes mode, respect any validation in accessors (doesn't apply in legacy mode,
// because we use mutable state which ensures the render effect always runs)
if (runes && value !== (value = get())) {
// Because `{#each ...}` blocks work by updating sources inside the flush,
// we need to wait a tick before checking to see if we should forcibly
// update the input and reset the selection state
await tick();
// Respect any validation in accessors
if (value !== (value = get())) {
var start = input.selectionStart;
var end = input.selectionEnd;

@ -0,0 +1,28 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client', 'hydrate'],
html: `<input><p>a</a>`,
async test({ assert, target }) {
const [input] = target.querySelectorAll('input');
input.focus();
input.value = 'ab';
input.dispatchEvent(new InputEvent('input', { bubbles: true }));
flushSync();
assert.htmlEqual(target.innerHTML, `<input><p>ab</a>`);
assert.equal(input.value, 'ab');
input.focus();
input.value = 'abc';
input.dispatchEvent(new InputEvent('input', { bubbles: true }));
flushSync();
assert.htmlEqual(target.innerHTML, `<input><p>abc</a>`);
assert.equal(input.value, 'abc');
}
});

@ -0,0 +1,8 @@
<script>
let array = $state([{ value: 'a' }]);
</script>
{#each array as obj}
<input bind:value={() => obj.value, (value) => array = [{ value }]} />
<p>{obj.value}</p>
{/each}

@ -0,0 +1,30 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client', 'hydrate'],
async test({ assert, target }) {
const [input] = target.querySelectorAll('input');
input.focus();
input.value = 'Ab';
input.dispatchEvent(new InputEvent('input', { bubbles: true }));
await tick();
await tick();
assert.equal(input.value, 'AB');
assert.htmlEqual(target.innerHTML, `<input /><p>AB</p>`);
input.focus();
input.value = 'ABc';
input.dispatchEvent(new InputEvent('input', { bubbles: true }));
await tick();
await tick();
assert.equal(input.value, 'ABC');
assert.htmlEqual(target.innerHTML, `<input /><p>ABC</p>`);
}
});

@ -0,0 +1,6 @@
<script>
let text = $state('A');
</script>
<input bind:value={() => text, (v) => text = v.toUpperCase()} />
<p>{text}</p>
Loading…
Cancel
Save