fix: CSS back to <style> for Safari/cascade reasons (#9908)

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
This commit is contained in:
Matt Hillsdon
2026-05-20 19:54:09 +01:00
committed by GitHub
parent 8bf2e1ed12
commit 6a6871ea54
2 changed files with 22 additions and 27 deletions
+14 -17
View File
@@ -5,9 +5,8 @@
*/
// Former goog.module ID: Blockly.Css
/** Has CSS already been injected? */
const injectionSites = new WeakSet<Document | ShadowRoot>();
const registeredStyleSheets: Array<CSSStyleSheet> = [];
const registeredCss: Array<string> = [];
import * as userAgent from './utils/useragent.js';
/**
@@ -17,11 +16,7 @@ import * as userAgent from './utils/useragent.js';
* @param cssContent Multiline CSS string or an array of single lines of CSS.
*/
export function register(cssContent: string) {
if (typeof window === 'undefined' || !window.CSSStyleSheet) return;
const sheet = new CSSStyleSheet();
sheet.replace(cssContent);
registeredStyleSheets.push(sheet);
registeredCss.push(cssContent);
}
/**
@@ -41,24 +36,26 @@ export function inject(
hasCss: boolean,
pathToMedia: string,
) {
if (!hasCss || typeof window === 'undefined' || !window.CSSStyleSheet) {
return;
}
if (!hasCss || typeof window === 'undefined') return;
const root = container.getRootNode() as Document | ShadowRoot;
// Only inject the CSS once.
if (injectionSites.has(root)) return;
injectionSites.add(root);
// Strip off any trailing slash (either Unix or Windows).
const mediaPath = pathToMedia.replace(/[\\/]$/, '');
const cssContent = content.replace(/<<<PATH>>>/g, mediaPath);
const cssText = [content, ...registeredCss]
.join('\n')
.replace(/<<<PATH>>>/g, mediaPath);
const sheet = new CSSStyleSheet();
sheet.replace(cssContent);
root.adoptedStyleSheets.push(sheet);
registeredStyleSheets.forEach((sheet) => root.adoptedStyleSheets.push(sheet));
const styleEl = document.createElement('style');
styleEl.id = 'blockly-common-style';
styleEl.textContent = cssText;
// Prepend so Blockly's rules sit at the start of the cascade; any user
// stylesheet declared later wins by document order. Style elements appended
// to the light DOM don't apply inside shadow roots, so for the shadow DOM
// case we prepend the style element to the shadow root itself.
(root instanceof ShadowRoot ? root : document.head).prepend(styleEl);
}
/**
@@ -1126,17 +1126,15 @@ export class ConstantProvider {
* @param selector The CSS selector to interpolate into the stylesheet.
*/
protected injectCSS_(root: Document | ShadowRoot, selector: string) {
if (
typeof window === 'undefined' ||
!window.CSSStyleSheet ||
injectionSites.get(selector)?.has(root)
) {
return;
}
if (typeof window === 'undefined') return;
if (injectionSites.get(selector)?.has(root)) return;
const sheet = new CSSStyleSheet();
sheet.replace(this.getCSS_(selector).join('\n'));
root.adoptedStyleSheets.push(sheet);
const styleEl = document.createElement('style');
styleEl.className = 'blockly-renderer-style';
styleEl.textContent = this.getCSS_(selector).join('\n');
// See css.ts inject() for the rationale on prepending and shadow root
// handling.
(root instanceof ShadowRoot ? root : document.head).prepend(styleEl);
const sitesForSelector =
injectionSites.get(selector) ?? new WeakSet<Document | ShadowRoot>();