Skip to content

feat!: Allow using Blockly in web components/shadow DOM#9611

Open
gonfunko wants to merge 9 commits intov13from
shadow-dom
Open

feat!: Allow using Blockly in web components/shadow DOM#9611
gonfunko wants to merge 9 commits intov13from
shadow-dom

Conversation

@gonfunko
Copy link
Contributor

@gonfunko gonfunko commented Mar 3, 2026

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.
  • The tagName argument has been removed from ConstantProvider.createDom() and ConstantProvider.injectCss_(), since CSS is no longer added via a <style> tag. If you have a custom renderer or ConstantProvider or call these methods, ensure that the tagName argument is removed from your implementation/calls.

@gonfunko gonfunko requested a review from maribethb March 3, 2026 21:50
@gonfunko gonfunko requested a review from a team as a code owner March 3, 2026 21:50
@github-actions github-actions bot added breaking change Used to mark a PR or issue that changes our public APIs. PR: feature Adds a feature labels Mar 3, 2026
Copy link
Contributor

@maribethb maribethb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again not too familiar with this code, but is it possible to have container be optional and the default value be getParentContainer?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change Used to mark a PR or issue that changes our public APIs. PR: feature Adds a feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants