Skip to content

Commit dee1467

Browse files
committed
[fix] 'render' export uses ReactDOM.createRoot
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
1 parent a3ea2a0 commit dee1467

File tree

8 files changed

+308
-110
lines changed

8 files changed

+308
-110
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,137 @@
1-
import React from 'react';
2-
import { AppRegistry, Text, StyleSheet } from 'react-native';
1+
import React, { useRef, useEffect, useState } from 'react';
2+
import { Pressable, Text, StyleSheet, View, render } from 'react-native';
33
import Example from '../../shared/example';
44

5+
function IframeWrapper({ children }) {
6+
const iframeHost = useRef();
7+
const reactRoot = useRef();
8+
9+
useEffect(() => {
10+
if (iframeHost.current) {
11+
if (!reactRoot.current) {
12+
const iframeElement = iframeHost.current;
13+
const iframeAppContainer = document.createElement('div');
14+
iframeElement.contentWindow.document.body.appendChild(
15+
iframeAppContainer
16+
);
17+
reactRoot.current = render(children, iframeAppContainer);
18+
}
19+
reactRoot.current.render(children);
20+
}
21+
});
22+
23+
return <iframe ref={iframeHost} style={{ border: 'none' }} />;
24+
}
25+
26+
function ShadowDomWrapper({ children }) {
27+
const shadowHost = useRef();
28+
const reactRoot = useRef();
29+
30+
useEffect(() => {
31+
if (shadowHost.current) {
32+
if (!reactRoot.current) {
33+
const shadowRoot = shadowHost.current.attachShadow({ mode: 'open' });
34+
reactRoot.current = render(children, shadowRoot);
35+
}
36+
reactRoot.current.render(children);
37+
}
38+
});
39+
40+
return <div ref={shadowHost} />;
41+
}
42+
43+
function Heading({ children }) {
44+
return (
45+
<Text role="heading" style={styles.heading}>
46+
{children}
47+
</Text>
48+
);
49+
}
50+
51+
function Button({ active, onPress, title }) {
52+
return (
53+
<Pressable
54+
onPress={() => onPress((old) => !old)}
55+
style={[styles.button, active && styles.buttonActive]}
56+
>
57+
<Text style={styles.buttonText}>{title}</Text>
58+
</Pressable>
59+
);
60+
}
61+
562
function App() {
6-
return <Text style={styles.text}>Should be red and bold</Text>;
63+
const [active, setActive] = useState(false);
64+
const [activeIframe, setActiveIframe] = useState(false);
65+
const [activeShadow, setActiveShadow] = useState(false);
66+
67+
return (
68+
<Example title="AppRegistry">
69+
<View style={styles.app}>
70+
<View style={styles.header}>
71+
<Heading>Styles in document</Heading>
72+
<Text style={styles.text}>Should be red and bold</Text>
73+
<Button active={active} onPress={setActive} title={'Button'} />
74+
75+
<Heading>Styles in ShadowRoot</Heading>
76+
<ShadowDomWrapper>
77+
<Text style={styles.text}>Should be red and bold</Text>
78+
<Button
79+
active={activeShadow}
80+
onPress={setActiveShadow}
81+
title={'Button'}
82+
/>
83+
</ShadowDomWrapper>
84+
85+
<Heading>Styles in iframe</Heading>
86+
<IframeWrapper>
87+
<Text style={styles.text}>Should be red and bold</Text>
88+
<Button
89+
active={activeIframe}
90+
onPress={setActiveIframe}
91+
title={'Button'}
92+
/>
93+
</IframeWrapper>
94+
</View>
95+
</View>
96+
</Example>
97+
);
798
}
899

9100
const styles = StyleSheet.create({
101+
app: {
102+
marginHorizontal: 'auto',
103+
maxWidth: 500
104+
},
105+
header: {
106+
padding: 20
107+
},
108+
heading: {
109+
fontWeight: 'bold',
110+
fontSize: '1.125rem',
111+
marginBlockStart: '1rem',
112+
marginBlockEnd: '0.25rem'
113+
},
10114
text: {
11115
color: 'red',
12116
fontWeight: 'bold'
117+
},
118+
button: {
119+
backgroundColor: 'red',
120+
paddingBlock: 5,
121+
paddingInline: 10
122+
},
123+
buttonActive: {
124+
backgroundColor: 'blue'
125+
},
126+
buttonText: {
127+
color: 'white',
128+
fontWeight: 'bold',
129+
textAlign: 'center',
130+
textTransform: 'uppercase',
131+
userSelect: 'none'
13132
}
14133
});
15134

16-
AppRegistry.registerComponent('App', () => App);
17-
18135
export default function AppStatePage() {
19-
const iframeRef = React.useRef(null);
20-
const shadowRef = React.useRef(null);
21-
22-
React.useEffect(() => {
23-
const iframeElement = iframeRef.current;
24-
const iframeBody = iframeElement.contentWindow.document.body;
25-
const iframeRootTag = document.createElement('div');
26-
iframeRootTag.id = 'iframe-root';
27-
iframeBody.appendChild(iframeRootTag);
28-
const app1 = AppRegistry.runApplication('App', { rootTag: iframeRootTag });
29-
30-
const shadowElement = shadowRef.current;
31-
const shadowRoot = shadowElement.attachShadow({ mode: 'open' });
32-
const shadowRootTag = document.createElement('div');
33-
shadowRootTag.id = 'shadow-root';
34-
shadowRoot.appendChild(shadowRootTag);
35-
const app2 = AppRegistry.runApplication('App', { rootTag: shadowRootTag });
36-
37-
return () => {
38-
app1.unmount();
39-
app2.unmount();
40-
};
41-
}, []);
42-
43-
return (
44-
<Example title="AppRegistry">
45-
<Text>Styles in iframe</Text>
46-
<iframe ref={iframeRef} />
47-
<Text>Styles in ShadowRoot</Text>
48-
<div ref={shadowRef} />
49-
</Example>
50-
);
136+
return <App />;
51137
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`AppRegistry runApplication styles roots in iframes: iframe css 1`] = `
4+
"[stylesheet-group=\\"0\\"]{}
5+
body{margin:0;}
6+
button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}
7+
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0);}
8+
input::-webkit-search-cancel-button,input::-webkit-search-decoration,input::-webkit-search-results-button,input::-webkit-search-results-decoration{display:none;}
9+
[stylesheet-group=\\"1\\"]{}
10+
.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;}
11+
[stylesheet-group=\\"2\\"]{}
12+
.r-display-xoduu5{display:inline-flex;}
13+
.r-flex-13awgt0{flex:1;}
14+
[stylesheet-group=\\"3\\"]{}
15+
.r-bottom-1p0dtai{bottom:0px;}
16+
.r-left-1d2f490{left:0px;}
17+
.r-pointerEvents-105ug2t{pointer-events:auto!important;}
18+
.r-pointerEvents-12vffkv>*{pointer-events:auto;}
19+
.r-pointerEvents-12vffkv{pointer-events:none!important;}
20+
.r-pointerEvents-633pao{pointer-events:none!important;}
21+
.r-pointerEvents-ah5dr5>*{pointer-events:none;}
22+
.r-pointerEvents-ah5dr5{pointer-events:auto!important;}
23+
.r-position-u8s1d{position:absolute;}
24+
.r-right-zchlnj{right:0px;}
25+
.r-top-ipm5af{top:0px;}"
26+
`;
27+
28+
exports[`AppRegistry runApplication styles roots in iframes: iframe css 2`] = `
29+
"[stylesheet-group=\\"0\\"]{}
30+
body{margin:0;}
31+
button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}
32+
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0);}
33+
input::-webkit-search-cancel-button,input::-webkit-search-decoration,input::-webkit-search-results-button,input::-webkit-search-results-decoration{display:none;}
34+
[stylesheet-group=\\"1\\"]{}
35+
.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;}
36+
[stylesheet-group=\\"2\\"]{}
37+
.r-display-xoduu5{display:inline-flex;}
38+
.r-flex-13awgt0{flex:1;}
39+
[stylesheet-group=\\"3\\"]{}
40+
.r-bottom-1p0dtai{bottom:0px;}
41+
.r-left-1d2f490{left:0px;}
42+
.r-pointerEvents-105ug2t{pointer-events:auto!important;}
43+
.r-pointerEvents-12vffkv>*{pointer-events:auto;}
44+
.r-pointerEvents-12vffkv{pointer-events:none!important;}
45+
.r-pointerEvents-633pao{pointer-events:none!important;}
46+
.r-pointerEvents-ah5dr5>*{pointer-events:none;}
47+
.r-pointerEvents-ah5dr5{pointer-events:auto!important;}
48+
.r-position-u8s1d{position:absolute;}
49+
.r-right-zchlnj{right:0px;}
50+
.r-top-ipm5af{top:0px;}"
51+
`;
52+
53+
exports[`AppRegistry runApplication styles roots in shadow trees: shadow dom css 1`] = `
54+
"[stylesheet-group=\\"0\\"]{}
55+
body{margin:0;}
56+
button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}
57+
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0);}
58+
input::-webkit-search-cancel-button,input::-webkit-search-decoration,input::-webkit-search-results-button,input::-webkit-search-results-decoration{display:none;}
59+
[stylesheet-group=\\"1\\"]{}
60+
.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;}
61+
[stylesheet-group=\\"2\\"]{}
62+
.r-display-xoduu5{display:inline-flex;}
63+
.r-flex-13awgt0{flex:1;}
64+
[stylesheet-group=\\"3\\"]{}
65+
.r-bottom-1p0dtai{bottom:0px;}
66+
.r-left-1d2f490{left:0px;}
67+
.r-pointerEvents-105ug2t{pointer-events:auto!important;}
68+
.r-pointerEvents-12vffkv>*{pointer-events:auto;}
69+
.r-pointerEvents-12vffkv{pointer-events:none!important;}
70+
.r-pointerEvents-633pao{pointer-events:none!important;}
71+
.r-pointerEvents-ah5dr5>*{pointer-events:none;}
72+
.r-pointerEvents-ah5dr5{pointer-events:auto!important;}
73+
.r-position-u8s1d{position:absolute;}
74+
.r-right-zchlnj{right:0px;}
75+
.r-top-ipm5af{top:0px;}"
76+
`;
77+
78+
exports[`AppRegistry runApplication styles roots in shadow trees: shadow dom css 2`] = `
79+
"[stylesheet-group=\\"0\\"]{}
80+
body{margin:0;}
81+
button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}
82+
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0);}
83+
input::-webkit-search-cancel-button,input::-webkit-search-decoration,input::-webkit-search-results-button,input::-webkit-search-results-decoration{display:none;}
84+
[stylesheet-group=\\"1\\"]{}
85+
.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;}
86+
[stylesheet-group=\\"2\\"]{}
87+
.r-display-xoduu5{display:inline-flex;}
88+
.r-flex-13awgt0{flex:1;}
89+
[stylesheet-group=\\"3\\"]{}
90+
.r-bottom-1p0dtai{bottom:0px;}
91+
.r-left-1d2f490{left:0px;}
92+
.r-pointerEvents-105ug2t{pointer-events:auto!important;}
93+
.r-pointerEvents-12vffkv>*{pointer-events:auto;}
94+
.r-pointerEvents-12vffkv{pointer-events:none!important;}
95+
.r-pointerEvents-633pao{pointer-events:none!important;}
96+
.r-pointerEvents-ah5dr5>*{pointer-events:none;}
97+
.r-pointerEvents-ah5dr5{pointer-events:auto!important;}
98+
.r-position-u8s1d{position:absolute;}
99+
.r-right-zchlnj{right:0px;}
100+
.r-top-ipm5af{top:0px;}"
101+
`;

packages/react-native-web/src/exports/AppRegistry/__tests__/index-test.js

+29-34
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,14 @@ describe.each([['concurrent'], ['legacy']])('AppRegistry', (mode) => {
6868
expect(setMountedState).toHaveBeenLastCalledWith(false);
6969
});
7070

71-
test('styles roots in different documents', () => {
71+
test('styles roots in iframes', () => {
7272
AppRegistry.registerComponent('App', () => NoopComponent);
7373
act(() => {
7474
AppRegistry.runApplication('App', { initialProps: {}, rootTag, mode });
7575
});
7676
// Create iframe context
7777
const iframe = document.createElement('iframe');
7878
document.body.appendChild(iframe);
79-
8079
const iframeRootTag = document.createElement('div');
8180
iframeRootTag.id = 'react-iframe-root';
8281
iframe.contentWindow.document.body.appendChild(iframeRootTag);
@@ -90,43 +89,39 @@ describe.each([['concurrent'], ['legacy']])('AppRegistry', (mode) => {
9089
mode
9190
});
9291
});
93-
9492
const iframedoc = iframeRootTag.ownerDocument;
9593
expect(iframedoc).toBe(iframe.contentWindow.document);
9694
expect(iframedoc).not.toBe(document);
95+
const cssText = iframedoc.getElementById(
96+
'react-native-stylesheet'
97+
).textContent;
98+
expect(cssText).toMatchSnapshot('iframe css');
99+
});
97100

98-
const cssText = Array.prototype.slice
99-
.call(
100-
iframedoc.getElementById('react-native-stylesheet').sheet.cssRules
101-
)
102-
.map((cssRule) => cssRule.cssText);
101+
test('styles roots in shadow trees', () => {
102+
AppRegistry.registerComponent('App', () => NoopComponent);
103+
act(() => {
104+
AppRegistry.runApplication('App', { initialProps: {}, rootTag, mode });
105+
});
106+
// Create shadow dom
107+
const shadowRootHost = document.createElement('div');
108+
const shadowRoot = shadowRootHost.attachShadow({ mode: 'open' });
109+
const shadowContainer = document.createElement('div');
110+
shadowRoot.appendChild(shadowContainer);
111+
document.body.appendChild(shadowRootHost);
103112

104-
expect(cssText).toMatchInlineSnapshot(`
105-
[
106-
"[stylesheet-group=\\"0\\"] {}",
107-
"body {margin: 0;}",
108-
"button::-moz-focus-inner,input::-moz-focus-inner {border: 0; padding: 0;}",
109-
"html {-ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; -webkit-tap-highlight-color: rgba(0,0,0,0);}",
110-
"input::-webkit-search-cancel-button,input::-webkit-search-decoration,input::-webkit-search-results-button,input::-webkit-search-results-decoration {display: none;}",
111-
"[stylesheet-group=\\"1\\"] {}",
112-
".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;}",
113-
"[stylesheet-group=\\"2\\"] {}",
114-
".r-display-xoduu5 {display: inline-flex;}",
115-
".r-flex-13awgt0 {flex: 1;}",
116-
"[stylesheet-group=\\"3\\"] {}",
117-
".r-bottom-1p0dtai {bottom: 0px;}",
118-
".r-left-1d2f490 {left: 0px;}",
119-
".r-pointerEvents-105ug2t {pointer-events: auto !important;}",
120-
".r-pointerEvents-12vffkv>* {pointer-events: auto;}",
121-
".r-pointerEvents-12vffkv {pointer-events: none !important;}",
122-
".r-pointerEvents-633pao {pointer-events: none !important;}",
123-
".r-pointerEvents-ah5dr5>* {pointer-events: none;}",
124-
".r-pointerEvents-ah5dr5 {pointer-events: auto !important;}",
125-
".r-position-u8s1d {position: absolute;}",
126-
".r-right-zchlnj {right: 0px;}",
127-
".r-top-ipm5af {top: 0px;}",
128-
]
129-
`);
113+
// Run in shadow dom
114+
act(() => {
115+
AppRegistry.runApplication('App', {
116+
initialProps: {},
117+
rootTag: shadowContainer,
118+
mode
119+
});
120+
});
121+
const cssText = shadowRoot.getElementById(
122+
'react-native-stylesheet'
123+
).textContent;
124+
expect(cssText).toMatchSnapshot('shadow dom css');
130125
});
131126
});
132127
});

packages/react-native-web/src/exports/AppRegistry/renderApplication.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type { ComponentType, Node } from 'react';
1212

1313
import AppContainer from './AppContainer';
1414
import invariant from 'fbjs/lib/invariant';
15-
import renderLegacy, { hydrateLegacy, render, hydrate } from '../render';
15+
import render, { hydrateLegacy, renderLegacy, hydrate } from '../render';
1616
import StyleSheet from '../StyleSheet';
1717
import React from 'react';
1818

@@ -32,6 +32,7 @@ export default function renderApplication<Props: Object>(
3232
}
3333
): Application {
3434
const { hydrate: shouldHydrate, initialProps, mode, rootTag } = options;
35+
3536
const renderFn = shouldHydrate
3637
? mode === 'concurrent'
3738
? hydrate

0 commit comments

Comments
 (0)