Skip to content
Closed
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
35 changes: 35 additions & 0 deletions packages/docs/src/pages/docs/components/react-root-view.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
title: ReactRootView
date: Last Modified
permalink: /docs/react-root-view/index.html
eleventyNavigation:
key: ReactRootView
parent: Components
---

{% import "fragments/macros.html" as macro with context %}

:::lead
Root view component react-native-web app. This components manages HTML content of the window (css styles, root componet, event handling). By default this component is create in the AppRegistry, but you can use it if you want to render the react content into different native window.
:::

```jsx
import { ImageBackground } from 'react-native';

<ReactRootView {...props}>{children}</ReactRootView>;
```

---

## API

### Props

{% call macro.prop('children', '?any') %}
The children of a View element can be other elements and must not include strings (or components that render down to strings).
{% endcall %}

{% call macro.prop('rootTag', '?HTMLElement') %}
The native element where the ReactRootView will be shown.
{% endcall %}

6 changes: 6 additions & 0 deletions packages/examples/pages/_app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import * as React from 'react';
import { ReactRootView } from 'react-native-web';

export default function Application({ Component, pageProps }) {
return <ReactRootView>{<Component {...pageProps} />}</ReactRootView>;
}
80 changes: 80 additions & 0 deletions packages/examples/pages/multi-window/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Button, StyleSheet, Text, View, ReactRootView } from 'react-native';

import Example from '../../shared/example';

export function ChildWindowPage() {
const [clicked, setClicked] = React.useState(false);

function onClick() {
setClicked(true);
}

return (
<View style={styles.childItems}>
<Button onPress={onClick} title="Click" />
<Text>{clicked ? 'Clicked!' : 'Click this button!'}</Text>
</View>
);
}

export default function MultiWindowPage() {
const [rootTagContainer, setRootTagContainer] = React.useState(undefined);

const openWindow = () => {
const childWindow = window.open('', 'Child window', 'left=100,top=100,width=640,height=480');

const title = childWindow.document.createElement('title');
title.innerText = 'Child Window';
childWindow.document.head.appendChild(title);

const rootTag = childWindow.document.createElement('div');
rootTag.style.width = '100%';
rootTag.style.height = '100%';
rootTag.style.display = 'flex';
rootTag.style.margin = '0';
rootTag.style.padding = '0';
childWindow.document.body.appendChild(rootTag);

childWindow.addEventListener('beforeunload', () => {
setRootTagContainer(undefined);
});

setRootTagContainer(rootTag);
};

const closeWindow = () => {
setRootTagContainer(undefined);
};

return (
<Example title="Multi window">
<>
<View style={styles.buttons}>
<Button disabled={!!rootTagContainer} onPress={openWindow} title="Open window" />
<Button disabled={!rootTagContainer} onPress={closeWindow} title="Close window" />
</View>
{rootTagContainer
? ReactDOM.createPortal(
<ReactRootView rootTag={rootTagContainer}>
<ChildWindowPage />
</ReactRootView>,
rootTagContainer
)
: undefined}
</>
</Example>
);
}

const styles = StyleSheet.create({
buttons: {
flexDirection: 'row',
gap: '5px'
},
childItems: {
gap: '100px',
flexGrow: 1
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,56 +4,62 @@ import ActivityIndicator from '..';
import React from 'react';
import { act } from 'react-dom/test-utils';
import { createEventTarget } from 'dom-event-testing-library';
import { render } from '@testing-library/react';
import renderRootView from '../../../exports/AppRegistry/renderRootView';

describe('components/ActivityIndicator', () => {
describe('prop "accessibilityLabel"', () => {
test('value is set', () => {
const { container } = render(<ActivityIndicator accessibilityLabel="accessibility label" />);
const { container } = renderRootView(
<ActivityIndicator accessibilityLabel="accessibility label" />
);
expect(container.firstChild).toMatchSnapshot();
});
});

describe('prop "accessibilityLiveRegion"', () => {
test('value is set', () => {
const { container } = render(<ActivityIndicator accessibilityLiveRegion="polite" />);
const { container } = renderRootView(<ActivityIndicator accessibilityLiveRegion="polite" />);
expect(container.firstChild).toMatchSnapshot();
});
});

describe('prop "animating"', () => {
test('is "true"', () => {
const { container } = render(<ActivityIndicator animating={true} />);
const { container } = renderRootView(<ActivityIndicator animating={true} />);
expect(container.firstChild).toMatchSnapshot();
});

test('is "false"', () => {
const { container } = render(<ActivityIndicator animating={false} />);
const { container } = renderRootView(<ActivityIndicator animating={false} />);
expect(container.firstChild).toMatchSnapshot();
});
});

test('prop "color"', () => {
const { container } = render(<ActivityIndicator color="red" />);
const { container } = renderRootView(<ActivityIndicator color="red" />);
const svg = container.firstChild.querySelector('svg');
expect(svg).toMatchSnapshot();
});

describe('prop "dataSet"', () => {
test('value is set', () => {
const { container } = render(<ActivityIndicator dataSet={{ one: 'one', two: 'two' }} />);
const { container } = renderRootView(
<ActivityIndicator dataSet={{ one: 'one', two: 'two' }} />
);
expect(container.firstChild).toMatchSnapshot();
});
});

describe('prop "hidesWhenStopped"', () => {
test('is "true"', () => {
const { container } = render(<ActivityIndicator animating={false} hidesWhenStopped={true} />);
const { container } = renderRootView(
<ActivityIndicator animating={false} hidesWhenStopped={true} />
);
expect(container.firstChild).toMatchSnapshot();
});

test('is "false"', () => {
const { container } = render(
const { container } = renderRootView(
<ActivityIndicator animating={false} hidesWhenStopped={false} />
);
expect(container.firstChild).toMatchSnapshot();
Expand All @@ -62,7 +68,7 @@ describe('components/ActivityIndicator', () => {

describe('prop "nativeID"', () => {
test('value is set', () => {
const { container } = render(<ActivityIndicator nativeID="123" />);
const { container } = renderRootView(<ActivityIndicator nativeID="123" />);
expect(container.firstChild).toMatchSnapshot();
});
});
Expand All @@ -72,7 +78,7 @@ describe('components/ActivityIndicator', () => {
const onBlur = jest.fn();
const ref = React.createRef();
act(() => {
render(<ActivityIndicator onBlur={onBlur} ref={ref} />);
renderRootView(<ActivityIndicator onBlur={onBlur} ref={ref} />);
});
const target = createEventTarget(ref.current);
const body = createEventTarget(document.body);
Expand All @@ -89,7 +95,7 @@ describe('components/ActivityIndicator', () => {
const onFocus = jest.fn();
const ref = React.createRef();
act(() => {
render(<ActivityIndicator onFocus={onFocus} ref={ref} />);
renderRootView(<ActivityIndicator onFocus={onFocus} ref={ref} />);
});
const target = createEventTarget(ref.current);
act(() => {
Expand All @@ -102,33 +108,33 @@ describe('components/ActivityIndicator', () => {
describe('prop "ref"', () => {
test('value is set', () => {
const ref = jest.fn();
render(<ActivityIndicator ref={ref} />);
renderRootView(<ActivityIndicator ref={ref} />);
expect(ref).toBeCalled();
});
});

describe('prop "size"', () => {
test('is "large"', () => {
const { container } = render(<ActivityIndicator size="large" />);
const { container } = renderRootView(<ActivityIndicator size="large" />);
expect(container.firstChild).toMatchSnapshot();
});

test('is a number', () => {
const { container } = render(<ActivityIndicator size={30} />);
const { container } = renderRootView(<ActivityIndicator size={30} />);
expect(container.firstChild).toMatchSnapshot();
});
});

describe('prop "style"', () => {
test('value is set', () => {
const { container } = render(<ActivityIndicator style={{ borderWidth: 5 }} />);
const { container } = renderRootView(<ActivityIndicator style={{ borderWidth: 5 }} />);
expect(container.firstChild).toMatchSnapshot();
});
});

describe('prop "testID"', () => {
test('value is set', () => {
const { container } = render(<ActivityIndicator testID="123" />);
const { container } = renderRootView(<ActivityIndicator testID="123" />);
expect(container.firstChild).toMatchSnapshot();
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,9 @@ import View from '../View';
type Props = {
WrapperComponent?: ?React.ComponentType<*>,
// $FlowFixMe
children?: React.Children,
rootTag: any
children?: React.Children
};

const RootTagContext: React.Context<any> = React.createContext(null);

export default function AppContainer(props: Props): React.Node {
const { children, WrapperComponent } = props;

Expand All @@ -33,11 +30,9 @@ export default function AppContainer(props: Props): React.Node {
}

return (
<RootTagContext.Provider value={props.rootTag}>
<View pointerEvents="box-none" style={styles.appContainer}>
{innerView}
</View>
</RootTagContext.Provider>
<View pointerEvents="box-none" style={styles.appContainer}>
{innerView}
</View>
);
}

Expand Down
49 changes: 49 additions & 0 deletions packages/react-native-web/src/exports/AppRegistry/ReactRootView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Copyright (c) Ondrej Zaruba.
* Copyright (c) Microsoft Corporation.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import * as React from 'react';
import RootContext, { RootContextType } from './RootContext';
import StyleResolver from '../StyleSheet/StyleResolver';
import ResponderSystem from '../../modules/useResponderEvents/ResponderSystem';

export type ReactRootViewProps = {
rootTag?: HTMLElement,
children?: React.Children,
_styleResolver?: StyleResolver,
_responderSystem?: ResponderSystem
};

export default function ReactRootView(props: ReactRootView): React.Node {
const styleResolver = React.useRef<StyleResolver>();
if (!styleResolver.current) {
styleResolver.current = props._styleResolver || new StyleResolver(props.rootTag);
}

const responderSystem = React.useRef<ResponderSystem>();
if (!responderSystem.current) {
responderSystem.current =
props._responderSystem || new ResponderSystem(props.rootTag?.ownerDocument?.defaultView);
}

React.useEffect(() => {
return () => {
styleResolver.current.clear();
responderSystem.current.terminateResponder();
};
}, []);

const styleContext: RootContextType = {
rootTag: props.rootTag,
styleResolver: styleResolver.current,
responderSystem: responderSystem.current
};

return <RootContext.Provider value={styleContext}>{props.children}</RootContext.Provider>;
}
23 changes: 23 additions & 0 deletions packages/react-native-web/src/exports/AppRegistry/RootContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Copyright (c) Ondrej Zaruba.
* Copyright (c) Microsoft Corporation.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import * as React from 'react';
import ResponderSystem from '../../modules/useResponderEvents/ResponderSystem';
import StyleResolver from '../StyleSheet/StyleResolver';

export type RootContextType = {
styleResoler: StyleResolver,
responderSystem: typeof ResponderSystem,
rootTag?: Node
};

const RootContext = React.createContext<RootContextType>(null);

export default RootContext;
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,30 @@ input::-webkit-search-cancel-button,input::-webkit-search-decoration,input::-web
`;

exports[`AppRegistry getApplication returns "element" and "getStyleElement" 1`] = `
<AppContainer
rootTag={Object {}}
<ReactRootView
_styleResolver={
StyleResolver {
"_cache": Object {},
"_inserted": Object {
"css": Object {},
"ltr": Object {},
"rtl": Object {},
"rtlNoSwap": Object {},
},
"_sheet": Object {
"clear": [Function],
"getTextContent": [Function],
"insert": [Function],
},
"_styleElement": null,
}
}
rootTag={<body />}
>
<NoopComponent />
</AppContainer>
<AppContainer>
<NoopComponent />
</AppContainer>
</ReactRootView>
`;

exports[`AppRegistry getApplication returns "element" and "getStyleElement" 2`] = `
Expand Down
Loading