From 15d6308d60f5fdc5fabf23568d629418c652ee93 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 2 Jan 2024 23:00:49 +0100 Subject: [PATCH 1/4] fix: catch delegated events from elements moved outside the container (#10060) fixes #9777 --- .changeset/dirty-bats-punch.md | 5 ++++ packages/svelte/src/internal/client/render.js | 19 +++++++++++++++ .../_config.js | 23 +++++++++++++++++++ .../main.svelte | 12 ++++++++++ 4 files changed, 59 insertions(+) create mode 100644 .changeset/dirty-bats-punch.md create mode 100644 packages/svelte/tests/runtime-runes/samples/event-listener-moved-outside-container/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/event-listener-moved-outside-container/main.svelte diff --git a/.changeset/dirty-bats-punch.md b/.changeset/dirty-bats-punch.md new file mode 100644 index 0000000000..8274a2bbc3 --- /dev/null +++ b/.changeset/dirty-bats-punch.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: handle delegated events of elements moved outside the container diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index a963c832c3..06bf9d406c 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -1280,6 +1280,10 @@ function handle_event_propagation(root_element, event) { const handled_at = event.__root; if (handled_at) { const at_idx = path.indexOf(handled_at); + if (at_idx !== -1 && root_element === document) { + // This is the fallback document listener but the event was already handled -> ignore + return; + } if (at_idx < path.indexOf(root_element)) { path_idx = at_idx; } @@ -2778,6 +2782,7 @@ export function mount(component, options) { set_current_hydration_fragment(previous_hydration_fragment); } const bound_event_listener = handle_event_propagation.bind(null, container); + const bound_document_event_listener = handle_event_propagation.bind(null, document); /** @param {Array} events */ const event_handle = (events) => { @@ -2785,6 +2790,9 @@ export function mount(component, options) { const event_name = events[i]; if (!registered_events.has(event_name)) { registered_events.add(event_name); + // Add the event listener to both the container and the document. + // The container listener ensures we catch events from within in case + // the outer content stops propagation of the event. container.addEventListener( event_name, bound_event_listener, @@ -2794,6 +2802,17 @@ export function mount(component, options) { } : undefined ); + // The document listener ensures we catch events that originate from elements that were + // manually moved outside of the container (e.g. via manual portals). + document.addEventListener( + event_name, + bound_document_event_listener, + PassiveDelegatedEvents.includes(event_name) + ? { + passive: true + } + : undefined + ); } } }; diff --git a/packages/svelte/tests/runtime-runes/samples/event-listener-moved-outside-container/_config.js b/packages/svelte/tests/runtime-runes/samples/event-listener-moved-outside-container/_config.js new file mode 100644 index 0000000000..a4b60bd912 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-listener-moved-outside-container/_config.js @@ -0,0 +1,23 @@ +import { test } from '../../test'; + +// Tests that event delegation still works when the element with the event listener is moved outside the container +export default test({ + async test({ assert, target }) { + const btn1 = target.parentElement?.querySelector('button'); + const btn2 = target.querySelector('button'); + + btn1?.click(); + await Promise.resolve(); + assert.htmlEqual( + target.parentElement?.innerHTML ?? '', + '
' + ); + + btn2?.click(); + await Promise.resolve(); + assert.htmlEqual( + target.parentElement?.innerHTML ?? '', + '
' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/event-listener-moved-outside-container/main.svelte b/packages/svelte/tests/runtime-runes/samples/event-listener-moved-outside-container/main.svelte new file mode 100644 index 0000000000..47edcf7253 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-listener-moved-outside-container/main.svelte @@ -0,0 +1,12 @@ + + +
+ + +
From 5f3fcaf88a7068e9427ce96bf2a4bc7ff4209d62 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Tue, 2 Jan 2024 23:56:30 +0000 Subject: [PATCH 2/4] fix: bail-out event handler referencing each index (#10063) * fix: bail-out event handler referencing each index * lint * ts * types --- .changeset/tall-garlics-try.md | 5 +++++ packages/svelte/src/compiler/phases/2-analyze/index.js | 4 ++++ packages/svelte/src/compiler/phases/scope.js | 6 +++--- packages/svelte/src/compiler/types/index.d.ts | 10 ++++++++-- packages/svelte/types/index.d.ts | 10 ++++++++-- 5 files changed, 28 insertions(+), 7 deletions(-) create mode 100644 .changeset/tall-garlics-try.md diff --git a/.changeset/tall-garlics-try.md b/.changeset/tall-garlics-try.md new file mode 100644 index 0000000000..a7b45bdf69 --- /dev/null +++ b/.changeset/tall-garlics-try.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: bail-out event handler referencing each index diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index e7f9e1a727..dc98fb31e6 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -183,6 +183,10 @@ function get_delegated_event(node, context) { ) { return non_hoistable; } + // If we referebnce the index within an each block, then bail-out. + if (binding !== null && binding.initial?.type === 'EachBlock') { + return non_hoistable; + } if ( binding !== null && diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index ad3e8fdb34..0530535087 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -65,7 +65,7 @@ export class Scope { * @param {import('estree').Identifier} node * @param {import('#compiler').Binding['kind']} kind * @param {import('#compiler').DeclarationKind} declaration_kind - * @param {null | import('estree').Expression | import('estree').FunctionDeclaration | import('estree').ClassDeclaration | import('estree').ImportDeclaration} initial + * @param {null | import('estree').Expression | import('estree').FunctionDeclaration | import('estree').ClassDeclaration | import('estree').ImportDeclaration | import('../types/template.js').EachBlock} initial * @returns {import('#compiler').Binding} */ declare(node, kind, declaration_kind, initial = null) { @@ -523,7 +523,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { const is_keyed = node.key && (node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index); - scope.declare(b.id(node.index), is_keyed ? 'derived' : 'normal', 'const'); + scope.declare(b.id(node.index), is_keyed ? 'derived' : 'normal', 'const', node); } if (node.key) visit(node.key, { scope }); @@ -680,7 +680,7 @@ export function set_scope(scopes) { /** * Returns the name of the rune if the given expression is a `CallExpression` using a rune. - * @param {import('estree').Node | null | undefined} node + * @param {import('estree').Node | import('../types/template.js').EachBlock | null | undefined} node * @param {Scope} scope * @returns {Runes[number] | null} */ diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index fcb960847e..b62b47bf0d 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -11,7 +11,7 @@ import type { SourceMap } from 'magic-string'; import type { Context } from 'zimmerframe'; import type { Scope } from '../phases/scope.js'; import * as Css from './css.js'; -import type { Namespace, SvelteNode } from './template.js'; +import type { EachBlock, Namespace, SvelteNode } from './template.js'; /** The return value of `compile` from `svelte/compiler` */ export interface CompileResult { @@ -269,7 +269,13 @@ export interface Binding { * What the value was initialized with. * For destructured props such as `let { foo = 'bar' } = $props()` this is `'bar'` and not `$props()` */ - initial: null | Expression | FunctionDeclaration | ClassDeclaration | ImportDeclaration; + initial: + | null + | Expression + | FunctionDeclaration + | ClassDeclaration + | ImportDeclaration + | EachBlock; is_called: boolean; references: { node: Identifier; path: SvelteNode[] }[]; mutated: boolean; diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 4db8209706..7baeab4189 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -746,7 +746,13 @@ declare module 'svelte/compiler' { * What the value was initialized with. * For destructured props such as `let { foo = 'bar' } = $props()` this is `'bar'` and not `$props()` */ - initial: null | Expression | FunctionDeclaration | ClassDeclaration | ImportDeclaration; + initial: + | null + | Expression + | FunctionDeclaration + | ClassDeclaration + | ImportDeclaration + | EachBlock; is_called: boolean; references: { node: Identifier; path: SvelteNode[] }[]; mutated: boolean; @@ -1021,7 +1027,7 @@ declare module 'svelte/compiler' { */ function_depth: number; - declare(node: import('estree').Identifier, kind: Binding['kind'], declaration_kind: DeclarationKind, initial?: null | import('estree').Expression | import('estree').FunctionDeclaration | import('estree').ClassDeclaration | import('estree').ImportDeclaration): Binding; + declare(node: import('estree').Identifier, kind: Binding['kind'], declaration_kind: DeclarationKind, initial?: null | import('estree').Expression | import('estree').FunctionDeclaration | import('estree').ClassDeclaration | import('estree').ImportDeclaration | EachBlock): Binding; child(porous?: boolean): Scope; generate(preferred_name: string): string; From fa8aa2d481e1c052c89640df0dfa541e30934c10 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 3 Jan 2024 02:02:54 +0000 Subject: [PATCH 3/4] Version Packages (next) (#10052) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 10 ++++++++++ packages/svelte/CHANGELOG.md | 24 ++++++++++++++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index f2b203735f..138837b375 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -25,6 +25,8 @@ "cool-ants-leave", "cuddly-pianos-drop", "curly-lizards-dream", + "curvy-ties-shout", + "dirty-bats-punch", "dirty-garlics-design", "dirty-tips-add", "dry-clocks-grow", @@ -54,6 +56,7 @@ "happy-suits-film", "healthy-planes-vanish", "heavy-ears-rule", + "hip-balloons-begin", "honest-icons-change", "hungry-dots-fry", "hungry-tips-unite", @@ -80,6 +83,7 @@ "lucky-schools-hang", "moody-frogs-exist", "moody-owls-cry", + "nasty-lions-double", "neat-dingos-clap", "new-boats-wait", "ninety-dingos-walk", @@ -88,12 +92,14 @@ "odd-shoes-cheat", "old-flies-jog", "old-mails-sneeze", + "orange-dingos-poke", "polite-dolphins-care", "polite-pumpkins-guess", "polite-ravens-study", "poor-eggs-enjoy", "poor-seahorses-flash", "popular-mangos-rest", + "pretty-ties-help", "purple-dragons-peel", "quiet-camels-mate", "rare-pears-whisper", @@ -125,10 +131,12 @@ "strong-gifts-smoke", "strong-lemons-provide", "sweet-mangos-beg", + "sweet-pens-sniff", "swift-donkeys-perform", "swift-ravens-hunt", "swift-seahorses-deliver", "tall-books-grin", + "tall-garlics-try", "tall-shrimps-worry", "tall-tigers-wait", "tasty-numbers-perform", @@ -140,12 +148,14 @@ "thirty-ghosts-fix", "thirty-impalas-repair", "thirty-wombats-relax", + "three-suits-grin", "tiny-kings-whisper", "twelve-dragons-join", "twelve-onions-juggle", "two-dragons-yell", "two-falcons-buy", "unlucky-boxes-obey", + "unlucky-trees-lick", "wet-games-fly", "wicked-clouds-exercise", "wicked-doors-train", diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 0f9afb242d..e0a7194973 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,29 @@ # svelte +## 5.0.0-next.28 + +### Patch Changes + +- fix: deeply unstate objects passed to inspect ([#10056](https://github.com/sveltejs/svelte/pull/10056)) + +- fix: handle delegated events of elements moved outside the container ([#10060](https://github.com/sveltejs/svelte/pull/10060)) + +- fix: improve script `lang` attribute detection ([#10046](https://github.com/sveltejs/svelte/pull/10046)) + +- fix: improve pseudo class parsing ([#10055](https://github.com/sveltejs/svelte/pull/10055)) + +- fix: add types for popover attributes and events ([#10041](https://github.com/sveltejs/svelte/pull/10041)) + +- fix: skip generating $.proxy() calls for unary and binary expressions ([#9979](https://github.com/sveltejs/svelte/pull/9979)) + +- fix: allow pseudo classes after `:global(..)` ([#10055](https://github.com/sveltejs/svelte/pull/10055)) + +- fix: bail-out event handler referencing each index ([#10063](https://github.com/sveltejs/svelte/pull/10063)) + +- fix: parse `:nth-of-type(xn+y)` correctly ([#9970](https://github.com/sveltejs/svelte/pull/9970)) + +- fix: ensure if block is executed in correct order ([#10053](https://github.com/sveltejs/svelte/pull/10053)) + ## 5.0.0-next.27 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 3cde08871a..d464f21558 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.0.0-next.27", + "version": "5.0.0-next.28", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index ab3307900a..f2f1da80e4 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -6,5 +6,5 @@ * https://svelte.dev/docs/svelte-compiler#svelte-version * @type {string} */ -export const VERSION = '5.0.0-next.27'; +export const VERSION = '5.0.0-next.28'; export const PUBLIC_VERSION = '5'; From 2133d7d67afc9908b9b7803bf76a0e2755ff913a Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 3 Jan 2024 15:31:06 +0100 Subject: [PATCH 4/4] fix: support TS satisfies operator (#10068) fixes #9606 --- .changeset/light-humans-hang.md | 5 +++++ packages/svelte/package.json | 2 +- packages/svelte/src/compiler/phases/1-parse/acorn.js | 2 +- .../svelte/src/compiler/phases/3-transform/typescript.js | 7 +++++-- .../tests/runtime-runes/samples/typescript/main.svelte | 3 +++ pnpm-lock.yaml | 8 ++++---- 6 files changed, 19 insertions(+), 8 deletions(-) create mode 100644 .changeset/light-humans-hang.md diff --git a/.changeset/light-humans-hang.md b/.changeset/light-humans-hang.md new file mode 100644 index 0000000000..de1f6ed8ff --- /dev/null +++ b/.changeset/light-humans-hang.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: support TypeScript's `satisfies` operator diff --git a/packages/svelte/package.json b/packages/svelte/package.json index d464f21558..a8616f0a17 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -117,7 +117,7 @@ "@ampproject/remapping": "^2.2.1", "@jridgewell/sourcemap-codec": "^1.4.15", "acorn": "^8.10.0", - "acorn-typescript": "^1.4.11", + "acorn-typescript": "^1.4.13", "aria-query": "^5.3.0", "axobject-query": "^4.0.0", "esm-env": "^1.0.0", diff --git a/packages/svelte/src/compiler/phases/1-parse/acorn.js b/packages/svelte/src/compiler/phases/1-parse/acorn.js index ad23d47d6b..23fa756a9b 100644 --- a/packages/svelte/src/compiler/phases/1-parse/acorn.js +++ b/packages/svelte/src/compiler/phases/1-parse/acorn.js @@ -2,7 +2,7 @@ import * as acorn from 'acorn'; import { walk } from 'zimmerframe'; import { tsPlugin } from 'acorn-typescript'; -const ParserWithTS = acorn.Parser.extend(tsPlugin()); +const ParserWithTS = acorn.Parser.extend(tsPlugin({ allowSatisfies: true })); /** * @param {string} source diff --git a/packages/svelte/src/compiler/phases/3-transform/typescript.js b/packages/svelte/src/compiler/phases/3-transform/typescript.js index 9449ced6a0..e34d75691e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/typescript.js +++ b/packages/svelte/src/compiler/phases/3-transform/typescript.js @@ -41,13 +41,16 @@ export const remove_types = { TSAsExpression(node, context) { return context.visit(node.expression); }, + TSSatisfiesExpression(node, context) { + return context.visit(node.expression); + }, TSNonNullExpression(node, context) { return context.visit(node.expression); }, - TSInterfaceDeclaration(node, context) { + TSInterfaceDeclaration() { return b.empty; }, - TSTypeAliasDeclaration(node, context) { + TSTypeAliasDeclaration() { return b.empty; } }; diff --git a/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte b/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte index 724988b55a..e991aa7533 100644 --- a/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte @@ -10,6 +10,9 @@ import { type Bar, type Baz } from './types'; let count = $state(0); + const person = { + message: 'goodbye' + } satisfies Goodbye;