Skip to content

Commit 538b8b3

Browse files
committed
Mocked side-menus - support (#8017)
1 parent 75e57cc commit 538b8b3

File tree

11 files changed

+209
-21
lines changed

11 files changed

+209
-21
lines changed

e2e/SideMenu.test.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import TestIDs from '../playground/src/testIDs';
33

44
const { elementByLabel, elementById } = Utils;
55

6-
describe.e2e('SideMenu', () => {
6+
describe('SideMenu', () => {
77
beforeEach(async () => {
88
await device.launchApp({ newInstance: true });
99
await elementById(TestIDs.SIDE_MENU_BTN).tap();
@@ -38,13 +38,13 @@ describe.e2e('SideMenu', () => {
3838
await expect(elementById(TestIDs.CLOSE_RIGHT_SIDE_MENU_BTN)).toBeNotVisible();
3939
});
4040

41-
it('should rotate', async () => {
41+
it.e2e('should rotate', async () => {
4242
await elementById(TestIDs.OPEN_LEFT_SIDE_MENU_BTN).tap();
4343
await device.setOrientation('landscape');
4444
await expect(elementById(TestIDs.LEFT_SIDE_MENU_PUSH_BTN)).toBeVisible();
4545
});
4646

47-
it(':ios: rotation should update drawer height', async () => {
47+
it.e2e(':ios: rotation should update drawer height', async () => {
4848
await elementById(TestIDs.OPEN_LEFT_SIDE_MENU_BTN).tap();
4949
await expect(elementByLabel('left drawer height: 869')).toBeVisible();
5050
await device.setOrientation('landscape');
@@ -53,23 +53,24 @@ describe.e2e('SideMenu', () => {
5353
await expect(elementByLabel('left drawer height: 869')).toBeVisible();
5454
});
5555

56-
it('should set left drawer width', async () => {
56+
it.e2e('should set left drawer width', async () => {
5757
await elementById(TestIDs.OPEN_LEFT_SIDE_MENU_BTN).tap();
58+
await expect(elementById(TestIDs.SIDE_MENU_LEFT_DRAWER_HEIGHT_TEXT)).toBeVisible();
5859
await expect(elementByLabel('left drawer width: 250')).toBeVisible();
5960
});
6061

61-
it('should change left drawer width', async () => {
62+
it.e2e('should change left drawer width', async () => {
6263
await elementById(TestIDs.CHANGE_LEFT_SIDE_MENU_WIDTH_BTN).tap();
6364
await elementById(TestIDs.OPEN_LEFT_SIDE_MENU_BTN).tap();
6465
await expect(elementByLabel('left drawer width: 50')).toBeVisible();
6566
});
6667

67-
it('should set right drawer width', async () => {
68+
it.e2e('should set right drawer width', async () => {
6869
await elementById(TestIDs.OPEN_RIGHT_SIDE_MENU_BTN).tap();
6970
await expect(elementByLabel('right drawer width: 250')).toBeVisible();
7071
});
7172

72-
it('should change right drawer width', async () => {
73+
it.e2e('should change right drawer width', async () => {
7374
await elementById(TestIDs.CHANGE_RIGHT_SIDE_MENU_WIDTH_BTN).tap();
7475
await elementById(TestIDs.OPEN_RIGHT_SIDE_MENU_BTN).tap();
7576
await expect(elementByLabel('right drawer width: 50')).toBeVisible();

lib/Mock/Components/ComponentScreen.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ export const ComponentScreen = connect(
2525
}
2626

2727
isVisible(): boolean {
28-
return LayoutStore.isVisibleLayout(this.props.layoutNode);
28+
const isVisible = LayoutStore.isVisibleLayout(this.props.layoutNode);
29+
return isVisible;
2930
}
3031

3132
renderTabBar() {

lib/Mock/Components/LayoutComponent.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { BottomTabs } from './BottomTabs';
44
import { ComponentProps } from '../ComponentProps';
55
import { ComponentScreen } from './ComponentScreen';
66
import { Stack } from './Stack';
7+
import { SideMenuRoot, SideMenuCenter, SideMenuLeft, SideMenuRight } from './SideMenu';
78

89
export const LayoutComponent = class extends Component<ComponentProps> {
910
render() {
@@ -14,6 +15,14 @@ export const LayoutComponent = class extends Component<ComponentProps> {
1415
return <Stack layoutNode={this.props.layoutNode} />;
1516
case 'Component':
1617
return <ComponentScreen layoutNode={this.props.layoutNode} />;
18+
case 'SideMenuRoot':
19+
return <SideMenuRoot layoutNode={this.props.layoutNode} />;
20+
case 'SideMenuLeft':
21+
return <SideMenuLeft layoutNode={this.props.layoutNode} />;
22+
case 'SideMenuCenter':
23+
return <SideMenuCenter layoutNode={this.props.layoutNode} />;
24+
case 'SideMenuRight':
25+
return <SideMenuRight layoutNode={this.props.layoutNode} />;
1726
}
1827

1928
return <View />;

lib/Mock/Components/SideMenu.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import React, { Component } from 'react';
2+
import { connect } from '../connect';
3+
import { ComponentProps } from '../ComponentProps';
4+
import { LayoutComponent } from './LayoutComponent';
5+
import ParentNode from '../Layouts/ParentNode';
6+
7+
export const SideMenuRoot = connect(
8+
class extends Component<ComponentProps> {
9+
render() {
10+
const children = this.props.layoutNode.children;
11+
return children.map((child: ParentNode) => {
12+
return <LayoutComponent key={child.nodeId} layoutNode={child} />;
13+
});
14+
}
15+
}
16+
);
17+
18+
class SideMenuComponent extends Component<ComponentProps> {
19+
render() {
20+
const children = this.props.layoutNode.children;
21+
const component = children[0];
22+
return <LayoutComponent key={component.nodeId} layoutNode={component} />;
23+
}
24+
}
25+
export const SideMenuLeft = connect(SideMenuComponent);
26+
export const SideMenuCenter = connect(SideMenuComponent);
27+
export const SideMenuRight = connect(SideMenuComponent);

lib/Mock/Layouts/BottomTabsNode.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ export default class BottomTabsNode extends ParentNode {
1010
this.selectedIndex = layout.data?.options?.bottomTabs?.currentTabIndex || 0;
1111
}
1212

13-
mergeOptions(options: Options) {
14-
super.mergeOptions(options);
13+
mergeOptions(_options: Options) {
14+
super.mergeOptions(_options);
15+
16+
const { options } = this.data;
1517
if (options.bottomTabs?.currentTabIndex) {
1618
this.selectedIndex = options.bottomTabs?.currentTabIndex;
1719
switchTabByIndex(this, this.selectedIndex);

lib/Mock/Layouts/LayoutNodeFactory.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ import BottomTabs from './BottomTabsNode';
22
import ComponentNode from './ComponentNode';
33
import Stack from './StackNode';
44
import ParentNode from './ParentNode';
5+
import SideMenuRootNode, {
6+
SideMenuLeftNode,
7+
SideMenuRightNode,
8+
SideMenuCenterNode,
9+
} from './SideMenu';
510

611
export default class LayoutNodeFactory {
712
static create(layout: any, parentNode?: ParentNode) {
@@ -10,7 +15,15 @@ export default class LayoutNodeFactory {
1015
return new ComponentNode(layout, parentNode);
1116
case 'Stack':
1217
return new Stack(layout, parentNode);
13-
default:
18+
case 'SideMenuRoot':
19+
return new SideMenuRootNode(layout, parentNode);
20+
case 'SideMenuLeft':
21+
return new SideMenuLeftNode(layout, parentNode);
22+
case 'SideMenuCenter':
23+
return new SideMenuCenterNode(layout, parentNode);
24+
case 'SideMenuRight':
25+
return new SideMenuRightNode(layout, parentNode);
26+
default: // TODO Undo
1427
case 'BottomTabs':
1528
return new BottomTabs(layout, parentNode);
1629
}

lib/Mock/Layouts/ParentNode.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ export default class ParentNode extends Node {
3535
return this;
3636
}
3737

38+
applyOptions(_options: Options) {
39+
this.parentNode?.applyOptions(_options);
40+
}
41+
3842
mergeOptions(options: Options) {
3943
this.data.options = _.mergeWith(this.data.options, options, (objValue, srcValue, key) => {
4044
if (_.isArray(objValue)) {

lib/Mock/Layouts/SideMenu.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import ParentNode from './ParentNode';
2+
import ComponentNode from './ComponentNode';
3+
import { Options } from '../../src/index';
4+
import * as layoutActions from '../actions/layoutActions';
5+
import { NodeType } from './Node';
6+
7+
const isCenterChild = (child: ParentNode) => child.type === 'SideMenuCenter';
8+
const isLeftChild = (child: ParentNode) => child.type === 'SideMenuLeft';
9+
const isRightChild = (child: ParentNode) => child.type === 'SideMenuRight';
10+
11+
export default class SideMenuRootNode extends ParentNode {
12+
visibleChild: ParentNode;
13+
14+
constructor(layout: any, parentNode?: ParentNode) {
15+
super(layout, 'SideMenuRoot', parentNode);
16+
17+
this.visibleChild = this._getCenterChild();
18+
if (!this.visibleChild) {
19+
throw new Error('SideMenuRootNode must have a SideMenuCenter child');
20+
}
21+
}
22+
23+
applyOptions(_options: Options) {
24+
super.applyOptions(_options);
25+
26+
this._updateVisibility(_options);
27+
}
28+
29+
mergeOptions(options: Options) {
30+
super.mergeOptions(options);
31+
32+
this._updateVisibility(options);
33+
}
34+
35+
/**
36+
* @override
37+
*/
38+
getVisibleLayout(): ComponentNode {
39+
return this.visibleChild.getVisibleLayout();
40+
}
41+
42+
_updateVisibility(options: Options) {
43+
if (options.sideMenu) {
44+
if (options.sideMenu.left?.visible) {
45+
this.visibleChild = this._getLeftChild();
46+
layoutActions.openSideMenu(this.visibleChild);
47+
} else if (options.sideMenu.right?.visible) {
48+
this.visibleChild = this._getRightChild();
49+
layoutActions.openSideMenu(this.visibleChild);
50+
} else {
51+
this.visibleChild = this._getCenterChild();
52+
layoutActions.closeSideMenu(this.visibleChild);
53+
}
54+
}
55+
}
56+
57+
_getCenterChild = () => this.children.find(isCenterChild) as ParentNode;
58+
_getLeftChild = () => this.children.find(isLeftChild) as ParentNode;
59+
_getRightChild = () => this.children.find(isRightChild) as ParentNode;
60+
}
61+
62+
export class SideMenuNode extends ParentNode {
63+
constructor(layout: any, type: NodeType, parentNode?: ParentNode) {
64+
super(layout, type, parentNode);
65+
}
66+
67+
getVisibleLayout() {
68+
return this.children[0].getVisibleLayout();
69+
}
70+
}
71+
72+
export class SideMenuLeftNode extends SideMenuNode {
73+
constructor(layout: any, parentNode?: ParentNode) {
74+
super(layout, 'SideMenuLeft', parentNode);
75+
}
76+
}
77+
export class SideMenuRightNode extends SideMenuNode {
78+
constructor(layout: any, parentNode?: ParentNode) {
79+
super(layout, 'SideMenuRight', parentNode);
80+
}
81+
}
82+
83+
export class SideMenuCenterNode extends SideMenuNode {
84+
constructor(layout: any, parentNode?: ParentNode) {
85+
super(layout, 'SideMenuCenter', parentNode);
86+
}
87+
}

lib/Mock/Stores/LayoutStore.ts

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@ import _ from 'lodash';
22
import BottomTabsNode from '../Layouts/BottomTabsNode';
33
import ParentNode from '../Layouts/ParentNode';
44
import LayoutNodeFactory from '../Layouts/LayoutNodeFactory';
5-
import { Options } from '../../src/interfaces/Options';
5+
import { SideMenuNode } from '../Layouts/SideMenu';
66
import StackNode from '../Layouts/StackNode';
7+
import { Options } from '../../src/interfaces/Options';
78

89
const remx = require('remx');
910

1011
const state = remx.state({
1112
root: {},
1213
modals: [],
1314
overlays: [],
15+
sideMenu: undefined,
1416
});
1517

1618
const setters = remx.setters({
@@ -75,6 +77,23 @@ const setters = remx.setters({
7577
selectTabIndex(layout: BottomTabsNode, index: number) {
7678
getters.getLayoutById(layout.nodeId).selectedIndex = index;
7779
},
80+
openSideMenu(layout: SideMenuNode) {
81+
if (state.sideMenu) {
82+
throw new Error(
83+
'A side-menu is already open; Mocked-testing of multiple side-menu scenarios is not supported yet.' +
84+
' You can submit a request in https://github.com/wix/react-native-navigation/issues/new/choose.'
85+
);
86+
}
87+
state.sideMenu = layout;
88+
},
89+
closeSideMenu(_layout: SideMenuNode) {
90+
state.sideMenu = undefined;
91+
},
92+
applyOptions(componentId: string, options: Options) {
93+
const layout = getters.getLayoutById(componentId);
94+
if (layout) layout.applyOptions(options);
95+
else console.warn(`[RNN error] Merge options failure: cannot find layout for: ${componentId}`);
96+
},
7897
mergeOptions(componentId: string, options: Options) {
7998
const layout = getters.getLayoutById(componentId);
8099
if (layout) layout.mergeOptions(options);
@@ -87,12 +106,26 @@ const getters = remx.getters({
87106
return state.root;
88107
},
89108
getVisibleLayout() {
109+
let layout: ParentNode | undefined;
90110
if (state.modals.length > 0) {
91-
return _.last<ParentNode>(state.modals)!.getVisibleLayout();
92-
} else if (!_.isEqual(state.root, {})) return state.root.getVisibleLayout();
111+
layout = _.last<ParentNode>(state.modals)!;
112+
} else if (!_.isEqual(state.root, {})) {
113+
layout = state.root;
114+
}
115+
116+
// Note: While this logic should be fair for all use cases (i.e. even multiple side-menus across tabs),
117+
// there is no current test case that justifies it. Nevertheless, it's required to pass the tests,
118+
// because otherwise getVisibleLayout() would not be revisited whenever side-menus are opened/closed.
119+
if (layout && state.sideMenu && findNode(state.sideMenu.nodeId, layout!)) {
120+
layout = state.sideMenu.parentNode;
121+
}
122+
123+
return layout?.getVisibleLayout();
93124
},
94125
isVisibleLayout(layout: ParentNode) {
95-
return getters.getVisibleLayout() && getters.getVisibleLayout().nodeId === layout.nodeId;
126+
const nodeId = layout.nodeId;
127+
const visibleLayout = getters.getVisibleLayout();
128+
return visibleLayout?.nodeId === nodeId;
96129
},
97130
getModals() {
98131
return state.modals;
@@ -101,13 +134,12 @@ const getters = remx.getters({
101134
return state.overlays;
102135
},
103136
getLayoutById(layoutId: string) {
104-
if (getters.getModalById(layoutId))
105-
return findParentNode(layoutId, getters.getModalById(layoutId));
137+
if (getters.getModalById(layoutId)) return findNode(layoutId, getters.getModalById(layoutId));
106138

107-
return findParentNode(layoutId, state.root);
139+
return findNode(layoutId, state.root);
108140
},
109141
getModalById(layoutId: string) {
110-
return _.find(state.modals, (layout) => findParentNode(layoutId, layout));
142+
return _.find(state.modals, (layout) => findNode(layoutId, layout));
111143
},
112144
getLayoutChildren(layoutId: string) {
113145
return getters.getLayoutById(layoutId).children;
@@ -120,13 +152,13 @@ const getters = remx.getters({
120152
},
121153
});
122154

123-
function findParentNode(layoutId: string, layout: ParentNode): any | ParentNode {
155+
function findNode(layoutId: string, layout: ParentNode): any | ParentNode {
124156
if (layoutId === layout.nodeId) {
125157
return layout;
126158
} else if (layout.children) {
127159
for (let i = 0; i < layout.children.length; i += 1) {
128160
const child = layout.children[i];
129-
const result = findParentNode(layoutId, child);
161+
const result = findNode(layoutId, child);
130162

131163
if (result !== false) {
132164
return result;

lib/Mock/actions/layoutActions.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import ParentNode from '../Layouts/ParentNode';
2+
import { SideMenuNode } from '../Layouts/SideMenu';
23
import { LayoutStore } from '../Stores/LayoutStore';
34

45
export const switchTabByIndex = (bottomTabs: ParentNode | undefined, index: number) => {
@@ -8,3 +9,13 @@ export const switchTabByIndex = (bottomTabs: ParentNode | undefined, index: numb
89
LayoutStore.getVisibleLayout().componentDidAppear();
910
}
1011
};
12+
13+
export const openSideMenu = (sideMenu: SideMenuNode) => {
14+
LayoutStore.openSideMenu(sideMenu);
15+
LayoutStore.getVisibleLayout().componentDidAppear();
16+
};
17+
18+
export const closeSideMenu = (layout: SideMenuNode) => {
19+
LayoutStore.getVisibleLayout().componentDidDisappear();
20+
LayoutStore.closeSideMenu(layout);
21+
};

0 commit comments

Comments
 (0)