Skip to content

Commit

Permalink
[fix] 'render' export uses ReactDOM.createRoot
Browse files Browse the repository at this point in the history
AppRegistry.runApplication uses `ReactDOM.createRoot` by default, but
the `render` export uses the legacy `ReactDOM.render`. This patch makes
those 2 APIs consistent.

It also makes some adjustments to the `createSheet` internals to more
reliably implement and test style sheet replication within ShadowRoot's
and iframes.

Fix #2612
  • Loading branch information
necolas committed Nov 29, 2023
1 parent a3ea2a0 commit dee1467
Show file tree
Hide file tree
Showing 8 changed files with 308 additions and 110 deletions.
160 changes: 123 additions & 37 deletions packages/react-native-web-examples/pages/app-registry/index.js
Original file line number Diff line number Diff line change
@@ -1,51 +1,137 @@
import React from 'react';
import { AppRegistry, Text, StyleSheet } from 'react-native';
import React, { useRef, useEffect, useState } from 'react';
import { Pressable, Text, StyleSheet, View, render } from 'react-native';
import Example from '../../shared/example';

function IframeWrapper({ children }) {
const iframeHost = useRef();
const reactRoot = useRef();

useEffect(() => {
if (iframeHost.current) {
if (!reactRoot.current) {
const iframeElement = iframeHost.current;
const iframeAppContainer = document.createElement('div');
iframeElement.contentWindow.document.body.appendChild(
iframeAppContainer
);
reactRoot.current = render(children, iframeAppContainer);
}
reactRoot.current.render(children);
}
});

return <iframe ref={iframeHost} style={{ border: 'none' }} />;
}

function ShadowDomWrapper({ children }) {
const shadowHost = useRef();
const reactRoot = useRef();

useEffect(() => {
if (shadowHost.current) {
if (!reactRoot.current) {
const shadowRoot = shadowHost.current.attachShadow({ mode: 'open' });
reactRoot.current = render(children, shadowRoot);
}
reactRoot.current.render(children);
}
});

return <div ref={shadowHost} />;
}

function Heading({ children }) {
return (
<Text role="heading" style={styles.heading}>
{children}
</Text>
);
}

function Button({ active, onPress, title }) {
return (
<Pressable
onPress={() => onPress((old) => !old)}
style={[styles.button, active && styles.buttonActive]}
>
<Text style={styles.buttonText}>{title}</Text>
</Pressable>
);
}

function App() {
return <Text style={styles.text}>Should be red and bold</Text>;
const [active, setActive] = useState(false);
const [activeIframe, setActiveIframe] = useState(false);
const [activeShadow, setActiveShadow] = useState(false);

return (
<Example title="AppRegistry">
<View style={styles.app}>
<View style={styles.header}>
<Heading>Styles in document</Heading>
<Text style={styles.text}>Should be red and bold</Text>
<Button active={active} onPress={setActive} title={'Button'} />

<Heading>Styles in ShadowRoot</Heading>
<ShadowDomWrapper>
<Text style={styles.text}>Should be red and bold</Text>
<Button
active={activeShadow}
onPress={setActiveShadow}
title={'Button'}
/>
</ShadowDomWrapper>

<Heading>Styles in iframe</Heading>
<IframeWrapper>
<Text style={styles.text}>Should be red and bold</Text>
<Button
active={activeIframe}
onPress={setActiveIframe}
title={'Button'}
/>
</IframeWrapper>
</View>
</View>
</Example>
);
}

const styles = StyleSheet.create({
app: {
marginHorizontal: 'auto',
maxWidth: 500
},
header: {
padding: 20
},
heading: {
fontWeight: 'bold',
fontSize: '1.125rem',
marginBlockStart: '1rem',
marginBlockEnd: '0.25rem'
},
text: {
color: 'red',
fontWeight: 'bold'
},
button: {
backgroundColor: 'red',
paddingBlock: 5,
paddingInline: 10
},
buttonActive: {
backgroundColor: 'blue'
},
buttonText: {
color: 'white',
fontWeight: 'bold',
textAlign: 'center',
textTransform: 'uppercase',
userSelect: 'none'
}
});

AppRegistry.registerComponent('App', () => App);

export default function AppStatePage() {
const iframeRef = React.useRef(null);
const shadowRef = React.useRef(null);

React.useEffect(() => {
const iframeElement = iframeRef.current;
const iframeBody = iframeElement.contentWindow.document.body;
const iframeRootTag = document.createElement('div');
iframeRootTag.id = 'iframe-root';
iframeBody.appendChild(iframeRootTag);
const app1 = AppRegistry.runApplication('App', { rootTag: iframeRootTag });

const shadowElement = shadowRef.current;
const shadowRoot = shadowElement.attachShadow({ mode: 'open' });
const shadowRootTag = document.createElement('div');
shadowRootTag.id = 'shadow-root';
shadowRoot.appendChild(shadowRootTag);
const app2 = AppRegistry.runApplication('App', { rootTag: shadowRootTag });

return () => {
app1.unmount();
app2.unmount();
};
}, []);

return (
<Example title="AppRegistry">
<Text>Styles in iframe</Text>
<iframe ref={iframeRef} />
<Text>Styles in ShadowRoot</Text>
<div ref={shadowRef} />
</Example>
);
return <App />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`AppRegistry runApplication styles roots in iframes: iframe css 1`] = `
"[stylesheet-group=\\"0\\"]{}
body{margin:0;}
button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0);}
input::-webkit-search-cancel-button,input::-webkit-search-decoration,input::-webkit-search-results-button,input::-webkit-search-results-decoration{display:none;}
[stylesheet-group=\\"1\\"]{}
.css-view-175oi2r{align-items:stretch;background-color:rgba(0,0,0,0.00);border:0 solid black;box-sizing:border-box;display:flex;flex-basis:auto;flex-direction:column;flex-shrink:0;list-style:none;margin:0px;min-height:0px;min-width:0px;padding:0px;position:relative;text-decoration:none;z-index:0;}
[stylesheet-group=\\"2\\"]{}
.r-display-xoduu5{display:inline-flex;}
.r-flex-13awgt0{flex:1;}
[stylesheet-group=\\"3\\"]{}
.r-bottom-1p0dtai{bottom:0px;}
.r-left-1d2f490{left:0px;}
.r-pointerEvents-105ug2t{pointer-events:auto!important;}
.r-pointerEvents-12vffkv>*{pointer-events:auto;}
.r-pointerEvents-12vffkv{pointer-events:none!important;}
.r-pointerEvents-633pao{pointer-events:none!important;}
.r-pointerEvents-ah5dr5>*{pointer-events:none;}
.r-pointerEvents-ah5dr5{pointer-events:auto!important;}
.r-position-u8s1d{position:absolute;}
.r-right-zchlnj{right:0px;}
.r-top-ipm5af{top:0px;}"
`;

exports[`AppRegistry runApplication styles roots in iframes: iframe css 2`] = `
"[stylesheet-group=\\"0\\"]{}
body{margin:0;}
button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0);}
input::-webkit-search-cancel-button,input::-webkit-search-decoration,input::-webkit-search-results-button,input::-webkit-search-results-decoration{display:none;}
[stylesheet-group=\\"1\\"]{}
.css-view-175oi2r{align-items:stretch;background-color:rgba(0,0,0,0.00);border:0 solid black;box-sizing:border-box;display:flex;flex-basis:auto;flex-direction:column;flex-shrink:0;list-style:none;margin:0px;min-height:0px;min-width:0px;padding:0px;position:relative;text-decoration:none;z-index:0;}
[stylesheet-group=\\"2\\"]{}
.r-display-xoduu5{display:inline-flex;}
.r-flex-13awgt0{flex:1;}
[stylesheet-group=\\"3\\"]{}
.r-bottom-1p0dtai{bottom:0px;}
.r-left-1d2f490{left:0px;}
.r-pointerEvents-105ug2t{pointer-events:auto!important;}
.r-pointerEvents-12vffkv>*{pointer-events:auto;}
.r-pointerEvents-12vffkv{pointer-events:none!important;}
.r-pointerEvents-633pao{pointer-events:none!important;}
.r-pointerEvents-ah5dr5>*{pointer-events:none;}
.r-pointerEvents-ah5dr5{pointer-events:auto!important;}
.r-position-u8s1d{position:absolute;}
.r-right-zchlnj{right:0px;}
.r-top-ipm5af{top:0px;}"
`;

exports[`AppRegistry runApplication styles roots in shadow trees: shadow dom css 1`] = `
"[stylesheet-group=\\"0\\"]{}
body{margin:0;}
button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0);}
input::-webkit-search-cancel-button,input::-webkit-search-decoration,input::-webkit-search-results-button,input::-webkit-search-results-decoration{display:none;}
[stylesheet-group=\\"1\\"]{}
.css-view-175oi2r{align-items:stretch;background-color:rgba(0,0,0,0.00);border:0 solid black;box-sizing:border-box;display:flex;flex-basis:auto;flex-direction:column;flex-shrink:0;list-style:none;margin:0px;min-height:0px;min-width:0px;padding:0px;position:relative;text-decoration:none;z-index:0;}
[stylesheet-group=\\"2\\"]{}
.r-display-xoduu5{display:inline-flex;}
.r-flex-13awgt0{flex:1;}
[stylesheet-group=\\"3\\"]{}
.r-bottom-1p0dtai{bottom:0px;}
.r-left-1d2f490{left:0px;}
.r-pointerEvents-105ug2t{pointer-events:auto!important;}
.r-pointerEvents-12vffkv>*{pointer-events:auto;}
.r-pointerEvents-12vffkv{pointer-events:none!important;}
.r-pointerEvents-633pao{pointer-events:none!important;}
.r-pointerEvents-ah5dr5>*{pointer-events:none;}
.r-pointerEvents-ah5dr5{pointer-events:auto!important;}
.r-position-u8s1d{position:absolute;}
.r-right-zchlnj{right:0px;}
.r-top-ipm5af{top:0px;}"
`;

exports[`AppRegistry runApplication styles roots in shadow trees: shadow dom css 2`] = `
"[stylesheet-group=\\"0\\"]{}
body{margin:0;}
button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0);}
input::-webkit-search-cancel-button,input::-webkit-search-decoration,input::-webkit-search-results-button,input::-webkit-search-results-decoration{display:none;}
[stylesheet-group=\\"1\\"]{}
.css-view-175oi2r{align-items:stretch;background-color:rgba(0,0,0,0.00);border:0 solid black;box-sizing:border-box;display:flex;flex-basis:auto;flex-direction:column;flex-shrink:0;list-style:none;margin:0px;min-height:0px;min-width:0px;padding:0px;position:relative;text-decoration:none;z-index:0;}
[stylesheet-group=\\"2\\"]{}
.r-display-xoduu5{display:inline-flex;}
.r-flex-13awgt0{flex:1;}
[stylesheet-group=\\"3\\"]{}
.r-bottom-1p0dtai{bottom:0px;}
.r-left-1d2f490{left:0px;}
.r-pointerEvents-105ug2t{pointer-events:auto!important;}
.r-pointerEvents-12vffkv>*{pointer-events:auto;}
.r-pointerEvents-12vffkv{pointer-events:none!important;}
.r-pointerEvents-633pao{pointer-events:none!important;}
.r-pointerEvents-ah5dr5>*{pointer-events:none;}
.r-pointerEvents-ah5dr5{pointer-events:auto!important;}
.r-position-u8s1d{position:absolute;}
.r-right-zchlnj{right:0px;}
.r-top-ipm5af{top:0px;}"
`;
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,14 @@ describe.each([['concurrent'], ['legacy']])('AppRegistry', (mode) => {
expect(setMountedState).toHaveBeenLastCalledWith(false);
});

test('styles roots in different documents', () => {
test('styles roots in iframes', () => {
AppRegistry.registerComponent('App', () => NoopComponent);
act(() => {
AppRegistry.runApplication('App', { initialProps: {}, rootTag, mode });
});
// Create iframe context
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);

const iframeRootTag = document.createElement('div');
iframeRootTag.id = 'react-iframe-root';
iframe.contentWindow.document.body.appendChild(iframeRootTag);
Expand All @@ -90,43 +89,39 @@ describe.each([['concurrent'], ['legacy']])('AppRegistry', (mode) => {
mode
});
});

const iframedoc = iframeRootTag.ownerDocument;
expect(iframedoc).toBe(iframe.contentWindow.document);
expect(iframedoc).not.toBe(document);
const cssText = iframedoc.getElementById(
'react-native-stylesheet'
).textContent;
expect(cssText).toMatchSnapshot('iframe css');
});

const cssText = Array.prototype.slice
.call(
iframedoc.getElementById('react-native-stylesheet').sheet.cssRules
)
.map((cssRule) => cssRule.cssText);
test('styles roots in shadow trees', () => {
AppRegistry.registerComponent('App', () => NoopComponent);
act(() => {
AppRegistry.runApplication('App', { initialProps: {}, rootTag, mode });
});
// Create shadow dom
const shadowRootHost = document.createElement('div');
const shadowRoot = shadowRootHost.attachShadow({ mode: 'open' });
const shadowContainer = document.createElement('div');
shadowRoot.appendChild(shadowContainer);
document.body.appendChild(shadowRootHost);

expect(cssText).toMatchInlineSnapshot(`
[
"[stylesheet-group=\\"0\\"] {}",
"body {margin: 0;}",
"button::-moz-focus-inner,input::-moz-focus-inner {border: 0; padding: 0;}",
"html {-ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; -webkit-tap-highlight-color: rgba(0,0,0,0);}",
"input::-webkit-search-cancel-button,input::-webkit-search-decoration,input::-webkit-search-results-button,input::-webkit-search-results-decoration {display: none;}",
"[stylesheet-group=\\"1\\"] {}",
".css-view-175oi2r {align-items: stretch; background-color: rgba(0,0,0,0.00); border: 0 solid black; box-sizing: border-box; display: flex; flex-basis: auto; flex-direction: column; flex-shrink: 0; list-style: none; margin: 0px; min-height: 0px; min-width: 0px; padding: 0px; position: relative; text-decoration: none; z-index: 0;}",
"[stylesheet-group=\\"2\\"] {}",
".r-display-xoduu5 {display: inline-flex;}",
".r-flex-13awgt0 {flex: 1;}",
"[stylesheet-group=\\"3\\"] {}",
".r-bottom-1p0dtai {bottom: 0px;}",
".r-left-1d2f490 {left: 0px;}",
".r-pointerEvents-105ug2t {pointer-events: auto !important;}",
".r-pointerEvents-12vffkv>* {pointer-events: auto;}",
".r-pointerEvents-12vffkv {pointer-events: none !important;}",
".r-pointerEvents-633pao {pointer-events: none !important;}",
".r-pointerEvents-ah5dr5>* {pointer-events: none;}",
".r-pointerEvents-ah5dr5 {pointer-events: auto !important;}",
".r-position-u8s1d {position: absolute;}",
".r-right-zchlnj {right: 0px;}",
".r-top-ipm5af {top: 0px;}",
]
`);
// Run in shadow dom
act(() => {
AppRegistry.runApplication('App', {
initialProps: {},
rootTag: shadowContainer,
mode
});
});
const cssText = shadowRoot.getElementById(
'react-native-stylesheet'
).textContent;
expect(cssText).toMatchSnapshot('shadow dom css');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { ComponentType, Node } from 'react';

import AppContainer from './AppContainer';
import invariant from 'fbjs/lib/invariant';
import renderLegacy, { hydrateLegacy, render, hydrate } from '../render';
import render, { hydrateLegacy, renderLegacy, hydrate } from '../render';
import StyleSheet from '../StyleSheet';
import React from 'react';

Expand All @@ -32,6 +32,7 @@ export default function renderApplication<Props: Object>(
}
): Application {
const { hydrate: shouldHydrate, initialProps, mode, rootTag } = options;

const renderFn = shouldHydrate
? mode === 'concurrent'
? hydrate
Expand Down
Loading

0 comments on commit dee1467

Please sign in to comment.