import React from 'react'; import createCache, { Options } from '@emotion/cache'; import { CacheProvider } from '@emotion/react'; import { ComponentType, StylesInjector } from './types.js'; import { useConnectedPortalsEffect, useShadowDom } from './hooks.js'; // Copied from emotion-cache with minor adjustments to work with multiple containers // https://github.com/emotion-js/emotion/blob/main/packages/sheet/src/index.js function sheetForTag(tag: HTMLStyleElement): CSSStyleSheet { if (tag.sheet) { return tag.sheet } // this weirdness brought to you by firefox for (let i = 0; i < document.styleSheets.length; i++) { if (document.styleSheets[i]!.ownerNode === tag) { return document.styleSheets[i]! } } return new CSSStyleSheet(); } type SheetOptions = { nonce?: string, key: string, containers?: Node[], container?: Node, speedy?: boolean, prepend?: boolean, insertionPoint?: HTMLElement } function createStyleElement(options: { key: string, nonce: string | void }): HTMLStyleElement { let tag = document.createElement('style') tag.setAttribute('data-emotion', options.key) if (options.nonce !== undefined) { tag.setAttribute('nonce', options.nonce) } tag.appendChild(document.createTextNode('')) tag.setAttribute('data-s', '') return tag } export class StyleSheet { isSpeedy: boolean ctr: number tags: Array // Using Node instead of HTMLElement since container may be a ShadowRoot containers: Node[] // HACK: emotion sometimes uses this prop to pass it to newly created StyleSheet (when inserting global styles), // so we keep it around container: Node[] key: string nonce: string | void prepend: boolean | void before: Element | null insertionPoint: HTMLElement | void constructor(options: SheetOptions) { this.isSpeedy = options.speedy === undefined ? process.env['NODE_ENV'] === 'production' : options.speedy if (!options.containers) { options.containers = [] } if (options.container) { options.containers.push(...(Array.isArray(options.container) ? options.container : [options.container])) } if (!options.containers) options.containers = [] this.tags = options.containers.map(c => []); this.ctr = 0 this.nonce = options.nonce // key is the value of the data-emotion attribute, it's used to identify different sheets this.key = options.key this.containers = options.containers this.container = options.containers this.prepend = options.prepend this.insertionPoint = options.insertionPoint this.before = null } _insertTags = (tag: HTMLStyleElement) => { this.containers.forEach((container, i) => { const tagCopy = tag.cloneNode(true); let before if (this.tags[i]!.length === 0) { if (this.insertionPoint) { before = this.insertionPoint.nextSibling } else if (this.prepend) { before = container.firstChild } else { before = this.before } } else { before = this.tags[i]![this.tags[i]!.length - 1]!.nextSibling } container.insertBefore(tagCopy, before) // @ts-ignore this.tags[i].push(tagCopy) }); } hydrate(nodes: HTMLStyleElement[]) { nodes.forEach(this._insertTags) } insert(rule: string) { // the max length is how many rules we have per style tag, it's 65000 in speedy mode // it's 1 in dev because we insert source maps that map a single rule to a location // and you can only have one source map per style tag if (this.ctr % (this.isSpeedy ? 65000 : 1) === 0) { this._insertTags(createStyleElement(this)) } this.containers.forEach((cont, i) => { const tag = this.tags[i]![this.tags[i]!.length - 1]! if (process.env['NODE_ENV'] !== 'production') { const isImportRule = rule.charCodeAt(0) === 64 && rule.charCodeAt(1) === 105 if (isImportRule && (this as any)._alreadyInsertedOrderInsensitiveRule) { // this would only cause problem in speedy mode // but we don't want enabling speedy to affect the observable behavior // so we report this error at all times console.error( `You're attempting to insert the following rule:\n` + rule + '\n\n`@import` rules must be before all other types of rules in a stylesheet but other rules have already been inserted. Please ensure that `@import` rules are before all other rules.' ) } ; (this as any)._alreadyInsertedOrderInsensitiveRule = (this as any)._alreadyInsertedOrderInsensitiveRule || !isImportRule } if (this.isSpeedy) { const sheet = sheetForTag(tag) try { // this is the ultrafast version, works across browsers // the big drawback is that the css won't be editable in devtools sheet.insertRule(rule, sheet.cssRules.length) } catch (e) { if ( process.env['NODE_ENV'] !== 'production' && !/:(-moz-placeholder|-moz-focus-inner|-moz-focusring|-ms-input-placeholder|-moz-read-write|-moz-read-only|-ms-clear|-ms-expand|-ms-reveal){/.test( rule ) ) { console.error( `There was a problem inserting the following rule: "${rule}"`, e ) } } } else { tag.appendChild(document.createTextNode(rule)) } }); this.ctr++ } flush() { this.tags.forEach(tags => tags.forEach(tag => tag.parentNode && tag.parentNode.removeChild(tag))); this.tags = this.containers.map(c => []); this.ctr = 0 if (process.env['NODE_ENV'] !== 'production') { ; (this as any)._alreadyInsertedOrderInsensitiveRule = false } } addContainer(tag: Node) { this.containers.push(tag); this.tags.push([]); } } interface EmotionInjectorOptions { stylisPlugins?: Options['stylisPlugins']; } export default (options: EmotionInjectorOptions = {}): StylesInjector => { return

( Component: ComponentType

, shadowHost: HTMLElement, shadowRoot: ShadowRoot, mountingInto: HTMLDivElement, stylesWrapper: HTMLDivElement ) => { const { stylisPlugins = [] } = options; const key = Math.random() .toString(36) .replace(/[^a-z]+/g, '') .slice(0, 5); // Some fluck with types. TS thinks createCache isn't callable // @ts-ignore const cache = createCache({ key, stylisPlugins: stylisPlugins, container: stylesWrapper, }); const sheet = new StyleSheet({ key, containers: [stylesWrapper], }); // @ts-ignore cache.sheet = sheet; return (props: P) => { useConnectedPortalsEffect((portals) => { portals.forEach(p => { if (!sheet.containers.includes(p.stylesWrapper)) { sheet.addContainer(p.stylesWrapper); } }); }); return ( ); }; }; };