fix another edge case

pull/11820/head
Dominic Gannaway 2 years ago
parent 8738c13107
commit 903e8ba103

@ -4,7 +4,7 @@ import { stringify } from '../../../render.js';
import { listen_to_event_and_reset_event } from './shared.js';
import * as e from '../../../errors.js';
import { get_proxied_value, is } from '../../../proxy.js';
import { yield_updates } from '../../../runtime.js';
import { yield_event_updates } from '../../../runtime.js';
/**
* @param {HTMLInputElement} input
@ -19,7 +19,9 @@ export function bind_value(input, get_value, update) {
e.bind_invalid_checkbox_value();
}
yield_updates(() => update(is_numberlike_input(input) ? to_number(input.value) : input.value));
yield_event_updates(() =>
update(is_numberlike_input(input) ? to_number(input.value) : input.value)
);
});
render_effect(() => {
@ -85,10 +87,10 @@ export function bind_group(inputs, group_index, input, get_value, update) {
value = get_binding_group_value(binding_group, value, input.checked);
}
yield_updates(() => update(value));
yield_event_updates(() => update(value));
},
// TODO better default value handling
() => yield_updates(() => update(is_checkbox ? [] : null))
() => yield_event_updates(() => update(is_checkbox ? [] : null))
);
render_effect(() => {
@ -129,7 +131,7 @@ export function bind_group(inputs, group_index, input, get_value, update) {
export function bind_checked(input, get_value, update) {
listen_to_event_and_reset_event(input, 'change', () => {
var value = input.checked;
yield_updates(() => update(value));
yield_event_updates(() => update(value));
});
if (get_value() == undefined) {
@ -188,7 +190,7 @@ function to_number(value) {
*/
export function bind_files(input, get_value, update) {
listen_to_event_and_reset_event(input, 'change', () => {
yield_updates(() => update(input.files));
yield_event_updates(() => update(input.files));
});
render_effect(() => {
input.files = get_value();

@ -1,7 +1,7 @@
import { hydrating } from '../../hydration.js';
import { render_effect, effect } from '../../../reactivity/effects.js';
import { listen } from './shared.js';
import { yield_updates } from '../../../runtime.js';
import { yield_event_updates } from '../../../runtime.js';
/** @param {TimeRanges} ranges */
function time_ranges_to_array(ranges) {
@ -36,7 +36,7 @@ export function bind_current_time(media, get_value, update) {
}
updating = true;
yield_updates(() => update(media.currentTime));
yield_event_updates(() => update(media.currentTime));
};
raf_id = requestAnimationFrame(callback);
@ -62,7 +62,7 @@ export function bind_current_time(media, get_value, update) {
*/
export function bind_buffered(media, update) {
listen(media, ['loadedmetadata', 'progress'], () =>
yield_updates(() => update(time_ranges_to_array(media.buffered)))
yield_event_updates(() => update(time_ranges_to_array(media.buffered)))
);
}
@ -80,7 +80,7 @@ export function bind_seekable(media, update) {
*/
export function bind_played(media, update) {
listen(media, ['timeupdate'], () =>
yield_updates(() => update(time_ranges_to_array(media.played)))
yield_event_updates(() => update(time_ranges_to_array(media.played)))
);
}
@ -89,7 +89,7 @@ export function bind_played(media, update) {
* @param {(seeking: boolean) => void} update
*/
export function bind_seeking(media, update) {
listen(media, ['seeking', 'seeked'], () => yield_updates(() => update(media.seeking)));
listen(media, ['seeking', 'seeked'], () => yield_event_updates(() => update(media.seeking)));
}
/**
@ -97,7 +97,7 @@ export function bind_seeking(media, update) {
* @param {(seeking: boolean) => void} update
*/
export function bind_ended(media, update) {
listen(media, ['timeupdate', 'ended'], () => yield_updates(() => update(media.ended)));
listen(media, ['timeupdate', 'ended'], () => yield_event_updates(() => update(media.ended)));
}
/**
@ -108,7 +108,7 @@ export function bind_ready_state(media, update) {
listen(
media,
['loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough', 'playing', 'waiting', 'emptied'],
() => yield_updates(() => update(media.readyState))
() => yield_event_updates(() => update(media.readyState))
);
}
@ -132,7 +132,7 @@ export function bind_playback_rate(media, get_value, update) {
}
listen(media, ['ratechange'], () => {
if (!updating) yield_updates(() => update(media.playbackRate));
if (!updating) yield_event_updates(() => update(media.playbackRate));
updating = false;
});
});
@ -150,7 +150,7 @@ export function bind_paused(media, get_value, update) {
var callback = () => {
if (paused !== media.paused) {
paused = media.paused;
yield_updates(() => update((paused = media.paused)));
yield_event_updates(() => update((paused = media.paused)));
}
};
@ -175,7 +175,7 @@ export function bind_paused(media, get_value, update) {
media.pause();
} else {
media.play().catch(() => {
yield_updates(() => update((paused = true)));
yield_event_updates(() => update((paused = true)));
});
}
};
@ -239,7 +239,7 @@ export function bind_muted(media, get_value, update) {
var callback = () => {
updating = true;
yield_updates(() => update(media.muted));
yield_event_updates(() => update(media.muted));
};
if (get_value() == null) {

@ -1,4 +1,4 @@
import { yield_updates } from '../../../runtime.js';
import { yield_event_updates } from '../../../runtime.js';
import { listen } from './shared.js';
/**
@ -7,6 +7,6 @@ import { listen } from './shared.js';
*/
export function bind_online(update) {
listen(window, ['online', 'offline'], () => {
yield_updates(() => update(navigator.onLine));
yield_event_updates(() => update(navigator.onLine));
});
}

@ -1,6 +1,6 @@
import { effect } from '../../../reactivity/effects.js';
import { listen_to_event_and_reset_event } from './shared.js';
import { untrack, yield_updates } from '../../../runtime.js';
import { untrack, yield_event_updates } from '../../../runtime.js';
import { is } from '../../../proxy.js';
/**
@ -90,7 +90,7 @@ export function bind_select_value(select, get_value, update) {
value = selected_option && get_option_value(selected_option);
}
yield_updates(() => update(value));
yield_event_updates(() => update(value));
});
// Needs to be an effect, not a render_effect, so that in case of each loops the logic runs after the each block has updated

@ -1,5 +1,5 @@
import { effect, render_effect } from '../../../reactivity/effects.js';
import { untrack, yield_updates } from '../../../runtime.js';
import { untrack, yield_event_updates } from '../../../runtime.js';
/**
* Resize observer singleton.
@ -90,7 +90,7 @@ export function bind_resize_observer(element, type, update) {
var unsub = observer.observe(
element,
/** @param {any} entry */ (entry) => yield_updates(() => update(entry[type]))
/** @param {any} entry */ (entry) => yield_event_updates(() => update(entry[type]))
);
render_effect(() => unsub);
}
@ -104,7 +104,7 @@ export function bind_element_size(element, type, update) {
var unsub = resize_observer_border_box.observe(element, () => update(element[type]));
effect(() => {
yield_updates(() => untrack(() => update(element[type])));
yield_event_updates(() => untrack(() => update(element[type])));
return unsub;
});
}

@ -1,6 +1,6 @@
import { STATE_SYMBOL } from '../../../constants.js';
import { effect, render_effect } from '../../../reactivity/effects.js';
import { untrack, yield_updates } from '../../../runtime.js';
import { untrack, yield_event_updates } from '../../../runtime.js';
import { queue_micro_task } from '../../task.js';
/**
@ -37,7 +37,7 @@ export function bind_this(element_or_component, update, get_value, get_parts) {
untrack(() => {
if (element_or_component !== get_value(...parts)) {
yield_updates(() => {
yield_event_updates(() => {
update(element_or_component, ...parts);
// If this is an effect rerun (cause: each block context changes), then nullfiy the binding at
// the previous position if it isn't already taken over by a different effect.

@ -1,5 +1,5 @@
import { effect, render_effect } from '../../../reactivity/effects.js';
import { yield_updates } from '../../../runtime.js';
import { yield_event_updates } from '../../../runtime.js';
import { listen } from './shared.js';
/**
@ -16,7 +16,7 @@ export function bind_window_scroll(type, get_value, update) {
clearTimeout(timeout);
timeout = setTimeout(clear, 100); // TODO use scrollend event if supported (or when supported everywhere?)
yield_updates(() => update(window[is_scrolling_x ? 'scrollX' : 'scrollY']));
yield_event_updates(() => update(window[is_scrolling_x ? 'scrollX' : 'scrollY']));
};
addEventListener('scroll', target_handler, {
@ -64,5 +64,5 @@ export function bind_window_scroll(type, get_value, update) {
* @param {(size: number) => void} update
*/
export function bind_window_size(type, update) {
listen(window, ['resize'], () => yield_updates(() => update(window[type])));
listen(window, ['resize'], () => yield_event_updates(() => update(window[type])));
}

@ -1,6 +1,6 @@
import { render_effect } from '../../reactivity/effects.js';
import { all_registered_events, root_event_handles } from '../../render.js';
import { yield_updates } from '../../runtime.js';
import { yield_event_updates } from '../../runtime.js';
import { define_property, is_array } from '../../utils.js';
import { hydrating } from '../hydration.js';
import { queue_micro_task } from '../task.js';
@ -48,7 +48,7 @@ export function create_event(event_name, dom, handler, options) {
handle_event_propagation(dom, event);
}
if (!event.cancelBubble) {
return yield_updates(() => handler.call(this, event));
return yield_event_updates(() => handler.call(this, event), event.isTrusted);
}
}
@ -103,6 +103,18 @@ export function delegate(events) {
}
}
/**
* @param {Element} element
* @param {string} event_name
*/
function get_delegated(element, event_name) {
if (/** @type {any} */ (element).disabled) {
return undefined;
}
// @ts-expect-error
return element['__' + event_name];
}
/**
* @param {Node} handler_element
* @param {Event} event
@ -173,6 +185,10 @@ export function handle_event_propagation(handler_element, event) {
}
});
// We use the trusted status on the first event we encounter, if we encounter
// more from bubbling we trust those events as we've already yielded.
var is_trusted = event.isTrusted;
/** @param {Element} next_target */
function next(next_target) {
current_target = next_target;
@ -180,16 +196,24 @@ export function handle_event_propagation(handler_element, event) {
var parent_element = next_target.parentNode || /** @type {any} */ (next_target).host || null;
try {
// @ts-expect-error
var delegated = next_target['__' + event_name];
if (delegated !== undefined && !(/** @type {any} */ (next_target).disabled)) {
if (is_array(delegated)) {
var [fn, ...data] = delegated;
fn.apply(next_target, [event, ...data]);
} else {
delegated.call(next_target, event);
}
var delegated = get_delegated(next_target, event_name);
if (delegated !== undefined) {
yield_event_updates(() => {
// The event handler might have changed from yield_updates
var delegated = get_delegated(next_target, event_name);
if (!delegated) {
return;
}
if (is_array(delegated)) {
var [fn, ...data] = delegated;
fn.apply(next_target, [event, ...data]);
} else {
delegated.call(next_target, event);
}
// Further propagation will be trusted.
is_trusted = true;
}, is_trusted);
}
} finally {
if (
@ -204,7 +228,7 @@ export function handle_event_propagation(handler_element, event) {
}
try {
yield_updates(() => next(/** @type {Element} */ (current_target)));
next(/** @type {Element} */ (current_target));
} finally {
// @ts-expect-error is used above
event.__root = handler_element;

@ -723,17 +723,18 @@ function process_effects(effect, collected_effects) {
}
/**
* @param {{ (): void; (): any; }} fn
* @param {{(): void;(): any;}} fn
* @param {boolean} [is_trusted]
*/
export function yield_updates(fn) {
export function yield_event_updates(fn, is_trusted = false) {
const previous_scheduler_mode = current_scheduler_mode;
// If we're calling yield_updates and there is already an active yield in progress (is_yield_task_active)
// then it's likely that the event might might try and read from the UI. Additionally, we might be dealing with
// event that was sync called from another event flow, thus isTrusted() will be true such as a submit event from
// event that was sync called from another event flow, thus is_trusted will be true such as a submit event from
// a subsequent click event. In either case, the UI needs to be up-to-date, so we flush any pending changes.
if (
previous_scheduler_mode !== FLUSH_YIELD &&
(is_yield_task_active || (is_yield_task_queued && window.event?.isTrusted))
(is_yield_task_active || (is_yield_task_queued && is_trusted))
) {
flush_sync();
}

@ -0,0 +1,12 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
const [b1] = target.querySelectorAll('button');
b1.click();
flushSync();
assert.deepEqual(logs, ['works']);
}
});

@ -0,0 +1,12 @@
<script>
let toggle = $state(false);
const onclick = $derived(toggle ? () => { console.log('works') } : () => { console.log('fails')})
const props = $derived({ onclick });
</script>
<div {...props}>
<button onclick={() => { toggle = true }}>
click me
</button>
</div>
Loading…
Cancel
Save