diff --git a/packages/external-plugin-previewer/src/playground.ts b/packages/external-plugin-previewer/src/playground.ts
index 176bdb0e42af..40bc2d15f22d 100644
--- a/packages/external-plugin-previewer/src/playground.ts
+++ b/packages/external-plugin-previewer/src/playground.ts
@@ -1,7 +1,7 @@
import React from 'react'
import { t } from 'ef.js'
import { setupPortalShadowRoot } from '@masknet/theme'
-setupPortalShadowRoot({ mode: 'open' }, [])
+setupPortalShadowRoot({ mode: 'open' })
Object.assign(globalThis, { React })
diff --git a/packages/mask/src/plugins/ITO/SNSAdaptor/CompositionDialog.tsx b/packages/mask/src/plugins/ITO/SNSAdaptor/CompositionDialog.tsx
index 9acb3d152bfc..70e36f80b125 100644
--- a/packages/mask/src/plugins/ITO/SNSAdaptor/CompositionDialog.tsx
+++ b/packages/mask/src/plugins/ITO/SNSAdaptor/CompositionDialog.tsx
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useState } from 'react'
import Web3Utils from 'web3-utils'
import { DialogContent } from '@mui/material'
-import { makeStyles, usePortalShadowRoot } from '@masknet/theme'
+import { makeStyles } from '@masknet/theme'
import { useI18N } from '../../../utils'
import { useRemoteControlledDialog } from '@masknet/shared'
import { InjectedDialog, InjectedDialogProps } from '../../../components/shared/InjectedDialog'
@@ -201,14 +201,14 @@ export function CompositionDialog(props: CompositionDialogProps) {
tabs: [
{
label: t('plugin_ito_create_new'),
- children: usePortalShadowRoot(() => (
+ children: (
- )),
+ ),
sx: { p: 0 },
},
{
diff --git a/packages/mask/src/social-network/defaults/inject/PageInspector.tsx b/packages/mask/src/social-network/defaults/inject/PageInspector.tsx
index 6f5c1f21f245..415e765425c3 100644
--- a/packages/mask/src/social-network/defaults/inject/PageInspector.tsx
+++ b/packages/mask/src/social-network/defaults/inject/PageInspector.tsx
@@ -2,8 +2,7 @@ import { memo } from 'react'
import { makeStyles } from '@masknet/theme'
import { PageInspector, PageInspectorProps } from '../../../components/InjectedComponents/PageInspector'
import { createReactRootShadowed } from '../../../utils/shadow-root/renderInShadowRoot'
-import { MutationObserverWatcher, LiveSelector } from '@dimensiondev/holoflows-kit'
-import { startWatch } from '../../../utils/watcher'
+import { Flags } from '../../../../shared'
export function injectPageInspectorDefault(
config: InjectPageInspectorDefaultConfig = {},
@@ -17,9 +16,11 @@ export function injectPageInspectorDefault(
})
return function injectPageInspector(signal: AbortSignal) {
- const watcher = new MutationObserverWatcher(new LiveSelector().querySelector('body'))
- startWatch(watcher, signal)
- createReactRootShadowed(watcher.firstDOMProxy.afterShadow, { signal }).render()
+ const dom = document.body
+ .appendChild(document.createElement('div'))
+ .attachShadow({ mode: Flags.using_ShadowDOM_attach_mode })
+
+ createReactRootShadowed(dom, { signal, key: 'page-inspector' }).render()
}
}
diff --git a/packages/mask/src/social-network/defaults/inject/StartSetupGuide.tsx b/packages/mask/src/social-network/defaults/inject/StartSetupGuide.tsx
index e4e56de99d8b..549f0ed3da0e 100644
--- a/packages/mask/src/social-network/defaults/inject/StartSetupGuide.tsx
+++ b/packages/mask/src/social-network/defaults/inject/StartSetupGuide.tsx
@@ -10,12 +10,11 @@ function UI({ unmount, persona }: { unmount: () => void; persona: PersonaIdentif
export function createTaskStartSetupGuideDefault() {
let shadowRoot: ShadowRoot
return (signal: AbortSignal, for_: PersonaIdentifier) => {
- const dom = document.createElement('span')
- document.body.appendChild(dom)
- const root = createReactRootShadowed(
- shadowRoot || (shadowRoot = dom.attachShadow({ mode: Flags.using_ShadowDOM_attach_mode })),
- { signal },
- )
+ shadowRoot ??= document.body
+ .appendChild(document.createElement('div'))
+ .attachShadow({ mode: Flags.using_ShadowDOM_attach_mode })
+
+ const root = createReactRootShadowed(shadowRoot, { signal, key: 'setup-guide' })
root.render( root.destory()} />)
}
}
diff --git a/packages/mask/src/utils/shadow-root/renderInShadowRoot.tsx b/packages/mask/src/utils/shadow-root/renderInShadowRoot.tsx
index be4be4242674..2452bb95ddff 100644
--- a/packages/mask/src/utils/shadow-root/renderInShadowRoot.tsx
+++ b/packages/mask/src/utils/shadow-root/renderInShadowRoot.tsx
@@ -2,7 +2,6 @@ import { createReactRootShadowedPartial, setupPortalShadowRoot, CSSVariableInjec
import { Flags } from '../../../shared'
import { MaskUIRoot } from '../../UIRoot'
import { useClassicMaskSNSTheme } from '../theme'
-import { createRoot } from 'react-dom'
const captureEvents: (keyof HTMLElementEventMap)[] = [
'paste',
@@ -18,13 +17,7 @@ const captureEvents: (keyof HTMLElementEventMap)[] = [
'change',
]
export const setupShadowRootPortal = () => {
- const shadow = setupPortalShadowRoot({ mode: Flags.using_ShadowDOM_attach_mode }, captureEvents)
- createRoot(shadow.appendChild(document.createElement('head'))).render(
-
-
-
- ,
- )
+ setupPortalShadowRoot({ mode: Flags.using_ShadowDOM_attach_mode })
}
// https://github.com/DimensionDev/Maskbook/issues/3265 with fast refresh or import order?
diff --git a/packages/plugin-infra/src/utils/createInjectHooksRenderer.tsx b/packages/plugin-infra/src/utils/createInjectHooksRenderer.tsx
index e106fb977eb0..6adfdb8c502d 100644
--- a/packages/plugin-infra/src/utils/createInjectHooksRenderer.tsx
+++ b/packages/plugin-infra/src/utils/createInjectHooksRenderer.tsx
@@ -3,6 +3,7 @@ import { useEffect, useState, useRef, memo, createContext, useContext } from 're
import { ErrorBoundary } from '@masknet/shared'
import { usePluginI18NField, PluginWrapperComponent, PluginWrapperMethods } from '../hooks'
import { emptyPluginWrapperMethods, PluginWrapperMethodsContext } from '../hooks/usePluginWrapper'
+import { ShadowRootIsolation } from '@masknet/theme'
type Inject = Plugin.InjectUI
type Raw = Plugin.InjectUIRaw
@@ -44,16 +45,20 @@ export function createInjectHooksRenderer (
-
+
+
+
))
return <>{all}>
}
return memo(function PluginsInjectionHookRenderErrorBoundary(props: PropsType) {
return (
-
-
-
+
+
+
+
+
)
})
}
diff --git a/packages/plugins/FileService/src/SNSAdaptor/components/UploadDropArea.tsx b/packages/plugins/FileService/src/SNSAdaptor/components/UploadDropArea.tsx
index d9607b678cb3..22734badd051 100644
--- a/packages/plugins/FileService/src/SNSAdaptor/components/UploadDropArea.tsx
+++ b/packages/plugins/FileService/src/SNSAdaptor/components/UploadDropArea.tsx
@@ -40,7 +40,6 @@ const useStyles = makeStyles()((theme) => ({
borderColor: '#2CA4EF',
borderStyle: 'solid',
userSelect: 'none',
- '& > $indicator': { opacity: 1 },
},
indicator: {
position: 'absolute',
diff --git a/packages/polyfills/types/dom.d.ts b/packages/polyfills/types/dom.d.ts
index bb8e185efa39..832a89dd75b6 100644
--- a/packages/polyfills/types/dom.d.ts
+++ b/packages/polyfills/types/dom.d.ts
@@ -12,3 +12,11 @@ interface ImportMeta {
*/
url: string
}
+
+interface ShadowRoot {
+ adoptedStyleSheets?: readonly CSSStyleSheet[]
+}
+interface CSSStyleSheet {
+ replace(text: string): Promise
+ replaceSync(text: string): void
+}
diff --git a/packages/theme/src/CSSVariableInjector.tsx b/packages/theme/src/CSSVariableInjector.tsx
new file mode 100644
index 000000000000..feee253efdd5
--- /dev/null
+++ b/packages/theme/src/CSSVariableInjector.tsx
@@ -0,0 +1,10 @@
+import { GlobalStyles, Theme, useTheme } from '@mui/material'
+import { useRef } from 'react'
+import { CSSVariableInjectorCSS } from './constants'
+
+export function CSSVariableInjector(props: React.PropsWithChildren<{ useTheme?: () => Theme }>) {
+ const { current: useConsistentTheme } = useRef(props.useTheme || useTheme)
+ const colorScheme = useConsistentTheme().palette.mode
+
+ return
+}
diff --git a/packages/theme/src/ShadowRoot/CSSVariableInjector.tsx b/packages/theme/src/ShadowRoot/CSSVariableInjector.tsx
deleted file mode 100644
index 05b0a73945c2..000000000000
--- a/packages/theme/src/ShadowRoot/CSSVariableInjector.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { applyMaskColorVars } from '../'
-import { Theme, useTheme } from '@mui/material'
-import { useLayoutEffect, useRef } from 'react'
-
-export function CSSVariableInjector(props: React.PropsWithChildren<{ useTheme?: () => Theme }>) {
- const ref = useRef(null)
- const { current: useConsistentTheme } = useRef(props.useTheme || useTheme)
- const colorScheme = useConsistentTheme().palette.mode
-
- useLayoutEffect(() => {
- const host = ref.current!.closest('main')!.parentNode!.querySelector('head')!
- let style: HTMLStyleElement = host.querySelector('style[data-css-var]')!
- if (!style) {
- host.insertBefore((style = document.createElement('style')), host.firstChild)
- style.dataset.cssVar = ''
- }
-
- applyMaskColorVars(style, colorScheme)
- }, [colorScheme])
- return
-}
diff --git a/packages/theme/src/ShadowRoot/Contexts.ts b/packages/theme/src/ShadowRoot/Contexts.ts
new file mode 100644
index 000000000000..0a272a338148
--- /dev/null
+++ b/packages/theme/src/ShadowRoot/Contexts.ts
@@ -0,0 +1,7 @@
+import { createContext } from 'react'
+import type { StyleSheet } from './ShadowRootStyleSheet'
+
+/** @internal */
+export const StyleSheetsContext = createContext(null!)
+/** @internal */
+export const PreventEventPropagationListContext = createContext<(keyof HTMLElementEventMap)[]>([])
diff --git a/packages/theme/src/ShadowRoot/Portal.tsx b/packages/theme/src/ShadowRoot/Portal.tsx
index 9e88a0845eae..67d149ddb604 100644
--- a/packages/theme/src/ShadowRoot/Portal.tsx
+++ b/packages/theme/src/ShadowRoot/Portal.tsx
@@ -1,25 +1,15 @@
-import { useRef, useEffect, forwardRef, useState, createContext, useContext } from 'react'
-import { useCurrentShadowRootStyles } from './ShadowRootStyleProvider'
+/* eslint-disable react-hooks/rules-of-hooks */
+import { useRef, forwardRef, createContext, useContext, useEffect } from 'react'
import type { PopperProps } from '@mui/material'
+import { PreventEventPropagationListContext, StyleSheetsContext } from './Contexts'
-/**
- * ! Do not export !
- *
- * You SHOULD NOT use this in React directly
- */
let mountingPoint: HTMLDivElement
let mountingShadowRoot: ShadowRoot
-export function setupPortalShadowRoot(
- init: ShadowRootInit,
- preventEventPropagationList: (keyof HTMLElementEventMap)[],
-) {
- if (mountingPoint) return mountingShadowRoot!
+export function setupPortalShadowRoot(init: ShadowRootInit) {
+ if (mountingShadowRoot) return mountingShadowRoot
mountingShadowRoot = document.body.appendChild(document.createElement('div')).attachShadow(init)
- for (const each of preventEventPropagationList) {
- mountingShadowRoot.addEventListener(each, (e) => e.stopPropagation())
- }
mountingPoint = mountingShadowRoot.appendChild(document.createElement('div'))
- return mountingShadowRoot!
+ return mountingShadowRoot
}
/** usePortalShadowRoot under this context does not do anything. (And it will return an empty container). */
@@ -41,64 +31,56 @@ export const NoEffectUsePortalShadowRootContext = createContext(false)
* />
* ))
*/
-export function usePortalShadowRoot(renderer: (container: HTMLDivElement | undefined) => null | JSX.Element) {
+export function usePortalShadowRoot(renderer: (container: HTMLElement | undefined) => null | JSX.Element) {
+ const disabled = useRef(useContext(NoEffectUsePortalShadowRootContext)).current
// we ignore the changes on this property during multiple render
// so we can violates the React hooks rule and still be safe.
- const disabled = useRef(useContext(NoEffectUsePortalShadowRootContext)).current
if (disabled) return renderer(undefined)
- // eslint-disable-next-line react-hooks/rules-of-hooks
- const [findMountingShadowRef, setRef] = useState(null)
- // eslint-disable-next-line react-hooks/rules-of-hooks
- const doms = useSideEffectRef(() => {
- const root = document.createElement('div')
- const container = root.appendChild(document.createElement('div'))
- const style = root.appendChild(document.createElement('style'))
- return { root, container, style }
- })
- const { container } = doms
+ const sheets = useContext(StyleSheetsContext)
+ const signal = useRef(null!)
+ const preventEventPropagationList = useContext(PreventEventPropagationListContext)
+ const { container, root } = useRefInit(() => {
+ signal.current = new AbortController()
+ const portal = PortalShadowRoot()
- return (
-
- findMountingShadowRef !== ref && setRef(ref)} />
- {renderer(container)}
-
- )
-}
+ const root = document.createElement('div')
+ root.dataset.portalShadowRoot = ''
+ const shadow = root.attachShadow({ mode: 'open' })
-/*
-Here is a strange problem that `useMemo` in the `useCurrentShadowRootStyles` will re-render every event loop _even_ findMountingShadowRef is the same.
-React is isolating their render process in the unit of components. Split it into another component solves the problem.
-(And now it no longer re-render every event loop).
- */
-type IsolatedRenderProps = React.PropsWithChildren<{
- root: HTMLElement
- container: HTMLElement
- style: HTMLStyleElement
- findMountingShadowRef: HTMLSpanElement | null
-}>
-const IsolatedRender = ({ container, root, style, children, findMountingShadowRef }: IsolatedRenderProps) => {
- const update = useUpdate()
- const css = useCurrentShadowRootStyles(findMountingShadowRef)
- const containerInUse = container.children.length !== 0
+ const stop = (e: Event): void => e.stopPropagation()
+ for (const each of preventEventPropagationList) {
+ shadow.addEventListener(each, stop, { signal: signal.current.signal })
+ }
- useEffect(() => {
- container.appendChild = bind(container.appendChild, container, update)
- container.removeChild = bind(container.removeChild, container, update)
- }, [])
+ const container = shadow.appendChild(document.createElement('main'))
+ sheets.map((x) => x.addContainer(shadow))
- useEffect(() => {
- if (!containerInUse) return root.remove()
- const shadow = PortalShadowRoot()
- if (root.parentElement === shadow) return
- shadow.appendChild(root)
- }, [containerInUse, root])
+ // This is proved to be important to the correct portal orders...
+ container.appendChild = (child) => {
+ if (!root.parentElement) portal.appendChild(root)
+ Node.prototype.appendChild.call(container, child)
+ return child
+ }
+ container.removeChild = (child) => {
+ Node.prototype.removeChild.call(container, child)
+ if (container.childElementCount === 0) portal.removeChild(root)
+ return child
+ }
- useEffect(() => {
- if (findMountingShadowRef && style.textContent !== css) style.textContent = css
- }, [style, css, findMountingShadowRef])
+ return { container, root }
+ })
+ useEffect(
+ () => () => {
+ setTimeout(() => {
+ root.remove()
+ signal.current.abort()
+ }, 2000)
+ },
+ [],
+ )
- return children as any
+ return renderer(container)
}
export function createShadowRootForwardedComponent<
@@ -129,22 +111,7 @@ function PortalShadowRoot(): Element {
return mountingPoint
}
-function bind(f: Function, thisArg: unknown, hook: Function) {
- return (...args: any) => {
- try {
- return f.apply(thisArg, args)
- } finally {
- hook()
- }
- }
-}
-
-function useUpdate() {
- const [, _update] = useState(0)
- return () => _update((i) => i + 1)
-}
-
-function useSideEffectRef(f: () => T): T {
+function useRefInit(f: () => T): T {
const ref = useRef(undefined!)
if (!ref.current) ref.current = f()
return ref.current
diff --git a/packages/theme/src/ShadowRoot/ShadowRootIsolation.tsx b/packages/theme/src/ShadowRoot/ShadowRootIsolation.tsx
new file mode 100644
index 000000000000..049614304c9c
--- /dev/null
+++ b/packages/theme/src/ShadowRoot/ShadowRootIsolation.tsx
@@ -0,0 +1,35 @@
+import { useState, useRef, useLayoutEffect } from 'react'
+import { createPortal } from 'react-dom'
+import { ShadowRootStyleProvider } from './ShadowRootStyleProvider'
+
+/**
+ * Render it's children inside a ShadowRoot to provide style isolation.
+ */
+export function ShadowRootIsolation({
+ children,
+ ...props
+}: React.DetailedHTMLProps, HTMLSpanElement>) {
+ const [dom, setDOM] = useState()
+
+ const container = useRef()
+ if (!container.current) {
+ container.current = document.createElement('div')
+ }
+ useLayoutEffect(() => {
+ if (!dom) return
+ if (dom.shadowRoot) return
+
+ const shadow = dom.attachShadow({ mode: 'open' })
+ shadow.appendChild(container.current!)
+ }, [dom])
+
+ if (!dom?.shadowRoot) return (x !== dom ? setDOM(x) : undefined)} />
+
+ return (
+
+
+ {createPortal(children, container.current)}
+
+
+ )
+}
diff --git a/packages/theme/src/ShadowRoot/ShadowRootStyleProvider.tsx b/packages/theme/src/ShadowRoot/ShadowRootStyleProvider.tsx
index 76192a8b6738..1d72c064f43f 100644
--- a/packages/theme/src/ShadowRoot/ShadowRootStyleProvider.tsx
+++ b/packages/theme/src/ShadowRoot/ShadowRootStyleProvider.tsx
@@ -1,118 +1,51 @@
+import createEmotionCache, { type EmotionCache } from '@emotion/cache'
import { CacheProvider as EmotionCacheProvider } from '@emotion/react'
-import createEmotionCache, { EmotionCache } from '@emotion/cache'
-import { useMemo } from 'react'
-import { useSubscription } from 'use-subscription'
import { TssCacheProvider } from 'tss-react'
+import { StyleSheet } from './ShadowRootStyleSheet'
+import { StyleSheetsContext } from './Contexts'
-class Informative {
- private callback = new Set<() => void>()
- addListener(cb: () => void) {
- this.callback.add(cb)
- return () => void this.callback.delete(cb)
- }
- private pendingInform = false
- inform() {
- if (this.pendingInform) return
- requestAnimationFrame(() => {
- this.pendingInform = false
- this.callback.forEach((x) => x())
- })
- }
-}
-class EmotionInformativeSheetsRegistry {
- reg = new Informative()
- constructor(public cache: EmotionCache) {
- const orig = cache.sheet.insert
- cache.sheet.insert = (...args) => {
- const r = orig.call(cache.sheet, ...args)
- this.reg.inform()
- return r
- }
- }
- toString() {
- return this.cache.sheet.tags.map((x) => x.innerHTML).join('\n')
- }
-}
-
-const emotionRegistryMap = new WeakMap()
-const emotion2RegistryMap = new WeakMap()
-function createSubscription(registry: undefined | EmotionInformativeSheetsRegistry) {
- return {
- getCurrentValue: () => registry?.toString(),
- subscribe: (callback: () => void) => registry?.reg.addListener(callback) ?? (() => 0),
- }
-}
-
-/**
- * Return all CSS created by the emotion instance in the current ShadowRoot.
- *
- * This is used to keep the CSS correct when the rendering is crossing multiple ShadowRoot (e.g. a Modal, Dialog or other things need rendered by React Portal)
- *
- * @param ref DOM reference
- * @returns CSS string
- */
-export function useCurrentShadowRootStyles(ref: Node | null): string {
- const emotionSubscription = useMemo(() => {
- const root = ref?.getRootNode() as ShadowRoot
- const registry = emotionRegistryMap.get(root)
- return createSubscription(registry)
- }, [ref])
- const emotion2Subscription = useMemo(() => {
- const root = ref?.getRootNode() as ShadowRoot
- const registry = emotion2RegistryMap.get(root)
- return createSubscription(registry)
- }, [ref])
- return [useSubscription(emotionSubscription), useSubscription(emotion2Subscription)].filter(Boolean).join('\n')
-}
-
-const initOnceMap = new WeakMap()
-function initOnce(keyBy: ShadowRoot, init: () => T): T {
- if (initOnceMap.has(keyBy)) return initOnceMap.get(keyBy) as T
- const val = init()
- initOnceMap.set(keyBy, val)
- return val
-}
-function createElement(key: keyof HTMLElementTagNameMap, kind: string) {
- const e = document.createElement(key)
- e.setAttribute('data-' + kind, 'true')
- return e
-}
/** @internal */
export interface ShadowRootStyleProviderProps extends React.PropsWithChildren<{}> {
shadow: ShadowRoot
}
-/** @internal */
+/**
+ * @internal
+ * This component provide the modified version of tss-react and emotion context,
+ * therefore styles within this component can render correctly in ShadowRoot.
+ *
+ * This component is used to render inside a bare ShadowRoot.
+ * If you need a nested ShadowRoot render, use ShadowRootIsolation.
+ */
export function ShadowRootStyleProvider(props: ShadowRootStyleProviderProps) {
const { shadow, children } = props
- const { muiEmotionCache, tssEmotionCache } = initOnce(shadow, () => init(props))
+ const [mui, tss, sheets] = getShadowRootEmotionCache(shadow)
return (
-
- {children}
-
+
+
+ {children}
+
+
)
}
-function init({ shadow }: ShadowRootStyleProviderProps) {
- const head = shadow.appendChild(createElement('head', 'css-container'))
- // #region Emotion
- const MuiInsertionPoint = head.appendChild(createElement('div', 'mui-area'))
- const TSSInsertionPoint = head.appendChild(createElement('div', 'tss-area'))
+const styleSheetMap = new WeakMap()
+
+function getShadowRootEmotionCache(shadow: ShadowRoot) {
+ if (styleSheetMap.has(shadow)) return styleSheetMap.get(shadow)!
+
// emotion doesn't allow numbers appears in the key
- const instanceID = Math.random().toString(36).slice(2).replace(/\d/g, 'x')
- const muiEmotionCache = createEmotionCache({
- container: MuiInsertionPoint,
- key: 'mui-' + instanceID,
- speedy: false,
- // TODO: support speedy mode which use insertRule
- // https://github.com/emotion-js/emotion/blob/master/packages/sheet/src/index.js
- })
- const tssEmotionCache = createEmotionCache({
- container: TSSInsertionPoint,
- key: 'tss-' + instanceID,
- speedy: false,
- })
- emotionRegistryMap.set(shadow, new EmotionInformativeSheetsRegistry(muiEmotionCache))
- emotion2RegistryMap.set(shadow, new EmotionInformativeSheetsRegistry(tssEmotionCache))
- // #endregion
- return { muiEmotionCache, tssEmotionCache }
+ const instanceID = Math.random().toString(36).slice(2).replace(/\d/g, 'x').slice(0, 4)
+ const keyA = 'mui-' + instanceID
+ const keyB = 'tss-' + instanceID
+
+ const muiEmotionCache = createEmotionCache({ key: keyA })
+ const muiStyleSheet = new StyleSheet(keyA, shadow)
+ muiEmotionCache.sheet = muiStyleSheet
+
+ const tssEmotionCache = createEmotionCache({ key: keyB })
+ const tssStyleSheet = new StyleSheet(keyB, shadow)
+ tssEmotionCache.sheet = tssStyleSheet
+
+ styleSheetMap.set(shadow, [muiEmotionCache, tssEmotionCache, [muiStyleSheet, tssStyleSheet]])
+ return styleSheetMap.get(shadow)!
}
diff --git a/packages/theme/src/ShadowRoot/ShadowRootStyleSheet.ts b/packages/theme/src/ShadowRoot/ShadowRootStyleSheet.ts
new file mode 100644
index 000000000000..704158131ff6
--- /dev/null
+++ b/packages/theme/src/ShadowRoot/ShadowRootStyleSheet.ts
@@ -0,0 +1,177 @@
+const shadowHeadMap = new WeakMap()
+const constructableStyleSheetEnabled = true
+
+// There are 2 rendering mode of this ShadowRootStyleSheet.
+// 1. If the host supports ConstructableStyleSheet proposal:
+// It is the fastest but only Chrome supports it.
+// 2. style tags that being synchronize between all ShadowRoot.
+// See https://github.com/emotion-js/emotion/blob/main/packages/sheet/src/index.js
+// Note: We cannot use .sheet.insertRule (so called "speedy mode") because in our app,
+// the host of a ShadowRoot might be detached from the DOM and reattach to another place,
+// when a