feat!: Allow using Blockly in web components/shadow DOM#9611
Conversation
maribethb
left a comment
There was a problem hiding this comment.
I don't know enough about web components so this seems plausible enough, but a couple questions.
| if ( | ||
| typeof window === 'undefined' || | ||
| !window.CSSStyleSheet || | ||
| injectionSites.has(root) |
There was a problem hiding this comment.
will this return early now if the theme changes? i think the previous code allowed you to re-inject if the theme changed? not super familiar with this code though so could be misreading
There was a problem hiding this comment.
Great catch, yes! Updated it to be keyed by selector, so it only injects once per theme, and confirmed that fixes it.
| injected = true; | ||
| if (!hasCss) { | ||
| export function inject( | ||
| container: HTMLElement, |
There was a problem hiding this comment.
again not too familiar with this code, but is it possible to have container be optional and the default value be getParentContainer?
There was a problem hiding this comment.
We could, but this is only called from Blockly.inject(), and is explicitly a no-op if called again. At the point Blockly.inject() calls it, the workspace doesn't know its injection div yet, so getParentContainer() wouldn't return it.
|
@maribethb @gonfunko In the CSS cascade algorithm, adoptedStyleSheets are ordered after document.styleSheets, which affects precedence. As a result, where a Blockly class style was previously overridden by app-specific CSS without additional specificity by relying on document order, these overrides will no longer take effect. Consuming applications may need to bump the specificity of their overrides to restore custom styles. This change will likely require an update to documentation. Update: Possible additional CSS changes required due to inherited styles from the widget / dropdown div etc now being inside of the injectionDiv. |
…heets Reverts the storage mechanism introduced in RaspberryPiFoundation#9611 (constructable stylesheets via `adoptedStyleSheets`) while keeping the per-root injection-site tracking that RaspberryPiFoundation#9611 added for shadow-DOM support. Motivations: - Safari 15.4 compatibility. `new CSSStyleSheet()` and `adoptedStyleSheets` require Safari 16.4+; the previous `!window.CSSStyleSheet` feature check was also nominal-only since the CSSStyleSheet interface is present in every browser. - Cascade order. `adoptedStyleSheets` apply after `<style>`/`<link>` elements in the document, so Blockly's defaults silently overrode host stylesheets. Prepending a `<style>` to the head (or to the shadow root) restores the pre-RaspberryPiFoundation#9611 behavior where any author stylesheet declared later wins on specificity ties. Trade-offs: - Per-shadow-root CSS text is duplicated rather than shared via a single adopted sheet object. Negligible for typical use. - `Css.register()` calls made after the first `inject()` no longer reach already-injected roots (same as RaspberryPiFoundation#9611's behavior); subsequent `inject()` calls into other roots still pick them up. Web-component consumers can legitimately register late, so this is preferred to reinstating the pre-RaspberryPiFoundation#9611 throw. Fixes RaspberryPiFoundation#9876
Reverts the storage mechanism introduced in RaspberryPiFoundation#9611 (constructable stylesheets via `adoptedStyleSheets`) while keeping the per-root injection-site tracking that RaspberryPiFoundation#9611 added for shadow-DOM support. Motivations: - Safari 15.4 compatibility. `new CSSStyleSheet()` and `adoptedStyleSheets` require Safari 16.4+; the previous `!window.CSSStyleSheet` feature check was also nominal-only since the CSSStyleSheet interface is present in every browser. - Cascade order. `adoptedStyleSheets` apply after `<style>`/`<link>` elements in the document, so Blockly's defaults silently overrode host stylesheets. Prepending a `<style>` to the head (or to the shadow root) restores the pre-RaspberryPiFoundation#9611 behavior where any author stylesheet declared later wins on specificity ties. Trade-offs: - Per-shadow-root CSS text is duplicated rather than shared via a single adopted sheet object. Negligible for typical use. - `Css.register()` calls made after the first `inject()` no longer reach already-injected roots (same as RaspberryPiFoundation#9611's behavior); subsequent `inject()` calls into other roots still pick them up. Web-component consumers can legitimately register late, so this is preferred to reinstating the pre-RaspberryPiFoundation#9611 throw. Fixes RaspberryPiFoundation#9876
Reverts the storage mechanism introduced in RaspberryPiFoundation#9611 (constructable stylesheets via `adoptedStyleSheets`) while keeping the per-root injection-site tracking that RaspberryPiFoundation#9611 added for shadow-DOM support. Motivations: - Safari 15.4 compatibility. `new CSSStyleSheet()` and `adoptedStyleSheets` require Safari 16.4+ - Cascade order. `adoptedStyleSheets` apply after `<style>`/`<link>` elements in the document, so Blockly's defaults silently overrode host stylesheets. Prepending a `<style>` to the head (or to the shadow root) restores the pre-RaspberryPiFoundation#9611 behavior where any author stylesheet declared later wins on specificity ties. Trade-offs: - Per-shadow-root CSS text is duplicated rather than shared via a single adopted sheet object. Negligible for typical use. - `Css.register()` calls made after the first `inject()` no longer reach already-injected roots (same as RaspberryPiFoundation#9611's behavior); subsequent `inject()` calls into other roots still pick them up. Web-component consumers can legitimately register late, so this is preferred to reinstating the pre-RaspberryPiFoundation#9611 throw. Fixes RaspberryPiFoundation#9876
Reverts the storage mechanism introduced in #9611 (constructable stylesheets via `adoptedStyleSheets`) while keeping the per-root injection-site tracking that #9611 added for shadow-DOM support. Motivations: - Safari 15.4 compatibility. `new CSSStyleSheet()` and `adoptedStyleSheets` require Safari 16.4+ - Cascade order. `adoptedStyleSheets` apply after `<style>`/`<link>` elements in the document, so Blockly's defaults silently overrode host stylesheets. Prepending a `<style>` to the head (or to the shadow root) restores the pre-#9611 behavior where any author stylesheet declared later wins on specificity ties. Trade-offs: - Per-shadow-root CSS text is duplicated rather than shared via a single adopted sheet object. Negligible for typical use. - `Css.register()` calls made after the first `inject()` no longer reach already-injected roots (same as #9611's behavior); subsequent `inject()` calls into other roots still pick them up. Web-component consumers can legitimately register late, so this is preferred to reinstating the pre-#9611 throw. Fixes #9876
The basics
The details
Resolves
Fixes #1114
Proposed Changes
This PR adds support for using Blockly inside of the shadow DOM and web components.
Largely, this is a matter of injecting CSS into the document or shadow DOM root as appropriate, rather than creating
<style>tags and injecting them into<head>. Additionally, the global floaty elements (tooltips, input fields, widget div, dropdown div) needed some adjustments to their positioning logic; they had generally all been assuming that they lived in a global div at the root of the document, but this was already not necessarily the case. We've allowed specifying a parent element for these, but if that had bounds smaller than the document things could get wonky. Now, the parent container defaults to returning the injection div unless another value is specified, so these elements are all contained within that (and the shadow DOM/web component, if Blockly is used in one), and that element's position in the page is taken into account when determining the bounds for tooltips, dropdowns, etc.I also added an additional playground/test harness (with LLM assistance) that embeds Blockly inside a
<div>as usual, but also defines a web component and uses that to embed a separate instance on the same page.Reason for Changes
This has been a long-standing request, and web components have become more widespread and popular. We also have some partner applications that could benefit from this.
Breaking Changes
getParentContainer()returns the injection div if another element has not been specified. This behavior is likely fine, but if you were using this method you should either explicitly set the container you want or verify that your use is compatible with the injection div.Blockly.Css.inject()requires that the element into which Blockly/the CSS should be injected be specified.tagNameargument has been removed fromConstantProvider.createDom()andConstantProvider.injectCss_(), since CSS is no longer added via a<style>tag. If you have a custom renderer orConstantProvideror call these methods, ensure that thetagNameargument is removed from your implementation/calls.