Skip to content

Commit 389d7b0

Browse files
author
Hayata Suenaga
authored
Merge pull request #28277 from adamgrzybowski/@swm/global-nav-menu-v1
@swm/global nav menu v1
2 parents 168e7f7 + 9e87953 commit 389d7b0

30 files changed

+408
-58
lines changed

src/App.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {ReportAttachmentsProvider} from './pages/home/report/ReportAttachmentsCo
2626
import * as Session from './libs/actions/Session';
2727
import useDefaultDragAndDrop from './hooks/useDefaultDragAndDrop';
2828
import OnyxUpdateManager from './libs/actions/OnyxUpdateManager';
29+
import {SidebarNavigationContextProvider} from './pages/home/sidebar/SidebarNavigationContext';
2930

3031
// For easier debugging and development, when we are in web we expose Onyx to the window, so you can more easily set data into Onyx
3132
if (window && Environment.isDevelopment()) {
@@ -64,6 +65,7 @@ function App() {
6465
EnvironmentProvider,
6566
ThemeProvider,
6667
ThemeStylesProvider,
68+
SidebarNavigationContextProvider,
6769
]}
6870
>
6971
<CustomStatusBar />

src/CONST.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2702,19 +2702,29 @@ const CONST = {
27022702
DEFAULT_COORDINATE: [-122.4021, 37.7911],
27032703
STYLE_URL: 'mapbox://styles/expensify/cllcoiqds00cs01r80kp34tmq',
27042704
},
2705+
27052706
ONYX_UPDATE_TYPES: {
27062707
HTTPS: 'https',
27072708
PUSHER: 'pusher',
27082709
},
2710+
27092711
EVENTS: {
27102712
SCROLLING: 'scrolling',
27112713
},
2714+
27122715
HORIZONTAL_SPACER: {
27132716
DEFAULT_BORDER_BOTTOM_WIDTH: 1,
27142717
DEFAULT_MARGIN_VERTICAL: 8,
27152718
HIDDEN_MARGIN_VERTICAL: 0,
27162719
HIDDEN_BORDER_BOTTOM_WIDTH: 0,
27172720
},
2721+
2722+
GLOBAL_NAVIGATION_OPTION: {
2723+
HOME: 'home',
2724+
CHATS: 'chats',
2725+
SPEND: 'spend',
2726+
WORKSPACES: 'workspaces',
2727+
},
27182728
} as const;
27192729

27202730
export default CONST;

src/GLOBAL_NAVIGATION_MAPPING.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import CONST from './CONST';
2+
import SCREENS from './SCREENS';
3+
4+
export default {
5+
[CONST.GLOBAL_NAVIGATION_OPTION.HOME]: [SCREENS.HOME_OLDDOT],
6+
[CONST.GLOBAL_NAVIGATION_OPTION.CHATS]: [SCREENS.REPORT],
7+
[CONST.GLOBAL_NAVIGATION_OPTION.SPEND]: [SCREENS.EXPENSES_OLDDOT, SCREENS.REPORTS_OLDDOT, SCREENS.INSIGHTS_OLDDOT],
8+
[CONST.GLOBAL_NAVIGATION_OPTION.WORKSPACES]: [SCREENS.INDIVIDUAL_WORKSPACES_OLDDOT, SCREENS.GROUPS_WORKSPACES_OLDDOT, SCREENS.CARDS_AND_DOMAINS_OLDDOT],
9+
} as const;

src/ROUTES.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,4 +318,17 @@ export default {
318318
// These are some on-off routes that will be removed once they're no longer needed (see GH issues for details)
319319
SAASTR: 'saastr',
320320
SBE: 'sbe',
321+
322+
// Iframe screens from olddot
323+
HOME_OLDDOT: 'home',
324+
325+
// Spend tab
326+
EXPENSES_OLDDOT: 'expenses',
327+
REPORTS_OLDDOT: 'reports',
328+
INSIGHTS_OLDDOT: 'insights',
329+
330+
// Workspaces tab
331+
INDIVIDUALS_OLDDOT: 'individual_workspaces',
332+
GROUPS_OLDDOT: 'group_workspaces',
333+
CARDS_AND_DOMAINS_OLDDOT: 'cards-and-domains',
321334
} as const;

src/SCREENS.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,17 @@ export default {
2424
SIGN_IN_WITH_APPLE_DESKTOP: 'AppleSignInDesktop',
2525
SIGN_IN_WITH_GOOGLE_DESKTOP: 'GoogleSignInDesktop',
2626
DESKTOP_SIGN_IN_REDIRECT: 'DesktopSignInRedirect',
27+
28+
// Iframe screens from olddot
29+
HOME_OLDDOT: 'Home_OLDDOT',
30+
31+
// Spend tab
32+
EXPENSES_OLDDOT: 'Expenses_OLDDOT',
33+
REPORTS_OLDDOT: 'Reports_OLDDOT',
34+
INSIGHTS_OLDDOT: 'Insights_OLDDOT',
35+
36+
// Workspaces tab
37+
INDIVIDUAL_WORKSPACES_OLDDOT: 'IndividualWorkspaces_OLDDOT',
38+
GROUPS_WORKSPACES_OLDDOT: 'GroupWorkspaces_OLDDOT',
39+
CARDS_AND_DOMAINS_OLDDOT: 'CardsAndDomains_OLDDOT',
2740
} as const;

src/components/EnvironmentBadge.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ function EnvironmentBadge() {
2828
success={environment === CONST.ENVIRONMENT.STAGING || environment === CONST.ENVIRONMENT.ADHOC}
2929
error={environment !== CONST.ENVIRONMENT.STAGING && environment !== CONST.ENVIRONMENT.ADHOC}
3030
text={text}
31-
badgeStyles={[styles.alignSelfEnd, styles.headerEnvBadge]}
31+
badgeStyles={[styles.alignSelfEnd, styles.headerEnvBadge, styles.ml1]}
3232
textStyles={[styles.headerEnvBadgeText]}
3333
environment={environment}
3434
/>

src/components/FloatingActionButton.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import themeColors from '../styles/themes/default';
99
import Tooltip from './Tooltip';
1010
import withLocalize, {withLocalizePropTypes} from './withLocalize';
1111
import PressableWithFeedback from './Pressable/PressableWithFeedback';
12+
import variables from '../styles/variables';
1213

1314
const AnimatedIcon = Animated.createAnimatedComponent(Icon);
1415
AnimatedIcon.displayName = 'AnimatedIcon';
@@ -100,6 +101,8 @@ class FloatingActionButton extends PureComponent {
100101
style={[styles.floatingActionButton, StyleUtils.getAnimatedFABStyle(rotate, backgroundColor)]}
101102
>
102103
<AnimatedIcon
104+
width={variables.iconSizeSmall}
105+
height={variables.iconSizeSmall}
103106
src={Expensicons.Plus}
104107
fill={fill}
105108
/>

src/components/LHNOptionsList/OptionRowLHN.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ const propTypes = {
5656
};
5757

5858
const defaultProps = {
59-
hoverStyle: styles.sidebarLinkHover,
59+
hoverStyle: styles.sidebarLinkHoverLHN,
6060
viewMode: 'default',
6161
onSelectRow: () => {},
6262
style: null,
@@ -110,7 +110,7 @@ function OptionRowLHN(props) {
110110
: [styles.chatLinkRowPressable, styles.flexGrow1, styles.optionItemAvatarNameWrapper, styles.optionRow, styles.justifyContentCenter],
111111
);
112112
const hoveredBackgroundColor = props.hoverStyle && props.hoverStyle.backgroundColor ? props.hoverStyle.backgroundColor : themeColors.sidebar;
113-
const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor;
113+
const focusedBackgroundColor = styles.sidebarLinkActiveLHN.backgroundColor;
114114

115115
const hasBrickError = optionItem.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR;
116116
const defaultSubscriptSize = optionItem.isExpenseRequest ? CONST.AVATAR_SIZE.SMALL_NORMAL : CONST.AVATAR_SIZE.DEFAULT;
@@ -185,8 +185,8 @@ function OptionRowLHN(props) {
185185
styles.flexRow,
186186
styles.alignItemsCenter,
187187
styles.justifyContentBetween,
188-
styles.sidebarLink,
189-
styles.sidebarLinkInner,
188+
styles.sidebarLinkLHN,
189+
styles.sidebarLinkInnerLHN,
190190
StyleUtils.getBackgroundColorStyle(themeColors.sidebar),
191191
props.isFocused ? styles.sidebarLinkActive : null,
192192
(hovered || isContextMenuActive) && !props.isFocused ? props.hoverStyle : null,

src/languages/en.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1816,4 +1816,7 @@ export default {
18161816
selectSuggestedAddress: 'Please select a suggested address or use current location',
18171817
},
18181818
},
1819+
globalNavigationOptions: {
1820+
chats: 'Chats',
1821+
},
18191822
} satisfies TranslationBase;

src/languages/es.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2300,4 +2300,7 @@ export default {
23002300
selectSuggestedAddress: 'Por favor, selecciona una dirección sugerida o usa la ubicación actual.',
23012301
},
23022302
},
2303+
globalNavigationOptions: {
2304+
chats: 'Chats',
2305+
},
23032306
} satisfies EnglishTranslation;

src/libs/Navigation/Navigation.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import linkingConfig from './linkingConfig';
1010
import navigationRef from './navigationRef';
1111
import NAVIGATORS from '../../NAVIGATORS';
1212
import originalGetTopmostReportId from './getTopmostReportId';
13+
import originalGetTopMostCentralPaneRouteName from './getTopMostCentralPaneRouteName';
1314
import originalGetTopmostReportActionId from './getTopmostReportActionID';
1415
import getStateFromPath from './getStateFromPath';
1516
import SCREENS from '../../SCREENS';
@@ -47,6 +48,9 @@ function canNavigate(methodName, params = {}) {
4748
// Re-exporting the getTopmostReportId here to fill in default value for state. The getTopmostReportId isn't defined in this file to avoid cyclic dependencies.
4849
const getTopmostReportId = (state = navigationRef.getState()) => originalGetTopmostReportId(state);
4950

51+
// Re-exporting the getTopMostCentralPaneRouteName here to fill in default value for state. The getTopMostCentralPaneRouteName isn't defined in this file to avoid cyclic dependencies.
52+
const getTopMostCentralPaneRouteName = (state = navigationRef.getState()) => originalGetTopMostCentralPaneRouteName(state);
53+
5054
// Re-exporting the getTopmostReportActionID here to fill in default value for state. The getTopmostReportActionID isn't defined in this file to avoid cyclic dependencies.
5155
const getTopmostReportActionId = (state = navigationRef.getState()) => originalGetTopmostReportActionId(state);
5256

@@ -272,6 +276,7 @@ export default {
272276
setIsNavigationReady,
273277
getTopmostReportId,
274278
getRouteNameFromStateEvent,
279+
getTopMostCentralPaneRouteName,
275280
getTopmostReportActionId,
276281
};
277282

src/libs/Navigation/NavigationRoot.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, {useRef, useEffect} from 'react';
1+
import React, {useRef, useEffect, useContext} from 'react';
22
import PropTypes from 'prop-types';
33
import {NavigationContainer, DefaultTheme, getPathFromState} from '@react-navigation/native';
44
import {useSharedValue, useAnimatedReaction, interpolateColor, withTiming, withDelay, Easing, runOnJS} from 'react-native-reanimated';
@@ -11,6 +11,7 @@ import Log from '../Log';
1111
import StatusBar from '../StatusBar';
1212
import useCurrentReportID from '../../hooks/useCurrentReportID';
1313
import useWindowDimensions from '../../hooks/useWindowDimensions';
14+
import {SidebarNavigationContext} from '../../pages/home/sidebar/SidebarNavigationContext';
1415

1516
// https://reactnavigation.org/docs/themes
1617
const navigationTheme = {
@@ -53,6 +54,7 @@ function parseAndLogRoute(state) {
5354
function NavigationRoot(props) {
5455
useFlipper(navigationRef);
5556
const firstRenderRef = useRef(true);
57+
const globalNavigation = useContext(SidebarNavigationContext);
5658

5759
const {updateCurrentReportID} = useCurrentReportID();
5860
const {isSmallScreenWidth} = useWindowDimensions();
@@ -128,6 +130,9 @@ function NavigationRoot(props) {
128130
}, 0);
129131
parseAndLogRoute(state);
130132
animateStatusBarBackgroundColor();
133+
134+
// Update the global navigation to show the correct selected menu items.
135+
globalNavigation.updateFromNavigationState(state);
131136
};
132137

133138
return (
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import lodashFindLast from 'lodash/findLast';
2+
3+
/**
4+
* Find the name of top most central pane route.
5+
*
6+
* @param {Object} state - The react-navigation state
7+
* @returns {String | undefined} - It's possible that there is no central pane in the state.
8+
*/
9+
function getTopMostCentralPaneRouteName(state) {
10+
if (!state) {
11+
return undefined;
12+
}
13+
const topmostCentralPane = lodashFindLast(state.routes, (route) => route.name === 'CentralPaneNavigator');
14+
15+
if (!topmostCentralPane) {
16+
return undefined;
17+
}
18+
19+
if (topmostCentralPane.state && topmostCentralPane.state.routes) {
20+
// State may don't have index in some cases. But in this case there will be only one route in state.
21+
return topmostCentralPane.state.routes[topmostCentralPane.state.index || 0].name;
22+
}
23+
24+
if (topmostCentralPane.params) {
25+
// State may don't have inner state in some cases (e.g generating actions from path). But in this case there will be params available.
26+
return topmostCentralPane.params.screen;
27+
}
28+
29+
return undefined;
30+
}
31+
32+
export default getTopMostCentralPaneRouteName;

src/libs/Navigation/linkTo.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import linkingConfig from './linkingConfig';
55
import getTopmostReportId from './getTopmostReportId';
66
import getStateFromPath from './getStateFromPath';
77
import CONST from '../../CONST';
8+
import getTopMostCentralPaneRouteName from './getTopMostCentralPaneRouteName';
89

910
/**
1011
* Motivation for this function is described in NAVIGATION.md
@@ -61,12 +62,15 @@ export default function linkTo(navigation, path, type) {
6162

6263
// If action type is different than NAVIGATE we can't change it to the PUSH safely
6364
if (action.type === CONST.NAVIGATION.ACTION_TYPE.NAVIGATE) {
65+
// Make sure that we are pushing a screen that is not currently on top of the stack.
66+
const shouldPushIfCentralPane =
67+
action.payload.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR &&
68+
(getTopMostCentralPaneRouteName(root.getState()) !== getTopMostCentralPaneRouteName(state) || getTopmostReportId(root.getState()) !== getTopmostReportId(state));
69+
6470
// In case if type is 'FORCED_UP' we replace current screen with the provided. This means the current screen no longer exists in the stack
6571
if (type === CONST.NAVIGATION.TYPE.FORCED_UP) {
6672
action.type = CONST.NAVIGATION.ACTION_TYPE.REPLACE;
67-
68-
// If this action is navigating to the report screen and the top most navigator is different from the one we want to navigate - PUSH the new screen to the top of the stack
69-
} else if (action.payload.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR && getTopmostReportId(root.getState()) !== getTopmostReportId(state)) {
73+
} else if (shouldPushIfCentralPane) {
7074
action.type = CONST.NAVIGATION.ACTION_TYPE.PUSH;
7175

7276
// If the type is UP, we deeplinked into one of the RHP flows and we want to replace the current screen with the previous one in the flow
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import React from 'react';
2+
import {View} from 'react-native';
3+
import PropTypes from 'prop-types';
4+
import Text from '../../../../components/Text';
5+
import styles from '../../../../styles/styles';
6+
import * as StyleUtils from '../../../../styles/StyleUtils';
7+
import Icon from '../../../../components/Icon';
8+
import CONST from '../../../../CONST';
9+
import variables from '../../../../styles/variables';
10+
import PressableWithFeedback from '../../../../components/Pressable/PressableWithFeedback';
11+
12+
const propTypes = {
13+
/** Icon to display */
14+
icon: PropTypes.elementType,
15+
16+
/** Text to display for the item */
17+
title: PropTypes.string,
18+
19+
/** Function to fire when component is pressed */
20+
onPress: PropTypes.func,
21+
22+
/** Whether item is focused or active */
23+
isFocused: PropTypes.bool,
24+
};
25+
26+
const defaultProps = {
27+
icon: undefined,
28+
isFocused: false,
29+
onPress: () => {},
30+
title: '',
31+
};
32+
33+
const GlobalNavigationMenuItem = React.forwardRef(({icon, title, isFocused, onPress}, ref) => (
34+
<PressableWithFeedback
35+
onPress={() => !isFocused && onPress()}
36+
style={styles.globalNavigationItemContainer}
37+
ref={ref}
38+
accessibilityRole={CONST.ACCESSIBILITY_ROLE.MENUITEM}
39+
accessibilityLabel={title}
40+
>
41+
{({pressed}) => (
42+
<View style={[styles.alignItemsCenter, styles.flexRow, styles.flex1]}>
43+
<View style={styles.globalNavigationSelectionIndicator(isFocused)} />
44+
<View style={[styles.flexColumn, styles.flex1, styles.alignItemsCenter, styles.mr1]}>
45+
<Icon
46+
additionalStyles={[styles.popoverMenuIcon]}
47+
pressed={pressed}
48+
src={icon}
49+
fill={isFocused ? StyleUtils.getIconFillColor(CONST.BUTTON_STATES.DEFAULT, true) : StyleUtils.getIconFillColor()}
50+
/>
51+
<View style={[styles.mt1, styles.alignItemsCenter]}>
52+
<Text style={[StyleUtils.getFontSizeStyle(variables.fontSizeExtraSmall), styles.globalNavigationMenuItem(isFocused)]}>{title}</Text>
53+
</View>
54+
</View>
55+
</View>
56+
)}
57+
</PressableWithFeedback>
58+
));
59+
60+
GlobalNavigationMenuItem.propTypes = propTypes;
61+
GlobalNavigationMenuItem.defaultProps = defaultProps;
62+
GlobalNavigationMenuItem.displayName = 'GlobalNavigationMenuItem';
63+
64+
export default GlobalNavigationMenuItem;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import React, {useMemo, useContext} from 'react';
2+
import {View} from 'react-native';
3+
import _ from 'underscore';
4+
import styles from '../../../../styles/styles';
5+
import * as Expensicons from '../../../../components/Icon/Expensicons';
6+
import CONST from '../../../../CONST';
7+
import Navigation from '../../../../libs/Navigation/Navigation';
8+
import ROUTES from '../../../../ROUTES';
9+
import useLocalize from '../../../../hooks/useLocalize';
10+
import GlobalNavigationMenuItem from './GlobalNavigationMenuItem';
11+
import {SidebarNavigationContext} from '../SidebarNavigationContext';
12+
import SignInOrAvatarWithOptionalStatus from '../SignInOrAvatarWithOptionalStatus';
13+
14+
function GlobalNavigation() {
15+
const sidebarNavigation = useContext(SidebarNavigationContext);
16+
const {translate} = useLocalize();
17+
const items = useMemo(
18+
() => [
19+
{
20+
icon: Expensicons.ChatBubble,
21+
text: translate('globalNavigationOptions.chats'),
22+
value: CONST.GLOBAL_NAVIGATION_OPTION.CHATS,
23+
onSelected: () => {
24+
Navigation.navigate(ROUTES.REPORT);
25+
},
26+
},
27+
],
28+
[translate],
29+
);
30+
31+
return (
32+
<View style={[styles.ph5, styles.pv3, styles.alignItemsCenter, styles.h100, styles.globalNavigation]}>
33+
<SignInOrAvatarWithOptionalStatus />
34+
<View style={styles.globalNavigationMenuContainer}>
35+
{_.map(items, (item) => (
36+
<GlobalNavigationMenuItem
37+
key={item.value}
38+
icon={item.icon}
39+
title={item.text}
40+
onPress={() => item.onSelected(item.value)}
41+
isFocused={sidebarNavigation.selectedGlobalNavigationOption === item.value}
42+
/>
43+
))}
44+
</View>
45+
</View>
46+
);
47+
}
48+
49+
GlobalNavigation.displayName = 'GlobalNavigation';
50+
51+
export default GlobalNavigation;

0 commit comments

Comments
 (0)