diff --git a/.changeset/giant-plants-grin.md b/.changeset/giant-plants-grin.md
new file mode 100644
index 000000000000..4f5db1712ac8
--- /dev/null
+++ b/.changeset/giant-plants-grin.md
@@ -0,0 +1,5 @@
+---
+"svelte": patch
+---
+
+fix: keep sibling selectors when dealing with slots/render tags/`svelte:element` tags
diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js
index c299612fd140..a5010c543a90 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js
@@ -175,7 +175,13 @@ function apply_selector(relative_selectors, rule, element, stylesheet) {
let sibling_matched = false;
for (const possible_sibling of siblings.keys()) {
- if (apply_selector(parent_selectors, rule, possible_sibling, stylesheet)) {
+ if (possible_sibling.type === 'RenderTag' || possible_sibling.type === 'SlotElement') {
+ // `{@render foo()}
foo
` with `:global(.x) + p` is a match
+ if (parent_selectors.length === 1 && parent_selectors[0].metadata.is_global) {
+ mark(relative_selector, element);
+ sibling_matched = true;
+ }
+ } else if (apply_selector(parent_selectors, rule, possible_sibling, stylesheet)) {
mark(relative_selector, element);
sibling_matched = true;
}
@@ -564,38 +570,39 @@ function get_element_parent(node) {
function find_previous_sibling(node) {
/** @type {import('#compiler').SvelteNode} */
let current_node = node;
- do {
- if (current_node.type === 'SlotElement') {
- const slot_children = current_node.fragment.nodes;
- if (slot_children.length > 0) {
- current_node = slot_children.slice(-1)[0]; // go to its last child first
- continue;
- }
- }
- while (
- // @ts-expect-error TODO
- !current_node.prev &&
- // @ts-expect-error TODO
- current_node.parent &&
- // @ts-expect-error TODO
- current_node.parent.type === 'SlotElement'
- ) {
- // @ts-expect-error TODO
- current_node = current_node.parent;
+
+ while (
+ // @ts-expect-error TODO
+ !current_node.prev &&
+ // @ts-expect-error TODO
+ current_node.parent?.type === 'SlotElement'
+ ) {
+ // @ts-expect-error TODO
+ current_node = current_node.parent;
+ }
+
+ // @ts-expect-error
+ current_node = current_node.prev;
+
+ while (current_node?.type === 'SlotElement') {
+ const slot_children = current_node.fragment.nodes;
+ if (slot_children.length > 0) {
+ current_node = slot_children.slice(-1)[0];
+ } else {
+ break;
}
- // @ts-expect-error
- current_node = current_node.prev;
- } while (current_node && current_node.type === 'SlotElement');
+ }
+
return current_node;
}
/**
* @param {import('#compiler').SvelteNode} node
* @param {boolean} adjacent_only
- * @returns {Map}
+ * @returns {Map}
*/
function get_possible_element_siblings(node, adjacent_only) {
- /** @type {Map} */
+ /** @type {Map} */
const result = new Map();
/** @type {import('#compiler').SvelteNode} */
@@ -618,6 +625,14 @@ function get_possible_element_siblings(node, adjacent_only) {
if (adjacent_only && has_definite_elements(possible_last_child)) {
return result;
}
+ } else if (
+ prev.type === 'SlotElement' ||
+ prev.type === 'RenderTag' ||
+ prev.type === 'SvelteElement'
+ ) {
+ result.set(prev, NODE_PROBABLY_EXISTS);
+ // Special case: slots, render tags and svelte:element tags could resolve to no siblings,
+ // so we want to continue until we find a definite sibling even with the adjacent-only combinator
}
}
@@ -720,7 +735,7 @@ function get_possible_last_child(relative_selector, adjacent_only) {
}
/**
- * @param {Map} result
+ * @param {Map} result
* @returns {boolean}
*/
function has_definite_elements(result) {
@@ -734,8 +749,9 @@ function has_definite_elements(result) {
}
/**
- * @param {Map} from
- * @param {Map} to
+ * @template T
+ * @param {Map} from
+ * @param {Map} to
* @returns {void}
*/
function add_to_map(from, to) {
diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/_config.js b/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/_config.js
new file mode 100644
index 000000000000..2143e5e57555
--- /dev/null
+++ b/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/_config.js
@@ -0,0 +1,13 @@
+import { test } from '../../test';
+
+export default test({
+ warnings: [
+ // TODO
+ // {
+ // code: 'css-unused-selector',
+ // message: 'Unused CSS selector ".a ~ .b"',
+ // start: { character: 111, column: 1, line: 10 },
+ // end: { character: 118, column: 8, line: 10 }
+ // },
+ ]
+});
diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/expected.css b/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/expected.css
new file mode 100644
index 000000000000..5495a803ef7c
--- /dev/null
+++ b/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/expected.css
@@ -0,0 +1,13 @@
+
+ .before.svelte-xyz + .foo:where(.svelte-xyz) { color: green; }
+ .before.svelte-xyz ~ .foo:where(.svelte-xyz) { color: green; }
+ .before.svelte-xyz ~ .bar:where(.svelte-xyz) { color: green; }
+
+ .x + .foo.svelte-xyz { color: green; }
+ .x + .foo.svelte-xyz span:where(.svelte-xyz) { color: green; }
+ .x ~ .foo.svelte-xyz { color: green; }
+ .x ~ .foo.svelte-xyz span:where(.svelte-xyz) { color: green; }
+ .x ~ .bar.svelte-xyz { color: green; }
+
+ /* no match */
+ /* (unused) :global(.x) + .bar { color: green; }*/
diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/input.svelte b/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/input.svelte
new file mode 100644
index 000000000000..1e2c6fdc2dd6
--- /dev/null
+++ b/packages/svelte/tests/css/samples/general-siblings-combinator-rendertag-global/input.svelte
@@ -0,0 +1,23 @@
+
+
before
+ {@render children()}
+
+ foo
+
+
bar
+
+
+
diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/_config.js b/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/_config.js
new file mode 100644
index 000000000000..2143e5e57555
--- /dev/null
+++ b/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/_config.js
@@ -0,0 +1,13 @@
+import { test } from '../../test';
+
+export default test({
+ warnings: [
+ // TODO
+ // {
+ // code: 'css-unused-selector',
+ // message: 'Unused CSS selector ".a ~ .b"',
+ // start: { character: 111, column: 1, line: 10 },
+ // end: { character: 118, column: 8, line: 10 }
+ // },
+ ]
+});
diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/expected.css b/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/expected.css
new file mode 100644
index 000000000000..5495a803ef7c
--- /dev/null
+++ b/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/expected.css
@@ -0,0 +1,13 @@
+
+ .before.svelte-xyz + .foo:where(.svelte-xyz) { color: green; }
+ .before.svelte-xyz ~ .foo:where(.svelte-xyz) { color: green; }
+ .before.svelte-xyz ~ .bar:where(.svelte-xyz) { color: green; }
+
+ .x + .foo.svelte-xyz { color: green; }
+ .x + .foo.svelte-xyz span:where(.svelte-xyz) { color: green; }
+ .x ~ .foo.svelte-xyz { color: green; }
+ .x ~ .foo.svelte-xyz span:where(.svelte-xyz) { color: green; }
+ .x ~ .bar.svelte-xyz { color: green; }
+
+ /* no match */
+ /* (unused) :global(.x) + .bar { color: green; }*/
diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/input.svelte b/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/input.svelte
new file mode 100644
index 000000000000..d556ea6b8b11
--- /dev/null
+++ b/packages/svelte/tests/css/samples/general-siblings-combinator-slot-global/input.svelte
@@ -0,0 +1,23 @@
+
+
before
+
+
+ foo
+
+
bar
+
+
+
diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/_config.js b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/_config.js
new file mode 100644
index 000000000000..2143e5e57555
--- /dev/null
+++ b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/_config.js
@@ -0,0 +1,13 @@
+import { test } from '../../test';
+
+export default test({
+ warnings: [
+ // TODO
+ // {
+ // code: 'css-unused-selector',
+ // message: 'Unused CSS selector ".a ~ .b"',
+ // start: { character: 111, column: 1, line: 10 },
+ // end: { character: 118, column: 8, line: 10 }
+ // },
+ ]
+});
diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/expected.css b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/expected.css
new file mode 100644
index 000000000000..830d3667024b
--- /dev/null
+++ b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/expected.css
@@ -0,0 +1,13 @@
+
+ .before.svelte-xyz + .foo:where(.svelte-xyz) { color: green; }
+ .before.svelte-xyz ~ .foo:where(.svelte-xyz) { color: green; }
+ .before.svelte-xyz ~ .bar:where(.svelte-xyz) { color: green; }
+
+ .x.svelte-xyz + .foo:where(.svelte-xyz) { color: green; }
+ .x.svelte-xyz + .foo:where(.svelte-xyz) span:where(.svelte-xyz) { color: green; }
+ .x.svelte-xyz ~ .foo:where(.svelte-xyz) { color: green; }
+ .x.svelte-xyz ~ .foo:where(.svelte-xyz) span:where(.svelte-xyz) { color: green; }
+ .x.svelte-xyz ~ .bar:where(.svelte-xyz) { color: green; }
+
+ /* no match */
+ /* (unused) .x + .bar { color: green; }*/
diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/input.svelte b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/input.svelte
new file mode 100644
index 000000000000..1c51a2c516a1
--- /dev/null
+++ b/packages/svelte/tests/css/samples/general-siblings-combinator-svelteelement/input.svelte
@@ -0,0 +1,27 @@
+
+
+
+
before
+
+
+ foo
+
+
bar
+
+
+