diff --git a/documentation/docs/07-misc/04-custom-elements.md b/documentation/docs/07-misc/04-custom-elements.md index 4fecde115f..9b457c6cdc 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 34ed251400..3bb6e1e41f 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 14c9be52e2..7886a84dd6 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 0cc51e39a3..92b671b6b6 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 24b5f6967b..5c7ae32534 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 664d980149..3eab0b6c25 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 039c508727..94c010b723 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 d4e550291b..3caddb61c4 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 0000000000..9a90264b93 --- /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 0000000000..0d66ed2f35 --- /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 350ebcd1cb..e0eb8ec184 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, {