Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/loose-sloths-guess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': minor
---

feat: allow passing `ShadowRootInit` to custom element `shadow` option
11 changes: 9 additions & 2 deletions documentation/docs/07-misc/04-custom-elements.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<svelte:options>` 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 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: "<desired name>"`.
- `reflect: boolean`: By default, updated prop values do not reflect back to the DOM. To enable this behavior, set `reflect: true`.
Expand All @@ -78,7 +81,11 @@ When constructing a custom element, you can tailor several aspects by defining `
<svelte:options
customElement={{
tag: 'custom-element',
shadow: 'none',
shadow: () => ({
mode: import.meta.env.DEV ? 'open' : 'closed',
clonable: true,
// ...
}),
props: {
name: { reflect: true, type: 'Number', attribute: 'element-index' }
},
Expand Down
6 changes: 4 additions & 2 deletions documentation/docs/98-reference/.generated/compile-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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", `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

### svelte_options_invalid_tagname

```
Expand Down
7 changes: 6 additions & 1 deletion packages/svelte/elements.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2061,7 +2061,12 @@ export interface SvelteHTMLElements {
| undefined
| {
tag?: string;
shadow?: 'open' | 'none' | undefined;
shadow?:
| 'open'
| 'none'
| ShadowRootInit
| (() => ShadowRootInit | undefined)
| undefined;
props?:
| Record<
string,
Expand Down
6 changes: 4 additions & 2 deletions packages/svelte/messages/compile-errors/template.md
Original file line number Diff line number Diff line change
Expand Up @@ -403,15 +403,17 @@ 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

> "props" must be a statically analyzable object literal of the form "{ [key: string]: { attribute?: string; reflect?: boolean; type?: "String" | "Boolean" | "Number" | "Array" | "Object" }"

## svelte_options_invalid_customelement_shadow

> "shadow" must be either "open" or "none"
> "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

## svelte_options_invalid_tagname

Expand Down
8 changes: 4 additions & 4 deletions packages/svelte/src/compiler/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
}

/**
Expand All @@ -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", `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" or "none"\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`);
}

/**
Expand Down
13 changes: 9 additions & 4 deletions packages/svelte/src/compiler/phases/1-parse/read/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,16 @@ 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' && (shadow.value === 'open' || shadow.value === 'none')) {
ce.shadow = shadow.value;
} else if (
shadow.type === 'ObjectExpression' ||
shadow.type === 'ArrowFunctionExpression'
) {
ce.shadow = shadow;
} else {
e.svelte_options_invalid_customelement_shadow(attribute);
}
ce.shadow = shadowdom;
}

const extend = properties.find(([name]) => name === 'extend')?.[1];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -643,15 +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 {ESTree.ObjectExpression | ESTree.ArrowFunctionExpression | 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 {
shadow_root_init = ce.shadow;
}

const create_ce = b.call(
'$.create_custom_element',
b.id(analysis.name),
b.object(props_str),
slots_str,
accessors_str,
b.literal(use_shadow_dom),
shadow_root_init,
/** @type {any} */ (typeof ce !== 'boolean' ? ce.extend : undefined)
);

Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/src/compiler/types/template.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export namespace AST {
css?: 'injected';
customElement?: {
tag?: string;
shadow?: 'open' | 'none';
shadow?: 'open' | 'none' | ObjectExpression | ArrowFunctionExpression | undefined;
props?: Record<
string,
{
Expand Down
23 changes: 15 additions & 8 deletions packages/svelte/src/internal/client/dom/elements/custom-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,25 @@ 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 | (() => ShadowRootInit | undefined) | 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' });

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_value);
}
}

Expand Down Expand Up @@ -136,7 +143,7 @@ if (typeof HTMLElement === 'function') {
}
this.$$c = createClassComponent({
component: this.$$ctor,
target: this.shadowRoot || this,
target: this.$$shadowRoot || this,
props: {
...this.$$d,
$$slots,
Expand Down Expand Up @@ -277,20 +284,20 @@ function get_custom_elements_slots(element) {
* @param {Record<string, CustomElementPropDefinition>} 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 | (() => ShadowRootInit | undefined) | undefined} shadow_root_init Options passed to shadow DOM constructor
* @param {(ce: new () => HTMLElement) => new () => HTMLElement} [extend]
*/
export function create_custom_element(
Component,
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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { test } from '../../assert';
const tick = () => Promise.resolve();

export default test({
async test({ assert, target }) {
target.innerHTML = '<custom-element></custom-element>';
await tick();

const el = target.querySelector('custom-element');

assert.equal(el.shadowRoot, null);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<svelte:options customElement={{ tag: "custom-element", shadow: { mode: 'closed' } }} />

<h1>Hello world!</h1>
Original file line number Diff line number Diff line change
@@ -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 = '<custom-element></custom-element>';
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;
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<svelte:options
customElement={{
tag: 'custom-element',
shadow: () => ({
mode: 'open',
// This could also be some env variable.
clonable: window.temp_variable
})
}}
/>

<h1>Hello world!</h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { test } from '../../assert';
const tick = () => Promise.resolve();

export default test({
async test({ assert, target }) {
target.innerHTML = '<custom-element></custom-element>';
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');
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<svelte:options
customElement={{
tag: 'custom-element',
shadow: {
mode: 'open',
clonable: true,
delegatesFocus: true,
serializable: true,
slotAssignment: 'manual',
},
}}
/>

<h1>Hello world!</h1>
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1224,7 +1224,7 @@ declare module 'svelte/compiler' {
css?: 'injected';
customElement?: {
tag?: string;
shadow?: 'open' | 'none';
shadow?: 'open' | 'none' | ObjectExpression | ArrowFunctionExpression | undefined;
props?: Record<
string,
{
Expand Down
Loading