lion/packages/ui/components/core/src/ScopedElementsMixin.js
2024-03-27 18:02:16 +01:00

148 lines
6.5 KiB
JavaScript

/*
* This file is combination of '@open-wc/scoped-elements@v3/lit-element.js' and '@open-wc/scoped-elements@v3/html-element.js'.
* Then on top of those, some code from '@open-wc/scoped-elements@v2' is brought to to make polyfill not mandatory.
*
* ## Considerations
* In its current state, the [scoped-custom-element-registry](https://github.com/webcomponents/polyfills/tree/master/packages/scoped-custom-element-registry) draft spec has uncertainties:
* - the spec is not yet final; it's not clear how long it will be dependent on a polyfill
* - the polyfill conflicts with new browser functionality (form-associated custom elements in Safari, ShadowRoot.createElement in Chrome Canary, etc.).
* - the spsc is not compatible with SSR and it remains unclear if it will be in the future
*
* Also see: https://github.com/webcomponents/polyfills/issues?q=scoped-custom-element-registry
*
* In previous considerations (last year), we betted on the spec to evolve quickly and the polyfill to be stable.
* Till this day, little progress has been made. In the meantime. @lit-labs/ssr (incompatible with the spec) is released as well.
*
* This file aims to achieve two things:
* = being up to date with the latest version of @open-wc/scoped-elements (v3)
* - make the impact of this change for lion as minimal as possible
*
* In order to achieve the latter, we keep the ability to opt out of the polyfill.
* This can be beneficial for performance, bundle size, ease of use and SSR capabilities.
*
* We will keep a close eye on developments in spec and polyfill, and will re-evaluate the scoped-elements approach when the time is right.
*/
import { dedupeMixin } from '@open-wc/dedupe-mixin';
import { adoptStyles } from 'lit';
import { ScopedElementsMixin as OpenWcLitScopedElementsMixin } from '@open-wc/scoped-elements/lit-element.js';
/**
* @typedef {import('@open-wc/scoped-elements/lit-element.js').ScopedElementsHost} ScopedElementsHost
* @typedef {import('../../form-core/types/validate/ValidateMixinTypes.js').ScopedElementsMap} ScopedElementsMap
* @typedef {import('lit').CSSResultOrNative} CSSResultOrNative
* @typedef {import('lit').LitElement} LitElement
* @typedef {typeof import('lit').LitElement} TypeofLitElement
* @typedef {import('@open-wc/dedupe-mixin').Constructor<LitElement>} LitElementConstructor
* @typedef {import('@open-wc/dedupe-mixin').Constructor<ScopedElementsHost>} ScopedElementsHostConstructor
* @typedef {import('./types.js').ScopedElementsHostV2Constructor} ScopedElementsHostV2Constructor
*/
const supportsScopedRegistry = Boolean(
// @ts-expect-error
ShadowRoot.prototype.createElement && ShadowRoot.prototype.importNode,
);
/**
* @template {LitElementConstructor} T
* @param {T} superclass
* @return {T & ScopedElementsHostConstructor & ScopedElementsHostV2Constructor}
*/
const ScopedElementsMixinImplementation = superclass =>
/** @type {ScopedElementsHost} */
class ScopedElementsHost extends OpenWcLitScopedElementsMixin(superclass) {
createScopedElement(/** @type {string} */ tagName) {
const root = supportsScopedRegistry ? this.shadowRoot : document;
// @ts-expect-error polyfill to support createElement on shadowRoot is loaded
return root.createElement(tagName);
}
/**
* Defines a scoped element.
*
* @param {string} tagName
* @param {typeof HTMLElement} klass
*/
defineScopedElement(tagName, klass) {
// @ts-ignore
const registeredClass = this.registry.get(tagName);
if (registeredClass && supportsScopedRegistry === false && registeredClass !== klass) {
// eslint-disable-next-line no-console
console.error(
[
`You are trying to re-register the "${tagName}" custom element with a different class via ScopedElementsMixin.`,
'This is only possible with a CustomElementRegistry.',
'Your browser does not support this feature so you will need to load a polyfill for it.',
'Load "@webcomponents/scoped-custom-element-registry" before you register ANY web component to the global customElements registry.',
'e.g. add "<script src="/node_modules/@webcomponents/scoped-custom-element-registry/scoped-custom-element-registry.min.js"></script>" as your first script tag.',
'For more details you can visit https://open-wc.org/docs/development/scoped-elements/',
].join('\n'),
);
}
if (!registeredClass) {
// @ts-ignore
return this.registry.define(tagName, klass);
}
// @ts-ignore
return this.registry.get(tagName);
}
/**
* @param {ShadowRootInit} options
* @returns {ShadowRoot}
*/
attachShadow(options) {
// @ts-ignore
const { scopedElements } = /** @type {typeof ScopedElementsHost} */ (this.constructor);
const shouldCreateRegistry =
!this.registry ||
// @ts-ignore
(this.registry === this.constructor.__registry &&
!Object.prototype.hasOwnProperty.call(this.constructor, '__registry'));
/**
* Create a new registry if:
* - the registry is not defined
* - this class doesn't have its own registry *AND* has no shared registry
* This is important specifically for superclasses/inheritance
*/
if (shouldCreateRegistry) {
// @ts-ignore
this.registry = supportsScopedRegistry ? new CustomElementRegistry() : customElements;
for (const [tagName, klass] of Object.entries(scopedElements ?? {})) {
this.defineScopedElement(tagName, klass);
}
}
return Element.prototype.attachShadow.call(this, {
...options,
// The polyfill currently expects the registry to be passed as `customElements`
customElements: this.registry,
// But the proposal has moved forward, and renamed it to `registry`
// For backwards compatibility, we pass it as both
registry: this.registry,
});
}
createRenderRoot() {
const { shadowRootOptions, elementStyles } = /** @type {TypeofLitElement} */ (
this.constructor
);
const createdRoot = this.attachShadow(shadowRootOptions);
if (supportsScopedRegistry) {
// @ts-expect-error
this.renderOptions.creationScope = createdRoot;
}
if (createdRoot instanceof ShadowRoot) {
adoptStyles(createdRoot, elementStyles);
this.renderOptions.renderBefore = this.renderOptions.renderBefore || createdRoot.firstChild;
}
return createdRoot;
}
};
export const ScopedElementsMixin = dedupeMixin(ScopedElementsMixinImplementation);