From f6f4e67028d69c779967c20311c32db07ed183c1 Mon Sep 17 00:00:00 2001 From: Bladesheng Date: Fri, 31 Oct 2025 12:50:24 +0100 Subject: [PATCH 01/10] feat: allow passing ShadowRootInit to custom element shadow options --- .../98-reference/.generated/compile-errors.md | 4 +- packages/svelte/elements.d.ts | 2 +- .../messages/compile-errors/template.md | 4 +- packages/svelte/src/compiler/errors.js | 4 +- .../compiler/phases/1-parse/read/options.js | 46 +++++++++++++++++-- .../3-transform/client/transform-client.js | 21 ++++++++- .../svelte/src/compiler/types/template.d.ts | 2 +- .../client/dom/elements/custom-element.js | 20 ++++---- packages/svelte/types/index.d.ts | 2 +- 9 files changed, 84 insertions(+), 21 deletions(-) diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index c5703c636b70..083b0810015a 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -1102,9 +1102,11 @@ Value must be %list%, if specified ### svelte_options_invalid_customelement_shadow ``` -"shadow" must be either "open" or "none" +"shadow" must be either "open", "none" or `ShadowRootInit` ``` +See https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#options for more information on valid shadow root constructor options + ### svelte_options_invalid_tagname ``` diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index 17ff10072998..34ed251400b3 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -2061,7 +2061,7 @@ export interface SvelteHTMLElements { | undefined | { tag?: string; - shadow?: 'open' | 'none' | undefined; + shadow?: 'open' | 'none' | ShadowRootInit | undefined; props?: | Record< string, diff --git a/packages/svelte/messages/compile-errors/template.md b/packages/svelte/messages/compile-errors/template.md index ac95bfe4a703..14c9be52e209 100644 --- a/packages/svelte/messages/compile-errors/template.md +++ b/packages/svelte/messages/compile-errors/template.md @@ -411,7 +411,9 @@ HTML restricts where certain elements can appear. In case of a violation the bro ## svelte_options_invalid_customelement_shadow -> "shadow" must be either "open" or "none" +> "shadow" must be either "open", "none" or `ShadowRootInit` + +See https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#options for more information on valid shadow root constructor options ## svelte_options_invalid_tagname diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 5e3968215f1e..0cc51e39a37d 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -1550,12 +1550,12 @@ export function svelte_options_invalid_customelement_props(node) { } /** - * "shadow" must be either "open" or "none" + * "shadow" must be either "open", "none" or `ShadowRootInit` * @param {null | number | NodeLike} node * @returns {never} */ export function svelte_options_invalid_customelement_shadow(node) { - e(node, 'svelte_options_invalid_customelement_shadow', `"shadow" must be either "open" or "none"\nhttps://svelte.dev/e/svelte_options_invalid_customelement_shadow`); + e(node, 'svelte_options_invalid_customelement_shadow', `"shadow" must be either "open", "none" or \`ShadowRootInit\`\nhttps://svelte.dev/e/svelte_options_invalid_customelement_shadow`); } /** diff --git a/packages/svelte/src/compiler/phases/1-parse/read/options.js b/packages/svelte/src/compiler/phases/1-parse/read/options.js index a36e10146809..24b5f6967bf7 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/options.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/options.js @@ -133,11 +133,49 @@ export default function read_options(node) { const shadow = properties.find(([name]) => name === 'shadow')?.[1]; if (shadow) { - const shadowdom = shadow?.value; - if (shadowdom !== 'open' && shadowdom !== 'none') { - e.svelte_options_invalid_customelement_shadow(shadow); + if (shadow.type === 'Literal') { + if (shadow.value !== 'open' && shadow.value !== 'none') { + e.svelte_options_invalid_customelement_shadow(attribute); + } + ce.shadow = shadow.value; + } else if (shadow.type === 'ObjectExpression') { + ce.shadow = { mode: 'open' }; + for (const property of /** @type {ObjectExpression} */ (shadow).properties) { + if ( + property.type !== 'Property' || + property.computed || + property.key.type !== 'Identifier' || + property.value.type !== 'Literal' + ) { + e.svelte_options_invalid_customelement_shadow(attribute); + } + + if (property.key.name === 'mode') { + if (!['open', 'closed'].includes(/** @type {string} */ (property.value.value))) { + e.svelte_options_invalid_customelement_shadow(attribute); + } + ce.shadow.mode = /** @type {any} */ (property.value.value); + } else if (property.key.name === 'slotAssignment') { + if (!['named', 'manual'].includes(/** @type {string} */ (property.value.value))) { + e.svelte_options_invalid_customelement_shadow(attribute); + } + ce.shadow.slotAssignment = /** @type {any} */ (property.value.value); + } else if ( + property.key.name === 'clonable' || + property.key.name === 'delegatesFocus' || + property.key.name === 'serializable' + ) { + if (typeof property.value.value !== 'boolean') { + e.svelte_options_invalid_customelement_shadow(attribute); + } + ce.shadow[property.key.name] = property.value.value; + } else { + e.svelte_options_invalid_customelement_shadow(attribute); + } + } + } else { + e.svelte_options_invalid_customelement_shadow(attribute); } - ce.shadow = shadowdom; } const extend = properties.find(([name]) => name === 'extend')?.[1]; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index f51042eb7c62..664d9801499e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -643,7 +643,24 @@ export function client_component(analysis, options) { const accessors_str = b.array( analysis.exports.map(({ name, alias }) => b.literal(alias ?? name)) ); - const use_shadow_dom = typeof ce === 'boolean' || ce.shadow !== 'none' ? true : false; + + /** @type {ShadowRootInit | {}} */ + let ce_shadow_root_init = {}; + if (typeof ce === 'boolean' || ce.shadow === 'open' || ce.shadow === undefined) { + ce_shadow_root_init = { mode: 'open' }; + } else if (ce.shadow === 'none') { + ce_shadow_root_init = {}; + } else if (typeof ce.shadow === 'object') { + ce_shadow_root_init = ce.shadow; + } + + /** @type {ESTree.Property[]} */ + const shadow_root_init_str = Object.entries(ce_shadow_root_init).map(([key, value]) => + b.init(key, b.literal(value)) + ); + const shadow_root_init = shadow_root_init_str.length + ? b.object(shadow_root_init_str) + : undefined; const create_ce = b.call( '$.create_custom_element', @@ -651,7 +668,7 @@ export function client_component(analysis, options) { b.object(props_str), slots_str, accessors_str, - b.literal(use_shadow_dom), + shadow_root_init, /** @type {any} */ (typeof ce !== 'boolean' ? ce.extend : undefined) ); diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 9cf498d1871d..039c508727cc 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -88,7 +88,7 @@ export namespace AST { css?: 'injected'; customElement?: { tag?: string; - shadow?: 'open' | 'none'; + shadow?: 'open' | 'none' | (ShadowRootInit & { clonable?: boolean }); props?: Record< string, { diff --git a/packages/svelte/src/internal/client/dom/elements/custom-element.js b/packages/svelte/src/internal/client/dom/elements/custom-element.js index 2d118bfab3a4..d4e550291b90 100644 --- a/packages/svelte/src/internal/client/dom/elements/custom-element.js +++ b/packages/svelte/src/internal/client/dom/elements/custom-element.js @@ -35,18 +35,22 @@ if (typeof HTMLElement === 'function') { $$l_u = new Map(); /** @type {any} The managed render effect for reflecting attributes */ $$me; + /** @type {ShadowRoot | null} The ShadowRoot of the custom element */ + $$shadowRoot = null; /** * @param {*} $$componentCtor * @param {*} $$slots - * @param {*} use_shadow_dom + * @param {ShadowRootInit | undefined} shadow_root_init */ - constructor($$componentCtor, $$slots, use_shadow_dom) { + constructor($$componentCtor, $$slots, shadow_root_init) { super(); this.$$ctor = $$componentCtor; this.$$s = $$slots; - if (use_shadow_dom) { - this.attachShadow({ mode: 'open' }); + if (shadow_root_init) { + // We need to store the reference to shadow root, because `closed` shadow root cannot be + // accessed with `this.shadowRoot`. + this.$$shadowRoot = this.attachShadow(shadow_root_init); } } @@ -136,7 +140,7 @@ if (typeof HTMLElement === 'function') { } this.$$c = createClassComponent({ component: this.$$ctor, - target: this.shadowRoot || this, + target: this.$$shadowRoot || this, props: { ...this.$$d, $$slots, @@ -277,7 +281,7 @@ function get_custom_elements_slots(element) { * @param {Record} props_definition The props to observe * @param {string[]} slots The slots to create * @param {string[]} exports Explicitly exported values, other than props - * @param {boolean} use_shadow_dom Whether to use shadow DOM + * @param {ShadowRootInit | undefined} shadow_root_init Options passed to shadow DOM constructor * @param {(ce: new () => HTMLElement) => new () => HTMLElement} [extend] */ export function create_custom_element( @@ -285,12 +289,12 @@ export function create_custom_element( props_definition, slots, exports, - use_shadow_dom, + shadow_root_init, extend ) { let Class = class extends SvelteElement { constructor() { - super(Component, slots, use_shadow_dom); + super(Component, slots, shadow_root_init); this.$$p_d = props_definition; } static get observedAttributes() { diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 5e3ca77eb5cd..350ebcd1cbca 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1224,7 +1224,7 @@ declare module 'svelte/compiler' { css?: 'injected'; customElement?: { tag?: string; - shadow?: 'open' | 'none'; + shadow?: 'open' | 'none' | (ShadowRootInit & { clonable?: boolean }); props?: Record< string, { From 33eeefa3baa8bb7aef8d1eabd8f54407c4e71071 Mon Sep 17 00:00:00 2001 From: Bladesheng Date: Fri, 31 Oct 2025 15:10:29 +0100 Subject: [PATCH 02/10] tests --- .../closed-shadow-dom/_config.js | 13 +++++++++++++ .../closed-shadow-dom/main.svelte | 3 +++ .../shadow-root-init-options/_config.js | 18 ++++++++++++++++++ .../shadow-root-init-options/main.svelte | 14 ++++++++++++++ 4 files changed, 48 insertions(+) create mode 100644 packages/svelte/tests/runtime-browser/custom-elements-samples/closed-shadow-dom/_config.js create mode 100644 packages/svelte/tests/runtime-browser/custom-elements-samples/closed-shadow-dom/main.svelte create mode 100644 packages/svelte/tests/runtime-browser/custom-elements-samples/shadow-root-init-options/_config.js create mode 100644 packages/svelte/tests/runtime-browser/custom-elements-samples/shadow-root-init-options/main.svelte diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/closed-shadow-dom/_config.js b/packages/svelte/tests/runtime-browser/custom-elements-samples/closed-shadow-dom/_config.js new file mode 100644 index 000000000000..9e09967dab0c --- /dev/null +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/closed-shadow-dom/_config.js @@ -0,0 +1,13 @@ +import { test } from '../../assert'; +const tick = () => Promise.resolve(); + +export default test({ + async test({ assert, target }) { + target.innerHTML = ''; + await tick(); + + const el = target.querySelector('custom-element'); + + assert.equal(el.shadowRoot, null); + } +}); diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/closed-shadow-dom/main.svelte b/packages/svelte/tests/runtime-browser/custom-elements-samples/closed-shadow-dom/main.svelte new file mode 100644 index 000000000000..93744481b38d --- /dev/null +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/closed-shadow-dom/main.svelte @@ -0,0 +1,3 @@ + + +

Hello world!

diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/shadow-root-init-options/_config.js b/packages/svelte/tests/runtime-browser/custom-elements-samples/shadow-root-init-options/_config.js new file mode 100644 index 000000000000..106d27929ee4 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/shadow-root-init-options/_config.js @@ -0,0 +1,18 @@ +import { test } from '../../assert'; +const tick = () => Promise.resolve(); + +export default test({ + async test({ assert, target }) { + target.innerHTML = ''; + await tick(); + + /** @type {ShadowRoot} */ + const shadowRoot = target.querySelector('custom-element').shadowRoot; + + assert.equal(shadowRoot.mode, 'open'); + assert.equal(shadowRoot.clonable, true); + assert.equal(shadowRoot.delegatesFocus, true); + assert.equal(shadowRoot.serializable, true); + assert.equal(shadowRoot.slotAssignment, 'manual'); + } +}); diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/shadow-root-init-options/main.svelte b/packages/svelte/tests/runtime-browser/custom-elements-samples/shadow-root-init-options/main.svelte new file mode 100644 index 000000000000..25d69d7ef9ee --- /dev/null +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/shadow-root-init-options/main.svelte @@ -0,0 +1,14 @@ + + +

Hello world!

From 15dde952ee8e32939a9859304f8290794e44dd53 Mon Sep 17 00:00:00 2001 From: Bladesheng Date: Mon, 3 Nov 2025 08:16:10 +0100 Subject: [PATCH 03/10] add changeset --- .changeset/loose-sloths-guess.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/loose-sloths-guess.md diff --git a/.changeset/loose-sloths-guess.md b/.changeset/loose-sloths-guess.md new file mode 100644 index 000000000000..038aab0fa236 --- /dev/null +++ b/.changeset/loose-sloths-guess.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: allow passing `ShadowRootInit` to custom element `shadow` option From c02e3d407b73364bdd1c648c8faef9e02527f5d2 Mon Sep 17 00:00:00 2001 From: Bladesheng Date: Mon, 3 Nov 2025 08:44:05 +0100 Subject: [PATCH 04/10] update docs --- documentation/docs/07-misc/04-custom-elements.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/documentation/docs/07-misc/04-custom-elements.md b/documentation/docs/07-misc/04-custom-elements.md index 4e5afff7d251..4fecde115f5c 100644 --- a/documentation/docs/07-misc/04-custom-elements.md +++ b/documentation/docs/07-misc/04-custom-elements.md @@ -66,7 +66,10 @@ The inner Svelte component is destroyed in the next tick after the `disconnected When constructing a custom element, you can tailor several aspects by defining `customElement` as an object within `` since Svelte 4. This object may contain the following properties: - `tag: string`: an optional `tag` property for the custom element's name. If set, a custom element with this tag name will be defined with the document's `customElements` registry upon importing this component. -- `shadow`: an optional property that can be set to `"none"` to forgo shadow root creation. Note that styles are then no longer encapsulated, and you can't use slots +- `shadow`: an optional property to modify shadow root properties. It accepts the following values: + - `"none"`: No shadow root is created. Note that styles are then no longer encapsulated, and you can't use slots. + - `"open"`: Shadow root is created with the `mode: "open"` option. + - [`ShadowRootInit`](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#options): You can pass settings object that will be passed to `attachShadow()` when shadow root is created. - `props`: an optional property to modify certain details and behaviors of your component's properties. It offers the following settings: - `attribute: string`: To update a custom element's prop, you have two alternatives: either set the property on the custom element's reference as illustrated above or use an HTML attribute. For the latter, the default attribute name is the lowercase property name. Modify this by assigning `attribute: ""`. - `reflect: boolean`: By default, updated prop values do not reflect back to the DOM. To enable this behavior, set `reflect: true`. From f4294e378d64d4e85b503c5d5ee83bc67e8c94f2 Mon Sep 17 00:00:00 2001 From: Bladesheng Date: Mon, 3 Nov 2025 13:43:06 +0100 Subject: [PATCH 05/10] allow passing functions --- .../docs/07-misc/04-custom-elements.md | 10 +++++-- packages/svelte/elements.d.ts | 7 ++++- .../messages/compile-errors/template.md | 4 +-- packages/svelte/src/compiler/errors.js | 10 +++---- .../compiler/phases/1-parse/read/options.js | 2 ++ .../3-transform/client/transform-client.js | 27 ++++++++++--------- .../svelte/src/compiler/types/template.d.ts | 6 ++++- .../client/dom/elements/custom-element.js | 11 +++++--- .../_config.js | 19 +++++++++++++ .../main.svelte | 12 +++++++++ packages/svelte/types/index.d.ts | 6 ++++- 11 files changed, 85 insertions(+), 29 deletions(-) create mode 100644 packages/svelte/tests/runtime-browser/custom-elements-samples/shadow-root-init-options-function/_config.js create mode 100644 packages/svelte/tests/runtime-browser/custom-elements-samples/shadow-root-init-options-function/main.svelte diff --git a/documentation/docs/07-misc/04-custom-elements.md b/documentation/docs/07-misc/04-custom-elements.md index 4fecde115f5c..9b457c6cdcfc 100644 --- a/documentation/docs/07-misc/04-custom-elements.md +++ b/documentation/docs/07-misc/04-custom-elements.md @@ -69,7 +69,7 @@ When constructing a custom element, you can tailor several aspects by defining ` - `shadow`: an optional property to modify shadow root properties. It accepts the following values: - `"none"`: No shadow root is created. Note that styles are then no longer encapsulated, and you can't use slots. - `"open"`: Shadow root is created with the `mode: "open"` option. - - [`ShadowRootInit`](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#options): You can pass settings object that will be passed to `attachShadow()` when shadow root is created. + - [`ShadowRootInit`](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#options): You can pass a settings object that will be passed to `attachShadow()` when shadow root is created. Alternatively, you can pass function that returns the options object. This comes in handy if you need to dynamically change the options values - for example, based on value of environment variables. - `props`: an optional property to modify certain details and behaviors of your component's properties. It offers the following settings: - `attribute: string`: To update a custom element's prop, you have two alternatives: either set the property on the custom element's reference as illustrated above or use an HTML attribute. For the latter, the default attribute name is the lowercase property name. Modify this by assigning `attribute: ""`. - `reflect: boolean`: By default, updated prop values do not reflect back to the DOM. To enable this behavior, set `reflect: true`. @@ -81,7 +81,13 @@ When constructing a custom element, you can tailor several aspects by defining ` ({ + mode: import.meta.env.DEV ? 'open' : 'closed', + clonable: true, + delegatesFocus: true, + serializable: true, + slotAssignment: 'manual', + }), props: { name: { reflect: true, type: 'Number', attribute: 'element-index' } }, diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index 34ed251400b3..3bb6e1e41f3a 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -2061,7 +2061,12 @@ export interface SvelteHTMLElements { | undefined | { tag?: string; - shadow?: 'open' | 'none' | ShadowRootInit | undefined; + shadow?: + | 'open' + | 'none' + | ShadowRootInit + | (() => ShadowRootInit | undefined) + | undefined; props?: | Record< string, diff --git a/packages/svelte/messages/compile-errors/template.md b/packages/svelte/messages/compile-errors/template.md index 14c9be52e209..7886a84dd681 100644 --- a/packages/svelte/messages/compile-errors/template.md +++ b/packages/svelte/messages/compile-errors/template.md @@ -403,7 +403,7 @@ HTML restricts where certain elements can appear. In case of a violation the bro ## svelte_options_invalid_customelement -> "customElement" must be a string literal defining a valid custom element name or an object of the form { tag?: string; shadow?: "open" | "none"; props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } } +> "customElement" must be a string literal defining a valid custom element name or an object of the form { tag?: string; shadow?: "open" | "none" | `ShadowRootInit` | (() => `ShadowRootInit` | undefined); props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } } ## svelte_options_invalid_customelement_props @@ -411,7 +411,7 @@ HTML restricts where certain elements can appear. In case of a violation the bro ## svelte_options_invalid_customelement_shadow -> "shadow" must be either "open", "none" or `ShadowRootInit` +> "shadow" must be either "open", "none", `ShadowRootInit` or function that returns `ShadowRootInit` See https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#options for more information on valid shadow root constructor options diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 0cc51e39a37d..92b671b6b658 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -1532,12 +1532,12 @@ export function svelte_options_invalid_attribute_value(node, list) { } /** - * "customElement" must be a string literal defining a valid custom element name or an object of the form { tag?: string; shadow?: "open" | "none"; props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } } + * "customElement" must be a string literal defining a valid custom element name or an object of the form { tag?: string; shadow?: "open" | "none" | `ShadowRootInit` | (() => `ShadowRootInit` | undefined); props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } } * @param {null | number | NodeLike} node * @returns {never} */ export function svelte_options_invalid_customelement(node) { - e(node, 'svelte_options_invalid_customelement', `"customElement" must be a string literal defining a valid custom element name or an object of the form { tag?: string; shadow?: "open" | "none"; props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } }\nhttps://svelte.dev/e/svelte_options_invalid_customelement`); + e(node, 'svelte_options_invalid_customelement', `"customElement" must be a string literal defining a valid custom element name or an object of the form { tag?: string; shadow?: "open" | "none" | \`ShadowRootInit\` | (() => \`ShadowRootInit\` | undefined); props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } }\nhttps://svelte.dev/e/svelte_options_invalid_customelement`); } /** @@ -1550,12 +1550,12 @@ export function svelte_options_invalid_customelement_props(node) { } /** - * "shadow" must be either "open", "none" or `ShadowRootInit` + * "shadow" must be either "open", "none", `ShadowRootInit` or function that returns `ShadowRootInit` * @param {null | number | NodeLike} node * @returns {never} */ export function svelte_options_invalid_customelement_shadow(node) { - e(node, 'svelte_options_invalid_customelement_shadow', `"shadow" must be either "open", "none" or \`ShadowRootInit\`\nhttps://svelte.dev/e/svelte_options_invalid_customelement_shadow`); + e(node, 'svelte_options_invalid_customelement_shadow', `"shadow" must be either "open", "none", \`ShadowRootInit\` or function that returns \`ShadowRootInit\`\nhttps://svelte.dev/e/svelte_options_invalid_customelement_shadow`); } /** @@ -1698,4 +1698,4 @@ export function unterminated_string_constant(node) { */ export function void_element_invalid_content(node) { e(node, 'void_element_invalid_content', `Void elements cannot have children or closing tags\nhttps://svelte.dev/e/void_element_invalid_content`); -} \ No newline at end of file +} diff --git a/packages/svelte/src/compiler/phases/1-parse/read/options.js b/packages/svelte/src/compiler/phases/1-parse/read/options.js index 24b5f6967bf7..5c7ae325345d 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/options.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/options.js @@ -173,6 +173,8 @@ export default function read_options(node) { e.svelte_options_invalid_customelement_shadow(attribute); } } + } else if (shadow.type === 'ArrowFunctionExpression') { + ce.shadow = shadow; } else { e.svelte_options_invalid_customelement_shadow(attribute); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 664d9801499e..3eab0b6c25f6 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -644,23 +644,24 @@ export function client_component(analysis, options) { analysis.exports.map(({ name, alias }) => b.literal(alias ?? name)) ); - /** @type {ShadowRootInit | {}} */ - let ce_shadow_root_init = {}; + /** @type {ESTree.ObjectExpression | ESTree.ArrowFunctionExpression | undefined} */ + let shadow_root_init = undefined; if (typeof ce === 'boolean' || ce.shadow === 'open' || ce.shadow === undefined) { - ce_shadow_root_init = { mode: 'open' }; + shadow_root_init = b.object([b.init('mode', b.literal('open'))]); } else if (ce.shadow === 'none') { - ce_shadow_root_init = {}; + shadow_root_init = undefined; + } else if ('type' in ce.shadow && ce.shadow.type === 'ArrowFunctionExpression') { + shadow_root_init = /** @type {ESTree.ArrowFunctionExpression} */ (ce.shadow); } else if (typeof ce.shadow === 'object') { - ce_shadow_root_init = ce.shadow; - } + /** @type {ESTree.Property[]} */ + const shadow_root_init_props = Object.entries(ce.shadow).map(([key, value]) => + b.init(key, b.literal(value)) + ); - /** @type {ESTree.Property[]} */ - const shadow_root_init_str = Object.entries(ce_shadow_root_init).map(([key, value]) => - b.init(key, b.literal(value)) - ); - const shadow_root_init = shadow_root_init_str.length - ? b.object(shadow_root_init_str) - : undefined; + shadow_root_init = shadow_root_init_props.length + ? b.object(shadow_root_init_props) + : undefined; + } const create_ce = b.call( '$.create_custom_element', diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 039c508727cc..94c010b72317 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -88,7 +88,11 @@ export namespace AST { css?: 'injected'; customElement?: { tag?: string; - shadow?: 'open' | 'none' | (ShadowRootInit & { clonable?: boolean }); + shadow?: + | 'open' + | 'none' + | (ShadowRootInit & { clonable?: boolean }) + | ArrowFunctionExpression; props?: Record< string, { diff --git a/packages/svelte/src/internal/client/dom/elements/custom-element.js b/packages/svelte/src/internal/client/dom/elements/custom-element.js index d4e550291b90..3caddb61c471 100644 --- a/packages/svelte/src/internal/client/dom/elements/custom-element.js +++ b/packages/svelte/src/internal/client/dom/elements/custom-element.js @@ -41,16 +41,19 @@ if (typeof HTMLElement === 'function') { /** * @param {*} $$componentCtor * @param {*} $$slots - * @param {ShadowRootInit | undefined} shadow_root_init + * @param {ShadowRootInit | (() => ShadowRootInit | undefined) | undefined} shadow_root_init */ constructor($$componentCtor, $$slots, shadow_root_init) { super(); this.$$ctor = $$componentCtor; this.$$s = $$slots; - if (shadow_root_init) { + + const shadow_root_init_value = + typeof shadow_root_init === 'function' ? shadow_root_init() : shadow_root_init; + if (shadow_root_init_value) { // We need to store the reference to shadow root, because `closed` shadow root cannot be // accessed with `this.shadowRoot`. - this.$$shadowRoot = this.attachShadow(shadow_root_init); + this.$$shadowRoot = this.attachShadow(shadow_root_init_value); } } @@ -281,7 +284,7 @@ function get_custom_elements_slots(element) { * @param {Record} props_definition The props to observe * @param {string[]} slots The slots to create * @param {string[]} exports Explicitly exported values, other than props - * @param {ShadowRootInit | undefined} shadow_root_init Options passed to shadow DOM constructor + * @param {ShadowRootInit | (() => ShadowRootInit | undefined) | undefined} shadow_root_init Options passed to shadow DOM constructor * @param {(ce: new () => HTMLElement) => new () => HTMLElement} [extend] */ export function create_custom_element( diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/shadow-root-init-options-function/_config.js b/packages/svelte/tests/runtime-browser/custom-elements-samples/shadow-root-init-options-function/_config.js new file mode 100644 index 000000000000..9a90264b93cd --- /dev/null +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/shadow-root-init-options-function/_config.js @@ -0,0 +1,19 @@ +import { test } from '../../assert'; +const tick = () => Promise.resolve(); + +export default test({ + async test({ assert, target, window }) { + window.temp_variable = true; + + target.innerHTML = ''; + await tick(); + + /** @type {ShadowRoot} */ + const shadowRoot = target.querySelector('custom-element').shadowRoot; + + assert.equal(shadowRoot.mode, 'open'); + assert.equal(shadowRoot.clonable, true); + + delete window.temp_variable; + } +}); diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/shadow-root-init-options-function/main.svelte b/packages/svelte/tests/runtime-browser/custom-elements-samples/shadow-root-init-options-function/main.svelte new file mode 100644 index 000000000000..0d66ed2f3557 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/shadow-root-init-options-function/main.svelte @@ -0,0 +1,12 @@ + ({ + mode: 'open', + // This could also be some env variable. + clonable: window.temp_variable + }) + }} +/> + +

Hello world!

diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 350ebcd1cbca..e0eb8ec184bb 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1224,7 +1224,11 @@ declare module 'svelte/compiler' { css?: 'injected'; customElement?: { tag?: string; - shadow?: 'open' | 'none' | (ShadowRootInit & { clonable?: boolean }); + shadow?: + | 'open' + | 'none' + | (ShadowRootInit & { clonable?: boolean }) + | ArrowFunctionExpression; props?: Record< string, { From 1cfafe0be910a5062c3cfe975699793db0346bfa Mon Sep 17 00:00:00 2001 From: Bladesheng Date: Mon, 3 Nov 2025 13:48:28 +0100 Subject: [PATCH 06/10] remove newline --- packages/svelte/src/compiler/errors.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 92b671b6b658..bc20b736c378 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -1698,4 +1698,4 @@ export function unterminated_string_constant(node) { */ export function void_element_invalid_content(node) { e(node, 'void_element_invalid_content', `Void elements cannot have children or closing tags\nhttps://svelte.dev/e/void_element_invalid_content`); -} +} \ No newline at end of file From aa9b12aaac3fbc20a38fd6f109e253118d927dcb Mon Sep 17 00:00:00 2001 From: Bladesheng Date: Mon, 3 Nov 2025 13:57:20 +0100 Subject: [PATCH 07/10] update docs --- documentation/docs/98-reference/.generated/compile-errors.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index 083b0810015a..4eca5cdbae6a 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -1090,7 +1090,7 @@ Value must be %list%, if specified ### svelte_options_invalid_customelement ``` -"customElement" must be a string literal defining a valid custom element name or an object of the form { tag?: string; shadow?: "open" | "none"; props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } } +"customElement" must be a string literal defining a valid custom element name or an object of the form { tag?: string; shadow?: "open" | "none" | `ShadowRootInit` | (() => `ShadowRootInit` | undefined); props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } } ``` ### svelte_options_invalid_customelement_props @@ -1102,7 +1102,7 @@ Value must be %list%, if specified ### svelte_options_invalid_customelement_shadow ``` -"shadow" must be either "open", "none" or `ShadowRootInit` +"shadow" must be either "open", "none", `ShadowRootInit` or function that returns `ShadowRootInit` ``` See https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#options for more information on valid shadow root constructor options From 4a8db914e2e3ca1a293ad1807adec59306ee3b16 Mon Sep 17 00:00:00 2001 From: Bladesheng Date: Mon, 3 Nov 2025 13:57:26 +0100 Subject: [PATCH 08/10] fix test --- .../svelte/tests/validator/samples/tag-non-string/errors.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/tests/validator/samples/tag-non-string/errors.json b/packages/svelte/tests/validator/samples/tag-non-string/errors.json index 71f8df4d0092..7dafea7ee21b 100644 --- a/packages/svelte/tests/validator/samples/tag-non-string/errors.json +++ b/packages/svelte/tests/validator/samples/tag-non-string/errors.json @@ -1,7 +1,7 @@ [ { "code": "svelte_options_invalid_customelement", - "message": "\"customElement\" must be a string literal defining a valid custom element name or an object of the form { tag?: string; shadow?: \"open\" | \"none\"; props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } }", + "message": "\"customElement\" must be a string literal defining a valid custom element name or an object of the form { tag?: string; shadow?: \"open\" | \"none\" | `ShadowRootInit` | (() => `ShadowRootInit` | undefined); props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } }", "start": { "line": 1, "column": 16 From 50f5986fffcdc8f32408b8f7dfbb05066e2e38fa Mon Sep 17 00:00:00 2001 From: Bladesheng Date: Mon, 3 Nov 2025 14:03:31 +0100 Subject: [PATCH 09/10] update docs --- documentation/docs/07-misc/04-custom-elements.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/documentation/docs/07-misc/04-custom-elements.md b/documentation/docs/07-misc/04-custom-elements.md index 9b457c6cdcfc..9d86172ae0bb 100644 --- a/documentation/docs/07-misc/04-custom-elements.md +++ b/documentation/docs/07-misc/04-custom-elements.md @@ -84,9 +84,7 @@ When constructing a custom element, you can tailor several aspects by defining ` shadow: () => ({ mode: import.meta.env.DEV ? 'open' : 'closed', clonable: true, - delegatesFocus: true, - serializable: true, - slotAssignment: 'manual', + // ... }), props: { name: { reflect: true, type: 'Number', attribute: 'element-index' } From 8d32602f99cf7e9854fbcbc6cba51abbd61cb185 Mon Sep 17 00:00:00 2001 From: Bladesheng Date: Mon, 3 Nov 2025 14:52:16 +0100 Subject: [PATCH 10/10] simplify options reading --- .../compiler/phases/1-parse/read/options.js | 45 +++---------------- .../3-transform/client/transform-client.js | 15 ++----- .../svelte/src/compiler/types/template.d.ts | 6 +-- packages/svelte/types/index.d.ts | 6 +-- 4 files changed, 10 insertions(+), 62 deletions(-) diff --git a/packages/svelte/src/compiler/phases/1-parse/read/options.js b/packages/svelte/src/compiler/phases/1-parse/read/options.js index 5c7ae325345d..4e15548cced3 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/options.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/options.js @@ -133,47 +133,12 @@ export default function read_options(node) { const shadow = properties.find(([name]) => name === 'shadow')?.[1]; if (shadow) { - if (shadow.type === 'Literal') { - if (shadow.value !== 'open' && shadow.value !== 'none') { - e.svelte_options_invalid_customelement_shadow(attribute); - } + if (shadow.type === 'Literal' && (shadow.value === 'open' || shadow.value === 'none')) { ce.shadow = shadow.value; - } else if (shadow.type === 'ObjectExpression') { - ce.shadow = { mode: 'open' }; - for (const property of /** @type {ObjectExpression} */ (shadow).properties) { - if ( - property.type !== 'Property' || - property.computed || - property.key.type !== 'Identifier' || - property.value.type !== 'Literal' - ) { - e.svelte_options_invalid_customelement_shadow(attribute); - } - - if (property.key.name === 'mode') { - if (!['open', 'closed'].includes(/** @type {string} */ (property.value.value))) { - e.svelte_options_invalid_customelement_shadow(attribute); - } - ce.shadow.mode = /** @type {any} */ (property.value.value); - } else if (property.key.name === 'slotAssignment') { - if (!['named', 'manual'].includes(/** @type {string} */ (property.value.value))) { - e.svelte_options_invalid_customelement_shadow(attribute); - } - ce.shadow.slotAssignment = /** @type {any} */ (property.value.value); - } else if ( - property.key.name === 'clonable' || - property.key.name === 'delegatesFocus' || - property.key.name === 'serializable' - ) { - if (typeof property.value.value !== 'boolean') { - e.svelte_options_invalid_customelement_shadow(attribute); - } - ce.shadow[property.key.name] = property.value.value; - } else { - e.svelte_options_invalid_customelement_shadow(attribute); - } - } - } else if (shadow.type === 'ArrowFunctionExpression') { + } else if ( + shadow.type === 'ObjectExpression' || + shadow.type === 'ArrowFunctionExpression' + ) { ce.shadow = shadow; } else { e.svelte_options_invalid_customelement_shadow(attribute); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 3eab0b6c25f6..c33938e65959 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -645,22 +645,13 @@ export function client_component(analysis, options) { ); /** @type {ESTree.ObjectExpression | ESTree.ArrowFunctionExpression | undefined} */ - let shadow_root_init = undefined; + let shadow_root_init; if (typeof ce === 'boolean' || ce.shadow === 'open' || ce.shadow === undefined) { shadow_root_init = b.object([b.init('mode', b.literal('open'))]); } else if (ce.shadow === 'none') { shadow_root_init = undefined; - } else if ('type' in ce.shadow && ce.shadow.type === 'ArrowFunctionExpression') { - shadow_root_init = /** @type {ESTree.ArrowFunctionExpression} */ (ce.shadow); - } else if (typeof ce.shadow === 'object') { - /** @type {ESTree.Property[]} */ - const shadow_root_init_props = Object.entries(ce.shadow).map(([key, value]) => - b.init(key, b.literal(value)) - ); - - shadow_root_init = shadow_root_init_props.length - ? b.object(shadow_root_init_props) - : undefined; + } else { + shadow_root_init = ce.shadow; } const create_ce = b.call( diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 94c010b72317..8950ff273c93 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -88,11 +88,7 @@ export namespace AST { css?: 'injected'; customElement?: { tag?: string; - shadow?: - | 'open' - | 'none' - | (ShadowRootInit & { clonable?: boolean }) - | ArrowFunctionExpression; + shadow?: 'open' | 'none' | ObjectExpression | ArrowFunctionExpression | undefined; props?: Record< string, { diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index e0eb8ec184bb..ed188d6c36ba 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1224,11 +1224,7 @@ declare module 'svelte/compiler' { css?: 'injected'; customElement?: { tag?: string; - shadow?: - | 'open' - | 'none' - | (ShadowRootInit & { clonable?: boolean }) - | ArrowFunctionExpression; + shadow?: 'open' | 'none' | ObjectExpression | ArrowFunctionExpression | undefined; props?: Record< string, {