Skip to content

Commit 28ba788

Browse files
authored
'together' flag tabs loading webviews (#8202)
* fix for TOGETHER flag #8201 * update * webview test fix * android commit * move tab button test to ButtonTab page * update * update * code alignment * test id update * some more refactoring * clean up * first try with e2e test * test update * android test update * update * better naming for units and classes * update * changes
1 parent 91db15b commit 28ba788

File tree

13 files changed

+242
-160
lines changed

13 files changed

+242
-160
lines changed

__mocks__/react-native-webview.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const React = require('react');
2+
3+
const WebView = (props) => React.createElement('WebView', props);
4+
5+
module.exports = { WebView };

ios/BottomTabsTogetherAttacher.mm

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,39 @@ @implementation BottomTabsTogetherAttacher
55

66
- (void)attach:(RNNBottomTabsController *)bottomTabsController {
77
dispatch_group_t ready = dispatch_group_create();
8-
8+
9+
UIWindow *preloadWindow = [[UIWindow alloc] initWithFrame:CGRectZero];
10+
preloadWindow.hidden = NO;
11+
12+
NSMapTable *reactViewToParent = [NSMapTable strongToStrongObjectsMapTable];
13+
914
for (UIViewController *vc in bottomTabsController.childViewControllers) {
1015
dispatch_group_enter(ready);
1116
[vc setReactViewReadyCallback:^{
12-
dispatch_group_leave(ready);
17+
dispatch_group_leave(ready);
1318
}];
19+
1420
[vc render];
21+
22+
if ([vc isKindOfClass:[UINavigationController class]]) {
23+
UIView *containerView = [(UINavigationController *)vc topViewController].view;
24+
UIView *reactView = containerView.subviews.firstObject;
25+
26+
if (reactView && !reactView.window) {
27+
[reactViewToParent setObject:containerView forKey:reactView];
28+
[preloadWindow addSubview:reactView];
29+
}
30+
}
1531
}
16-
32+
1733
dispatch_notify(ready, dispatch_get_main_queue(), ^{
18-
[bottomTabsController readyForPresentation];
34+
for (UIView *reactView in reactViewToParent) {
35+
UIView *parent = [reactViewToParent objectForKey:reactView];
36+
reactView.frame = parent.bounds;
37+
[parent addSubview:reactView];
38+
}
39+
preloadWindow.hidden = YES; //Keep preloadWindow reference alive to this point
40+
[bottomTabsController readyForPresentation];
1941
});
2042
}
2143

jest-setup.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ jest.mock('react-native-gesture-handler', () => {
2121
};
2222
});
2323

24+
2425
mockDetox(() => require('./playground/index'));
2526

2627
beforeEach(() => {

jest.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module.exports = {
22
preset: 'react-native',
33
transformIgnorePatterns: [
4-
'node_modules/(?!(@react-native|react-native|react-native-ui-lib|react-native-animatable|react-native-reanimated)/)',
4+
'node_modules/(?!(@react-native|react-native|react-native-ui-lib|react-native-animatable|react-native-reanimated|react-native-webview)/)',
55
],
66
transform: {
77
'\\.[jt]sx?$': 'babel-jest',
@@ -20,6 +20,7 @@ module.exports = {
2020
moduleNameMapper: {
2121
'^react-native$': '<rootDir>/node_modules/react-native',
2222
'^react-native-gesture-handler$': '<rootDir>/node_modules/react-native-gesture-handler',
23+
'^react-native-webview$': '<rootDir>/__mocks__/react-native-webview.js',
2324
'react-native-navigation/Mock': '<rootDir>/Mock/index',
2425
'react-native-navigation': '<rootDir>/src',
2526
'^src$': '<rootDir>/src',

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,4 +182,4 @@
182182
]
183183
]
184184
}
185-
}
185+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import Utils from './Utils';
2+
import TestIDs from '../src/testIDs';
3+
4+
const { elementById, elementByLabel } = Utils;
5+
6+
describe.e2e(':ios: Tabs with Together flag', () => {
7+
beforeEach(async () => {
8+
await device.launchApp({ newInstance: true });
9+
await elementById(TestIDs.BOTTOM_TABS_BTN).tap();
10+
await expect(elementByLabel('First Tab')).toBeVisible();
11+
});
12+
13+
it('should load all tabs when tabsAttachMode is together', async () => {
14+
await elementById(TestIDs.TABS_TOGETHER_BTN).tap();
15+
await waitFor(element(by.text(/\d\d\d/)))
16+
.toExist()
17+
.withTimeout(5000);
18+
19+
await elementById(TestIDs.TABS_TOGETHER_DISMISS).tap();
20+
await expect(elementByLabel('First Tab')).toBeVisible();
21+
});
22+
});

playground/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"prop-types": "15.x.x",
2222
"react-lifecycles-compat": "^3.0.4",
2323
"react-native-redash": "^12.6.1",
24+
"react-native-webview": "^13.12.5",
2425
"reanimated-color-picker": "^3.0.6",
2526
"ssim.js": "^3.5.0",
2627
"tslib": "1.9.3"

playground/src/screens/FirstBottomTabScreen.tsx

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import React, { Component } from 'react';
2-
import { Text } from 'react-native';
2+
import { EmitterSubscription, Platform, Text } from 'react-native';
33
import { NavigationProps, Options } from 'react-native-navigation';
44

55
import Root from '../components/Root';
66
import Button from '../components/Button';
77
import Navigation from './../services/Navigation';
88
import Screens from './Screens';
9-
import { component } from '../commons/Layouts';
9+
import { stack, component } from '../commons/Layouts';
1010
import testIDs from '../testIDs';
1111
import bottomTabsStruct from './BottomTabsLayoutStructure';
12+
import { resetWebViewLoadedOrder, TAB_SCREENS } from './TabbedWebViewScreen';
1213

1314
export class MountedBottomTabScreensState {
1415
static mountedBottomTabScreens: string[] = [];
15-
static callback: (mountedBottomTabScreens: string[]) => void = () => {};
16+
static callback: (mountedBottomTabScreens: string[]) => void = () => { };
1617

1718
static addScreen(screen: string) {
1819
this.mountedBottomTabScreens.push(screen);
@@ -34,6 +35,7 @@ const {
3435
SCREEN_ROOT,
3536
SET_ROOT_BTN,
3637
BOTTOM_TABS,
38+
TABS_TOGETHER_BTN,
3739
} = testIDs;
3840

3941
interface NavigationState {
@@ -74,9 +76,19 @@ export default class FirstBottomTabScreen extends Component<NavigationProps, Nav
7476
}
7577

7678
badgeVisible = true;
77-
bottomTabPressedListener = Navigation.events().registerBottomTabPressedListener((event) => {
78-
if (event.tabIndex == 2) {
79-
alert('BottomTabPressed');
79+
80+
registerBottomTabListener = () => {
81+
return Navigation.events().registerBottomTabPressedListener((event) => {
82+
if (event.tabIndex == 2) {
83+
alert('BottomTabPressed');
84+
}
85+
});
86+
};
87+
88+
bottomTabPressedListener: EmitterSubscription | null = this.registerBottomTabListener();
89+
modalDismissedListener = Navigation.events().registerModalDismissedListener((event) => {
90+
if (event.componentId === 'TogetherFlagTabTest' && !this.bottomTabPressedListener) {
91+
this.bottomTabPressedListener = this.registerBottomTabListener();
8092
}
8193
});
8294

@@ -93,6 +105,13 @@ export default class FirstBottomTabScreen extends Component<NavigationProps, Nav
93105
testID={SWITCH_TAB_BY_COMPONENT_ID_BTN}
94106
onPress={this.switchTabByComponentId}
95107
/>
108+
{Platform.OS === 'ios' && (
109+
<Button
110+
label="Tabs loading with 'together' flag"
111+
testID={TABS_TOGETHER_BTN}
112+
onPress={this.launchTabbedWebViewScreen}
113+
/>
114+
)}
96115
<Button label="Set Badge" testID={SET_BADGE_BTN} onPress={() => this.setBadge('NEW')} />
97116
<Button label="Clear Badge" testID={CLEAR_BADGE_BTN} onPress={() => this.setBadge('')} />
98117
<Button label="Show Notification Dot" onPress={() => this.setNotificationDot(true)} />
@@ -117,7 +136,8 @@ export default class FirstBottomTabScreen extends Component<NavigationProps, Nav
117136
}
118137

119138
componentWillUnmount() {
120-
this.bottomTabPressedListener.remove();
139+
this.bottomTabPressedListener?.remove();
140+
this.modalDismissedListener.remove();
121141
}
122142

123143
modifyBottomTabs = () => {
@@ -214,4 +234,19 @@ export default class FirstBottomTabScreen extends Component<NavigationProps, Nav
214234
);
215235

216236
push = () => Navigation.push(this, Screens.Pushed);
237+
238+
launchTabbedWebViewScreen = () => {
239+
resetWebViewLoadedOrder();
240+
this.bottomTabPressedListener?.remove();
241+
this.bottomTabPressedListener = null;
242+
Navigation.showModal({
243+
bottomTabs: {
244+
id: 'TogetherFlagTabTest',
245+
options: { bottomTabs: { tabsAttachMode: 'together', titleDisplayMode: 'alwaysShow' } },
246+
children: TAB_SCREENS.map((tab) =>
247+
stack(component(tab.name, undefined, { tabIndex: tab.tabIndex }))
248+
),
249+
},
250+
});
251+
};
217252
}

playground/src/screens/Screens.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ const Screens = {
133133
SearchBarModal: 'SearchBarModal',
134134
TopBar: 'TopBar',
135135
TopBarTitleTest: 'TopBarTitleTest',
136+
WebViewTab: 'TabbedWebViewScreen.WebViewTab',
136137
};
137138

138139
export default Screens;
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import React from 'react';
2+
import { StyleSheet } from 'react-native';
3+
import { NavigationComponent, NavigationProps, Options } from 'react-native-navigation';
4+
import { WebView } from 'react-native-webview';
5+
import Navigation from '../services/Navigation';
6+
import Screens from './Screens';
7+
import testIDs from '../testIDs';
8+
9+
const { TABS_TOGETHER_DISMISS } = testIDs;
10+
11+
const webViewLoadedOrder: number[] = [];
12+
const listeners: Set<() => void> = new Set();
13+
const notifyListeners = () => listeners.forEach((fn) => fn());
14+
15+
const baseOptions = (title: string): Options => ({
16+
topBar: {
17+
title: { text: title },
18+
leftButtons: [
19+
{ id: 'dismiss', testID: TABS_TOGETHER_DISMISS, icon: require('../../img/clear.png') },
20+
],
21+
},
22+
bottomTab: {
23+
text: title,
24+
icon: require('../../img/layouts.png'),
25+
},
26+
});
27+
28+
interface Props extends NavigationProps {
29+
tabIndex: number;
30+
}
31+
32+
interface State {
33+
loadStartTimestamp: number | null;
34+
}
35+
36+
class WebViewTab extends NavigationComponent<Props, State> {
37+
state: State = { loadStartTimestamp: null };
38+
39+
static options(passProps: Props): Options {
40+
return baseOptions(`Tab ${passProps.tabIndex + 1}`);
41+
}
42+
43+
constructor(props: Props) {
44+
super(props);
45+
Navigation.events().bindComponent(this);
46+
}
47+
48+
navigationButtonPressed({ buttonId }: { buttonId: string }) {
49+
if (buttonId === 'dismiss') {
50+
Navigation.dismissModal('TogetherFlagTabTest');
51+
}
52+
}
53+
54+
componentDidMount() {
55+
const update = () => {
56+
const text = webViewLoadedOrder.length > 0 ? webViewLoadedOrder.join('→') : '...';
57+
Navigation.mergeOptions(this.props.componentId, {
58+
topBar: {
59+
subtitle: { text },
60+
},
61+
});
62+
};
63+
listeners.add(update);
64+
update();
65+
}
66+
67+
onLoadStart = () => {
68+
if (!webViewLoadedOrder.includes(this.props.tabIndex)) {
69+
webViewLoadedOrder.push(this.props.tabIndex);
70+
this.setState({ loadStartTimestamp: Date.now() });
71+
notifyListeners();
72+
}
73+
};
74+
75+
render() {
76+
const { loadStartTimestamp } = this.state;
77+
const timeString = loadStartTimestamp
78+
? new Date(loadStartTimestamp).toLocaleTimeString('en-US', {
79+
hour12: false,
80+
hour: '2-digit',
81+
minute: '2-digit',
82+
second: '2-digit',
83+
fractionalSecondDigits: 3,
84+
})
85+
: 'loading...';
86+
87+
return (
88+
<WebView
89+
source={{ html: `<html><body><h1>Tab ${this.props.tabIndex + 1}: started loading at ${timeString}</h1></body></html>` }}
90+
style={styles.webview}
91+
onLoadStart={this.onLoadStart}
92+
/>
93+
);
94+
}
95+
}
96+
97+
export { WebViewTab };
98+
99+
export const resetWebViewLoadedOrder = () => {
100+
webViewLoadedOrder.length = 0;
101+
};
102+
103+
export const TAB_SCREENS = [
104+
{ name: Screens.WebViewTab, tabIndex: 0 },
105+
{ name: Screens.WebViewTab, tabIndex: 1 },
106+
{ name: Screens.WebViewTab, tabIndex: 2 },
107+
];
108+
109+
const styles = StyleSheet.create({
110+
webview: { flex: 1 },
111+
});

0 commit comments

Comments
 (0)