chore: merge

pull/18317/head
paoloricciuti 4 months ago
commit d8f94880fb

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: unlink errored and otherwise finished batch

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: walk composedPath() directly in delegated event propagation

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: transfer effects when merging batches

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: allow `$derived(await ...)` in disconnected effect roots

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: remove temporary raw-text hydration markers

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: declare `let:` directives before `{@const}` declarations on slotted elements

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: correctly coordinate component-level effects inside async blocks

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: make unnecessary commit work less likely

@ -0,0 +1,5 @@
---
"svelte": patch
---
chore: add tag name to `a11y_click_events_have_key_events` warning

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: catch rejected promises while merging/committing

@ -62,7 +62,7 @@ Enforce that `autofocus` is not used on elements. Autofocusing elements can caus
### a11y_click_events_have_key_events
```
Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type="button">` or `<a>` might be more appropriate
Visible, non-interactive element `<%element%>` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type="button">` or `<a>` might be more appropriate
```
Enforce that visible, non-interactive elements with an `onclick` event are accompanied by a keyboard event handler.

@ -49,7 +49,7 @@ Enforce that `autofocus` is not used on elements. Autofocusing elements can caus
## a11y_click_events_have_key_events
> Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type="button">` or `<a>` might be more appropriate
> Visible, non-interactive element `<%element%>` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type="button">` or `<a>` might be more appropriate
Enforce that visible, non-interactive elements with an `onclick` event are accompanied by a keyboard event handler.

@ -302,7 +302,7 @@ export function check_element(node, context) {
const has_key_event =
handlers.has('keydown') || handlers.has('keyup') || handlers.has('keypress');
if (!has_key_event) {
w.a11y_click_events_have_key_events(node);
w.a11y_click_events_have_key_events(node, node.name);
}
}
}

@ -203,8 +203,8 @@ export function RegularElement(node, context) {
}
}
// Let bindings first, they can be used on attributes
context.state.init.push(...lets);
// Let bindings first, they can be used on attributes and `{@const}` declarations
context.state.let_directives.push(...lets);
const node_id = context.state.node;

@ -166,11 +166,12 @@ export function a11y_autofocus(node) {
}
/**
* Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type="button">` or `<a>` might be more appropriate
* Visible, non-interactive element `<%element%>` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type="button">` or `<a>` might be more appropriate
* @param {null | NodeLike} node
* @param {string} element
*/
export function a11y_click_events_have_key_events(node) {
w(node, 'a11y_click_events_have_key_events', `Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as \`<button type="button">\` or \`<a>\` might be more appropriate\nhttps://svelte.dev/e/a11y_click_events_have_key_events`);
export function a11y_click_events_have_key_events(node, element) {
w(node, 'a11y_click_events_have_key_events', `Visible, non-interactive element \`<${element}>\` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as \`<button type="button">\` or \`<a>\` might be more appropriate\nhttps://svelte.dev/e/a11y_click_events_have_key_events`);
}
/**

@ -14,7 +14,8 @@ import {
append_child,
create_comment,
insert_before,
node_type
node_type,
remove_child
} from '../operations.js';
import { block, teardown } from '../../reactivity/effects.js';
import { set_should_intro } from '../../render.js';
@ -96,9 +97,11 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
assign_nodes(element, element);
if (render_fn) {
var tmp_comment = null;
if (hydrating && is_raw_text_element(next_tag)) {
// prevent hydration glitches
append_child(element, create_comment(''));
// prevent hydration glitches (code just below expects an anchor)
append_child(element, (tmp_comment = create_comment('')));
}
// If hydrating, use the existing ssr comment as the anchor so that the
@ -122,7 +125,9 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
// contains children, it's a user error (which is warned on elsewhere)
// and the DOM will be silently discarded
render_fn(element, child_anchor);
if (tmp_comment) {
remove_child(element, tmp_comment);
}
set_animation_effect_override(null);
}

@ -15,8 +15,7 @@ import {
remove_attribute,
dispatch_event,
add_event_listener,
remove_event_listener,
get_parent_node
remove_event_listener
} from '../operations.js';
import { current_renderer } from '../../custom-renderer/state.js';
@ -281,12 +280,7 @@ export function handle_event_propagation(event) {
var other_errors = [];
while (current_target !== null) {
/** @type {null | Element} */
var parent_element =
current_target.assignedSlot ||
get_parent_node(current_target) ||
/** @type {any} */ (current_target).host ||
null;
if (current_target === handler_element) break;
try {
// @ts-expect-error
@ -308,10 +302,10 @@ export function handle_event_propagation(event) {
throw_error = error;
}
}
if (event.cancelBubble || parent_element === handler_element || parent_element === null) {
break;
}
current_target = parent_element;
if (event.cancelBubble) break;
path_idx++;
current_target = path_idx < path.length ? /** @type {Element} */ (path[path_idx]) : null;
}
if (throw_error) {

@ -289,19 +289,19 @@ export class Batch {
}
}
// we only reschedule previously-deferred effects if we expect
// to be able to run them after processing the batch
if (!this.#is_deferred()) {
for (const e of this.#dirty_effects) {
this.#maybe_dirty_effects.delete(e);
set_signal_status(e, DIRTY);
this.schedule(e);
}
// We always reschedule previously-deferred effects, not just when
// #is_deferred() is true, because traversing the tree could make
// an if block that contains the last blocking pending effect falsy,
// causing the block to no longer be deferred.
for (const e of this.#dirty_effects) {
this.#maybe_dirty_effects.delete(e);
set_signal_status(e, DIRTY);
this.schedule(e);
}
for (const e of this.#maybe_dirty_effects) {
set_signal_status(e, MAYBE_DIRTY);
this.schedule(e);
}
for (const e of this.#maybe_dirty_effects) {
set_signal_status(e, MAYBE_DIRTY);
this.schedule(e);
}
const roots = this.#roots;
@ -326,6 +326,12 @@ export class Batch {
this.#traverse(root, effects, render_effects);
} catch (e) {
reset_all(root);
// If there's no async work left, this branch is now dead and needs
// to be unlinked to not become a zombie that is never cleaned up.
// See https://github.com/sveltejs/svelte/issues/18221#issuecomment-4497918414
// for a (non-minimal) reproduction that demonstrates a case where this is necessary
// to not get follow-up false-positives via "batch has scheduled roots" invariant errors.
if (!this.#is_deferred()) this.#unlink();
throw e;
}
}
@ -362,6 +368,10 @@ export class Batch {
const earlier_batch = this.#find_earlier_batch();
if (earlier_batch) {
// If this batch collected deferred effects during traversal, they still need
// to run after being merged into the earlier batch.
this.#defer_effects(render_effects);
this.#defer_effects(effects);
earlier_batch.#merge(this);
return;
}
@ -500,9 +510,12 @@ export class Batch {
for (const [effect, deferred] of batch.async_deriveds) {
const d = this.async_deriveds.get(effect);
if (d) deferred.promise.then(d.resolve);
if (d) deferred.promise.then(d.resolve).catch(d.reject);
}
// Mark is not guaranteed not touch these, so we transfer them
this.transfer_effects(batch.#dirty_effects, batch.#maybe_dirty_effects);
/**
* mark all effects that depend on `batch.current`, except the
* async effects that we just resolved (TODO unless they depend
@ -664,14 +677,16 @@ export class Batch {
// immediately resolving them? Likely not because of how this.apply() works.
for (const [effect, deferred] of this.async_deriveds) {
const d = batch.async_deriveds.get(effect);
if (d) deferred.promise.then(d.resolve);
if (d) deferred.promise.then(d.resolve).catch(d.reject);
}
}
if (!batch.#started) continue;
// Re-run async/block effects that depend on distinct values changed in both batches
var others = [...batch.current.keys()].filter((s) => !this.current.has(s));
// Re-run async/block effects that depend on distinct values changed in both batches (ignoring deriveds)
var others = [...batch.current.keys()].filter(
(s) => !(/** @type {[any, boolean]} */ (batch.current.get(s))[1]) && !this.current.has(s)
);
if (others.length === 0) {
if (is_earlier) {
@ -711,11 +726,14 @@ export class Batch {
}
checked = new Map();
var current_unequal = [...batch.current.keys()].filter((c) =>
this.current.has(c)
? /** @type {[any, boolean]} */ (this.current.get(c))[0] !== c.v
: true
);
var current_unequal = [...batch.current]
.filter(([c, v1]) => {
const v2 = this.current.get(c);
if (!v2) return true;
// Either their values are different or one is a derived but not the other
return v2[0] !== v1[0] || v2[1] !== v1[1];
})
.map(([c]) => c);
if (current_unequal.length > 0) {
for (const effect of this.#new_effects) {

@ -187,7 +187,10 @@ export function async_derived(fn, label, location) {
var decrement_pending = increment_pending();
}
if (/** @type {Boundary} */ (parent.b).is_rendered()) {
if (
// boundary can be null if the async derived is inside an $effect.root not connected to the component render tree
parent.b?.is_rendered()
) {
batch.async_deriveds.get(effect)?.reject(OBSOLETE);
} else {
// While the boundary is still showing pending, a new run supersedes all older in-flight runs

@ -20,7 +20,6 @@ import {
EFFECT,
DESTROYED,
INERT,
REACTION_RAN,
BLOCK_EFFECT,
ROOT_EFFECT,
EFFECT_TRANSPARENT,
@ -215,7 +214,11 @@ export function user_effect(fn) {
// Non-nested `$effect(...)` in a component should be deferred
// until the component is mounted
var flags = /** @type {Effect} */ (active_effect).f;
var defer = !active_reaction && (flags & BRANCH_EFFECT) !== 0 && (flags & REACTION_RAN) === 0;
var defer =
!active_reaction &&
(flags & BRANCH_EFFECT) !== 0 &&
component_context !== null &&
!component_context.i;
if (defer) {
// Top-level `$effect(...)` in an unmounted component — defer until mount

@ -1 +1 @@
<!--[--><!----><script>{}<!----></script><!----><!--]-->
<!--[--><!----><script>{}</script><!----><!--]-->

@ -0,0 +1,7 @@
import { test } from '../../test';
export default test({
props: {
css: 'body { color: red; }'
}
});

@ -0,0 +1,9 @@
<script>
let { css } = $props();
</script>
<svelte:head>
<svelte:element this="style" type="text/css">{css}</svelte:element>
</svelte:head>
<p>content</p>

@ -0,0 +1,7 @@
<script>
export let things;
</script>
{#each things as thing}
<slot name="foo" {thing} />
{/each}

@ -0,0 +1,15 @@
import { test } from '../../test';
// `let:` directives on a slotted element must be declared before sibling `{@const}`
// declarations that capture them. In dev mode the `{@const}` derived is read eagerly,
// so a wrong declaration order throws "Cannot access '...' before initialization".
export default test({
compileOptions: {
dev: true
},
html: `
<div slot="foo"><span>1</span></div>
<div slot="foo"><span>2</span></div>
`
});

@ -0,0 +1,10 @@
<script>
import Nested from './Nested.svelte';
</script>
<Nested things={[1, 2]}>
<div slot="foo" let:thing>
{@const props = { thing }}
<span>{props.thing}</span>
</div>
</Nested>

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
// Test that an async derived inside an $effect.root not connected to the component tree still works
async test({ assert, logs }) {
await new Promise((resolve) => setTimeout(resolve, 10));
assert.deepEqual(logs, [1]);
}
});

@ -0,0 +1,11 @@
<script>
setTimeout(() => {
$effect.root(() => {
async function fn() {
const value = $derived(await 1);
return { get value() { return value } };
}
fn().then(r => console.log(r.value));
})
})
</script>

@ -0,0 +1,25 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [x, x_y, pop] = target.querySelectorAll('button');
x.click();
await tick();
x_y.click();
await tick();
pop.click();
await tick();
pop.click();
await tick();
pop.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>x</button> <button>x/y</button> <button>pop</button> 2 1 1'
);
}
});

@ -0,0 +1,25 @@
<script>
let x = $state(0);
let y = $state(0);
const queued = [];
function push(v) {
if (v === 0) return v;
return new Promise((fulfil) => {
queued.push(() => fulfil(v));
});
}
</script>
<button onclick={() => x++}>x</button>
<button onclick={() => {x++;y++}}>x/y</button>
<button onclick={() => (queued.pop()?.())}>pop</button>
{await push(x)} {await push(y)}
{#if true}
{y}
{/if}

@ -0,0 +1,17 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [increment] = target.querySelectorAll('button');
increment.click();
await tick();
increment.click();
await tick();
increment.click();
await tick();
assert.htmlEqual(target.innerHTML, '<button>increment</button> done');
}
});

@ -0,0 +1,21 @@
<script>
let count = $state(0);
const queued = [];
function push(v) {
if (v === 0) return v;
return new Promise((fulfil) => {
queued.push(() => fulfil(v));
});
}
</script>
<button onclick={() => count++}>increment</button>
{#if count < 3}
{await push(count)}
{:else}
done
{/if}

@ -0,0 +1,6 @@
<script>
let x;
$effect(() => console.log(!!x))
</script>
<div bind:this={x}></div>

@ -0,0 +1,10 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
// Test that $effect/onMount etc at the top level of components are correctly deferred/coordinated if inside an async block
async test({ assert, logs }) {
await tick();
assert.deepEqual(logs, [true]);
}
});

@ -0,0 +1,5 @@
<script>
import Child from './Child.svelte';
</script>
<Child foo={await 1} />

@ -0,0 +1,25 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [increment, pop] = target.querySelectorAll('button');
increment.click();
await tick();
increment.click();
await tick();
increment.click();
await tick();
pop.click();
await tick();
assert.htmlEqual(target.innerHTML, '<button>increment</button><button>pop</button> failed');
pop.click();
await tick();
pop.click();
await tick();
assert.htmlEqual(target.innerHTML, '<button>increment</button><button>pop</button> failed');
}
});

@ -0,0 +1,22 @@
<script>
let count = $state(0);
const queued = [];
function push(v) {
if (v === 0) return v;
return new Promise((fulfil,reject) => {
queued.push(() => v === 3 ? reject('boom') : fulfil(v));
});
}
</script>
<button onclick={() => count++}>increment</button>
<button onclick={() => queued.pop()?.()}>pop</button>
<svelte:boundary>
{await push(count)}
{#snippet failed()}failed{/snippet}
</svelte:boundary>

@ -2,7 +2,7 @@ import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, compileOptions }) {
async test({ assert, target }) {
const [toggle, increment] = target.querySelectorAll('button');
flushSync(() => increment.click());
@ -25,8 +25,8 @@ export default test({
`
<button>toggle</button>
<button>count: 2</button>
<p>show: ${compileOptions.experimental?.async ? 'false' : 'true'}</p>
`
<p>show: true</p>
` // show: false would also be fine; this is more about ensuring that things continue to work _somehow_
);
}
});

@ -1,7 +1,7 @@
[
{
"code": "a11y_click_events_have_key_events",
"message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"message": "Visible, non-interactive element `<div>` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"start": {
"line": 13,
"column": 0
@ -13,7 +13,7 @@
},
{
"code": "a11y_click_events_have_key_events",
"message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"message": "Visible, non-interactive element `<div>` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"start": {
"line": 15,
"column": 0
@ -25,7 +25,7 @@
},
{
"code": "a11y_click_events_have_key_events",
"message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"message": "Visible, non-interactive element `<section>` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"start": {
"line": 18,
"column": 0
@ -37,7 +37,7 @@
},
{
"code": "a11y_click_events_have_key_events",
"message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"message": "Visible, non-interactive element `<main>` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"start": {
"line": 20,
"column": 0
@ -49,7 +49,7 @@
},
{
"code": "a11y_click_events_have_key_events",
"message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"message": "Visible, non-interactive element `<article>` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"start": {
"line": 22,
"column": 0
@ -61,7 +61,7 @@
},
{
"code": "a11y_click_events_have_key_events",
"message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"message": "Visible, non-interactive element `<header>` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"start": {
"line": 24,
"column": 0
@ -73,7 +73,7 @@
},
{
"code": "a11y_click_events_have_key_events",
"message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"message": "Visible, non-interactive element `<footer>` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"start": {
"line": 26,
"column": 0
@ -85,7 +85,7 @@
},
{
"code": "a11y_click_events_have_key_events",
"message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"message": "Visible, non-interactive element `<footer>` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"start": {
"line": 28,
"column": 0

@ -1,7 +1,7 @@
[
{
"code": "a11y_click_events_have_key_events",
"message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"message": "Visible, non-interactive element `<div>` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"start": {
"column": 1,
"line": 7

@ -1,7 +1,7 @@
[
{
"code": "a11y_click_events_have_key_events",
"message": "Visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"message": "Visible, non-interactive element `<div>` with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as `<button type=\"button\">` or `<a>` might be more appropriate",
"start": {
"column": 1,
"line": 8

Loading…
Cancel
Save