Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/external-plugin-previewer/src/playground.ts
Original file line number Diff line number Diff line change
@@ -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 })

Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -201,14 +201,14 @@ export function CompositionDialog(props: CompositionDialogProps) {
tabs: [
{
label: t('plugin_ito_create_new'),
children: usePortalShadowRoot(() => (
children: (
<CreateForm
onNext={onNext}
onClose={onClose}
origin={poolSettings}
onChangePoolSettings={setPoolSettings}
/>
)),
),
sx: { p: 0 },
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends string>(
config: InjectPageInspectorDefaultConfig = {},
Expand All @@ -17,9 +16,11 @@ export function injectPageInspectorDefault<T extends string>(
})

return function injectPageInspector(signal: AbortSignal) {
const watcher = new MutationObserverWatcher(new LiveSelector().querySelector('body'))
startWatch(watcher, signal)
createReactRootShadowed(watcher.firstDOMProxy.afterShadow, { signal }).render(<PageInspectorDefault />)
const dom = document.body
.appendChild(document.createElement('div'))
.attachShadow({ mode: Flags.using_ShadowDOM_attach_mode })

createReactRootShadowed(dom, { signal, key: 'page-inspector' }).render(<PageInspectorDefault />)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(<UI persona={for_} unmount={() => root.destory()} />)
}
}
9 changes: 1 addition & 8 deletions packages/mask/src/utils/shadow-root/renderInShadowRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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(
<main>
<head />
<CSSVariableInjector useTheme={useClassicMaskSNSTheme} />
</main>,
)
setupPortalShadowRoot({ mode: Flags.using_ShadowDOM_attach_mode })
}

// https://github.com/DimensionDev/Maskbook/issues/3265 with fast refresh or import order?
Expand Down
13 changes: 9 additions & 4 deletions packages/plugin-infra/src/utils/createInjectHooksRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> = Plugin.InjectUI<T>
type Raw<T> = Plugin.InjectUIRaw<T>
Expand Down Expand Up @@ -44,16 +45,20 @@ export function createInjectHooksRenderer<PluginDefinition extends Plugin.Shared
.filter(pickInjectorHook)
.map((plugin) => (
<PropsContext.Provider key={plugin.ID} value={props}>
<SinglePluginWithinErrorBoundary key={plugin.ID} plugin={plugin} />
<ShadowRootIsolation data-plugin={plugin.ID}>
<SinglePluginWithinErrorBoundary plugin={plugin} />
</ShadowRootIsolation>
</PropsContext.Provider>
))
return <>{all}</>
}
return memo(function PluginsInjectionHookRenderErrorBoundary(props: PropsType) {
return (
<ErrorBoundary>
<PluginsInjectionHookRender {...props} />
</ErrorBoundary>
<span data-plugin-render="">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this <span/> necessary?

Don't see any css selector match [data-plugin-render] or some other usage

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not necessary. This span is used to wrap all plugins because there're many plugins and it's a noise. With this span, we can fold all plugins at once.

This data-plugin-render is not used anywhere. It's for a better view in the F12 DOM view.

<ErrorBoundary>
<PluginsInjectionHookRender {...props} />
</ErrorBoundary>
</span>
)
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ const useStyles = makeStyles()((theme) => ({
borderColor: '#2CA4EF',
borderStyle: 'solid',
userSelect: 'none',
'& > $indicator': { opacity: 1 },
},
indicator: {
position: 'absolute',
Expand Down
8 changes: 8 additions & 0 deletions packages/polyfills/types/dom.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,11 @@ interface ImportMeta {
*/
url: string
}

interface ShadowRoot {
adoptedStyleSheets?: readonly CSSStyleSheet[]
}
interface CSSStyleSheet {
replace(text: string): Promise<void>
replaceSync(text: string): void
}
10 changes: 10 additions & 0 deletions packages/theme/src/CSSVariableInjector.tsx
Original file line number Diff line number Diff line change
@@ -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 <GlobalStyles styles={CSSVariableInjectorCSS(colorScheme)} />
}
21 changes: 0 additions & 21 deletions packages/theme/src/ShadowRoot/CSSVariableInjector.tsx

This file was deleted.

7 changes: 7 additions & 0 deletions packages/theme/src/ShadowRoot/Contexts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createContext } from 'react'
import type { StyleSheet } from './ShadowRootStyleSheet'

/** @internal */
export const StyleSheetsContext = createContext<readonly StyleSheet[]>(null!)
/** @internal */
export const PreventEventPropagationListContext = createContext<(keyof HTMLElementEventMap)[]>([])
127 changes: 47 additions & 80 deletions packages/theme/src/ShadowRoot/Portal.tsx
Original file line number Diff line number Diff line change
@@ -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). */
Expand All @@ -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<HTMLSpanElement | null>(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<AbortController>(null!)
const preventEventPropagationList = useContext(PreventEventPropagationListContext)
const { container, root } = useRefInit(() => {
signal.current = new AbortController()
const portal = PortalShadowRoot()

return (
<IsolatedRender {...doms} findMountingShadowRef={findMountingShadowRef}>
<span style={{ display: 'none' }} ref={(ref) => findMountingShadowRef !== ref && setRef(ref)} />
{renderer(container)}
</IsolatedRender>
)
}
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)
Comment thread
Jack-Works marked this conversation as resolved.
},
[],
)

return children as any
return renderer(container)
}

export function createShadowRootForwardedComponent<
Expand Down Expand Up @@ -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<T>(f: () => T): T {
function useRefInit<T>(f: () => T): T {
const ref = useRef<T>(undefined!)
if (!ref.current) ref.current = f()
return ref.current
Expand Down
35 changes: 35 additions & 0 deletions packages/theme/src/ShadowRoot/ShadowRootIsolation.tsx
Original file line number Diff line number Diff line change
@@ -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<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>) {
const [dom, setDOM] = useState<HTMLSpanElement | null>()

const container = useRef<HTMLDivElement>()
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 <span {...props} ref={(x) => (x !== dom ? setDOM(x) : undefined)} />

return (
<span {...props}>
<ShadowRootStyleProvider shadow={dom.shadowRoot}>
{createPortal(children, container.current)}
</ShadowRootStyleProvider>
</span>
)
}
Loading