Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix tooltips for bottom tab bar and sidebar list #160

Merged
merged 6 commits into from
Jan 16, 2025
Merged
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
10 changes: 6 additions & 4 deletions src/components/FloatingActionButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {useIsFocused, useNavigationState} from '@react-navigation/native';
import {useNavigationState} from '@react-navigation/native';
import type {ForwardedRef} from 'react';
import React, {forwardRef, useEffect, useRef} from 'react';
// eslint-disable-next-line no-restricted-imports
Expand Down Expand Up @@ -58,23 +58,25 @@ type FloatingActionButtonProps = {

/* An accessibility role for the button */
role: Role;

/* If the tooltip is allowed to be shown */
tooltipAllowed: boolean;
};

function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: FloatingActionButtonProps, ref: ForwardedRef<HTMLDivElement | View | Text>) {
function FloatingActionButton({onPress, isActive, accessibilityLabel, role, tooltipAllowed}: FloatingActionButtonProps, ref: ForwardedRef<HTMLDivElement | View | Text>) {
const {success, buttonDefaultBG, textLight, textDark} = useTheme();
const styles = useThemeStyles();
const borderRadius = styles.floatingActionButton.borderRadius;
const fabPressable = useRef<HTMLDivElement | View | Text | null>(null);
const {shouldUseNarrowLayout} = useResponsiveLayout();
const platform = getPlatform();
const isNarrowScreenOnWeb = shouldUseNarrowLayout && platform === CONST.PLATFORM.WEB;
const isFocused = useIsFocused();
const [isSidebarLoaded] = useOnyx(ONYXKEYS.IS_SIDEBAR_LOADED, {initialValue: false});
const isActiveRouteHome = useNavigationState((state) => state?.routes.some((route) => route.name === SCREENS.HOME));
const {renderProductTrainingTooltip, shouldShowProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext(
CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.GLOBAL_CREATE_TOOLTIP,
// On Home screen, We need to wait for the sidebar to load before showing the tooltip because there is the Concierge tooltip which is higher priority
isFocused && (!isActiveRouteHome || isSidebarLoaded),
tooltipAllowed && (!isActiveRouteHome || isSidebarLoaded),
);
const sharedValue = useSharedValue(isActive ? 1 : 0);
const buttonRef = ref;
Expand Down
12 changes: 7 additions & 5 deletions src/components/LHNOptionsList/OptionRowLHN.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {useFocusEffect, useNavigationState} from '@react-navigation/native';
import {useFocusEffect} from '@react-navigation/native';
import React, {useCallback, useMemo, useRef, useState} from 'react';
import type {GestureResponderEvent, ViewStyle} from 'react-native';
import {StyleSheet, View} from 'react-native';
Expand All @@ -18,6 +18,7 @@ import Tooltip from '@components/Tooltip';
import EducationalTooltip from '@components/Tooltip/EducationalTooltip';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useRootNavigationState from '@hooks/useRootNavigationState';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
Expand All @@ -33,8 +34,8 @@ import FreeTrial from '@pages/settings/Subscription/FreeTrial';
import variables from '@styles/variables';
import Timing from '@userActions/Timing';
import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
import ONYXKEYS from '@src/ONYXKEYS';
import SCREENS from '@src/SCREENS';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type {OptionRowLHNProps} from './types';

Expand All @@ -54,16 +55,17 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti
const shouldShowWokspaceChatTooltip = ReportUtils.isPolicyExpenseChat(report) && activePolicyID === report?.policyID && session?.accountID === report?.ownerAccountID;
const isOnboardingGuideAssigned = introSelected?.choice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM && !session?.email?.includes('+');
const shouldShowGetStartedTooltip = isOnboardingGuideAssigned ? ReportUtils.isAdminRoom(report) : ReportUtils.isConciergeChatReport(report);
const isActiveRouteHome = useNavigationState((state) => state?.routes.some((route) => route.name === SCREENS.HOME));

const isReportsSplitNavigatorLast = useRootNavigationState((state) => state?.routes?.at(-1)?.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR);

const {tooltipToRender, shouldShowTooltip} = useMemo(() => {
const tooltip = shouldShowGetStartedTooltip ? CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.CONCEIRGE_LHN_GBR : CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.LHN_WORKSPACE_CHAT_TOOLTIP;
const shouldShowTooltips = shouldShowWokspaceChatTooltip || shouldShowGetStartedTooltip;
const shouldTooltipBeVisible = shouldUseNarrowLayout ? isScreenFocused && isActiveRouteHome : isActiveRouteHome;
const shouldTooltipBeVisible = shouldUseNarrowLayout ? isScreenFocused && isReportsSplitNavigatorLast : isReportsSplitNavigatorLast;

// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
return {tooltipToRender: tooltip, shouldShowTooltip: shouldShowTooltips && shouldTooltipBeVisible};
}, [shouldShowGetStartedTooltip, shouldShowWokspaceChatTooltip, isScreenFocused, shouldUseNarrowLayout, isActiveRouteHome]);
}, [shouldShowGetStartedTooltip, shouldShowWokspaceChatTooltip, isScreenFocused, shouldUseNarrowLayout, isReportsSplitNavigatorLast]);

const {shouldShowProductTrainingTooltip, renderProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext(tooltipToRender, shouldShowTooltip);

Expand Down
10 changes: 5 additions & 5 deletions src/components/Navigation/BottomTabBar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import {useIsFocused} from '@react-navigation/native';
import React, {memo, useCallback, useEffect, useState} from 'react';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
Expand Down Expand Up @@ -42,6 +41,7 @@ type BottomTabName = ValueOf<typeof BOTTOM_TABS>;

type BottomTabBarProps = {
selectedTab: BottomTabName;
tooltipAllowed?: boolean;
};

/**
Expand Down Expand Up @@ -72,7 +72,7 @@ function handleQueryWithPolicyID(query: SearchQueryString, activePolicyID?: stri
return SearchQueryUtils.buildSearchQueryString(queryJSON);
}

function BottomTabBar({selectedTab}: BottomTabBarProps) {
function BottomTabBar({selectedTab, tooltipAllowed = false}: BottomTabBarProps) {
const theme = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();
Expand All @@ -89,12 +89,12 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) {
const [chatTabBrickRoad, setChatTabBrickRoad] = useState<BrickRoad>(() =>
getChatTabBrickRoad(activeWorkspaceID, currentReportID, reports, betas, policies, priorityMode, transactionViolations),
);
const isFocused = useIsFocused();

const platform = getPlatform();
const isWebOrDesktop = platform === CONST.PLATFORM.WEB || platform === CONST.PLATFORM.DESKTOP;
const {renderProductTrainingTooltip, shouldShowProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext(
CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.BOTTOM_NAV_INBOX_TOOLTIP,
selectedTab !== BOTTOM_TABS.HOME && isFocused,
tooltipAllowed && selectedTab !== BOTTOM_TABS.HOME,
);
useEffect(() => {
setChatTabBrickRoad(getChatTabBrickRoad(activeWorkspaceID, currentReportID, reports, betas, policies, priorityMode, transactionViolations));
Expand Down Expand Up @@ -290,7 +290,7 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) {
onPress={showSettingsPage}
/>
<View style={[styles.flex1, styles.bottomTabBarItem]}>
<BottomTabBarFloatingActionButton />
<BottomTabBarFloatingActionButton tooltipAllowed={tooltipAllowed} />
</View>
</View>
</>
Expand Down
78 changes: 78 additions & 0 deletions src/components/Navigation/TopLevelBottomTabBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {findFocusedRoute, useNavigationState} from '@react-navigation/native';
import React, {useEffect, useRef, useState} from 'react';
import {InteractionManager, View} from 'react-native';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets';
import useThemeStyles from '@hooks/useThemeStyles';
import {isFullScreenName, isSplitNavigatorName} from '@libs/Navigation/helpers/isNavigatorName';
import {FULLSCREEN_TO_TAB, SIDEBAR_TO_SPLIT} from '@libs/Navigation/linkingConfig/RELATIONS';
import type {FullScreenName} from '@libs/Navigation/types';
import NAVIGATORS from '@src/NAVIGATORS';
import SCREENS from '@src/SCREENS';
import BottomTabBar from './BottomTabBar';

const SCREENS_WITH_BOTTOM_TAB_BAR = [...Object.keys(SIDEBAR_TO_SPLIT), SCREENS.SEARCH.ROOT, SCREENS.SETTINGS.WORKSPACES];

/**
* TopLevelBottomTabBar is displayed when the user can interact with the bottom tab bar.
* We hide it when:
* 1. The bottom tab bar is not visible.
* 2. There is transition between screens with and without the bottom tab bar.
* 3. The bottom tab bar is under the overlay.
* For cases 2 and 3, local bottom tab bar mounted on the screen will be displayed.
*/

function TopLevelBottomTabBar() {
const styles = useThemeStyles();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const {paddingBottom} = useStyledSafeAreaInsets();
const [isAfterClosingTransition, setIsAfterClosingTransition] = useState(false);
const cancelAfterInteractions = useRef<ReturnType<typeof InteractionManager.runAfterInteractions> | undefined>();

const selectedTab = useNavigationState((state) => {
const topmostFullScreenRoute = state?.routes.findLast((route) => isFullScreenName(route.name));
return FULLSCREEN_TO_TAB[(topmostFullScreenRoute?.name as FullScreenName) ?? NAVIGATORS.REPORTS_SPLIT_NAVIGATOR];
});

// There always should be a focused screen.
const isScreenWithBottomTabFocused = useNavigationState((state) => {
const focusedRoute = findFocusedRoute(state);

// We are checking if the focused route is a split navigator because there may be a brief moment where the navigator don't have state yet.
// That mens we don't have screen with bottom tab focused. This caused glitching.
return SCREENS_WITH_BOTTOM_TAB_BAR.includes(focusedRoute?.name ?? '') || isSplitNavigatorName(focusedRoute?.name);
});

// Visible directly means not through the overlay.
const isScreenWithBottomTabVisibleDirectly = useNavigationState((state) => isFullScreenName(state.routes.at(-1)?.name));

const shouldDisplayTopLevelBottomTabBar = shouldUseNarrowLayout ? isScreenWithBottomTabFocused : isScreenWithBottomTabVisibleDirectly;

useEffect(() => {
cancelAfterInteractions.current?.cancel();

if (!shouldDisplayTopLevelBottomTabBar) {
// If the bottom tab is not visible, that means there is a screen covering it.
// In that case we need to set the flag to true because there will be a transition for which we need to wait.
setIsAfterClosingTransition(false);
} else {
// If the bottom tab should be visible, we want to wait for transition to finish.
cancelAfterInteractions.current = InteractionManager.runAfterInteractions(() => {
setIsAfterClosingTransition(true);
});
}
}, [shouldDisplayTopLevelBottomTabBar]);

return (
<View style={styles.topLevelBottomTabBar(shouldDisplayTopLevelBottomTabBar && isAfterClosingTransition, shouldUseNarrowLayout, paddingBottom)}>
{/* We are not rendering BottomTabBar conditionally for two reasons
1. It's faster to hide/show it than mount a new when needed.
2. We need to hide tooltips as well if they were displayed. */}
<BottomTabBar
selectedTab={selectedTab}
tooltipAllowed={shouldDisplayTopLevelBottomTabBar}
/>
</View>
);
}
export default TopLevelBottomTabBar;
33 changes: 33 additions & 0 deletions src/hooks/useRootNavigationState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type {NavigationState} from '@react-navigation/routers';
import {useEffect, useRef, useState} from 'react';
import navigationRef from '@libs/Navigation/navigationRef';

type Selector<T> = (state: NavigationState) => T;

/**
* Hook to get a value from the current root navigation state using a selector.
*
* @param selector Selector function to get a value from the state.
*/
function useRootNavigationState<T>(selector: Selector<T>): T {
const [result, setResult] = useState(() => selector(navigationRef.getRootState()));

// We store the selector in a ref to avoid re-subscribing listeners every render
const selectorRef = useRef(selector);

useEffect(() => {
selectorRef.current = selector;
});

useEffect(() => {
const unsubscribe = navigationRef.addListener('state', (e) => {
setResult(selectorRef.current(e.data.state as NavigationState));
});

return unsubscribe;
}, []);

return result;
}

export default useRootNavigationState;

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {createNavigatorFactory} from '@react-navigation/native';
import type {ParamListBase} from '@react-navigation/native';
import TopLevelBottomTabBar from '@components/Navigation/TopLevelBottomTabBar';
import useNavigationResetOnLayoutChange from '@libs/Navigation/AppNavigator/useNavigationResetOnLayoutChange';
import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName';
import createPlatformStackNavigatorComponent from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent';
import defaultPlatformStackScreenOptions from '@libs/Navigation/PlatformStackNavigation/defaultPlatformStackScreenOptions';
import type {CustomStateHookProps, PlatformStackNavigationEventMap, PlatformStackNavigationOptions, PlatformStackNavigationState} from '@libs/Navigation/PlatformStackNavigation/types';
import RootStackRouter from './RootStackRouter';
import TopLevelBottomTabBar from './TopLevelBottomTabBar';

// This is an optimization to keep mounted only last few screens in the stack.
function useCustomRootStackNavigatorState({state}: CustomStateHookProps) {
Expand Down
9 changes: 7 additions & 2 deletions src/libs/Navigation/helpers/isNavigatorName.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {SIDEBAR_TO_SPLIT, SPLIT_TO_SIDEBAR} from '@libs/Navigation/linkingConfig/RELATIONS';
import type {FullScreenName, OnboardingFlowName, SplitNavigatorSidebarScreen} from '@libs/Navigation/types';
import type {FullScreenName, OnboardingFlowName, SplitNavigatorName, SplitNavigatorSidebarScreen} from '@libs/Navigation/types';
import SCREENS from '@src/SCREENS';

const ONBOARDING_SCREENS = [
Expand All @@ -15,6 +15,7 @@ const ONBOARDING_SCREENS = [
const FULL_SCREENS_SET = new Set([...Object.values(SIDEBAR_TO_SPLIT), SCREENS.SEARCH.ROOT]);
const SIDEBARS_SET = new Set(Object.values(SPLIT_TO_SIDEBAR));
const ONBOARDING_SCREENS_SET = new Set(ONBOARDING_SCREENS);
const SPLIT_NAVIGATORS_SET = new Set(Object.values(SIDEBAR_TO_SPLIT));

/**
* Functions defined below are used to check whether a screen belongs to a specific group.
Expand All @@ -32,6 +33,10 @@ function isOnboardingFlowName(screen: string | undefined) {
return checkIfScreenHasMatchingNameToSetValues<OnboardingFlowName>(screen, ONBOARDING_SCREENS_SET);
}

function isSplitNavigatorName(screen: string | undefined) {
return checkIfScreenHasMatchingNameToSetValues<SplitNavigatorName>(screen, SPLIT_NAVIGATORS_SET);
}

function isFullScreenName(screen: string | undefined) {
return checkIfScreenHasMatchingNameToSetValues<FullScreenName>(screen, FULL_SCREENS_SET);
}
Expand All @@ -40,4 +45,4 @@ function isSidebarScreenName(screen: string | undefined) {
return checkIfScreenHasMatchingNameToSetValues<SplitNavigatorSidebarScreen>(screen, SIDEBARS_SET);
}

export {isFullScreenName, isOnboardingFlowName, isSidebarScreenName};
export {isFullScreenName, isOnboardingFlowName, isSidebarScreenName, isSplitNavigatorName};
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import React, {useCallback, useRef} from 'react';
import FloatingActionButtonAndPopover from '@pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover';
import type FloatingActionButtonPopoverMenuRef from './types';

function BottomTabBarFloatingActionButton() {
type BottomTabBarFloatingActionButtonProps = {
tooltipAllowed: boolean;
};

function BottomTabBarFloatingActionButton({tooltipAllowed}: BottomTabBarFloatingActionButtonProps) {
const popoverModal = useRef<FloatingActionButtonPopoverMenuRef>(null);

/**
Expand Down Expand Up @@ -32,6 +36,7 @@ function BottomTabBarFloatingActionButton() {
return (
<FloatingActionButtonAndPopover
ref={popoverModal}
tooltipAllowed={tooltipAllowed}
onShowCreateMenu={createDragoverListener}
onHideCreateMenu={removeDragoverListener}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ type FloatingActionButtonAndPopoverProps = {

/* Callback function before the menu is hidden */
onHideCreateMenu?: () => void;

/* If the tooltip is allowed to be shown */
tooltipAllowed: boolean;
};

type FloatingActionButtonAndPopoverRef = {
Expand Down Expand Up @@ -154,7 +157,7 @@ const getQuickActionTitle = (action: QuickActionName): TranslationPaths => {
* Responsible for rendering the {@link PopoverMenu}, and the accompanying
* FAB that can open or close the menu.
*/
function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: FloatingActionButtonAndPopoverProps, ref: ForwardedRef<FloatingActionButtonAndPopoverRef>) {
function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, tooltipAllowed}: FloatingActionButtonAndPopoverProps, ref: ForwardedRef<FloatingActionButtonAndPopoverRef>) {
const styles = useThemeStyles();
const theme = useTheme();
const {translate} = useLocalize();
Expand Down Expand Up @@ -533,6 +536,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: Fl
cancelText={translate('common.cancel')}
/>
<FloatingActionButton
tooltipAllowed={tooltipAllowed}
accessibilityLabel={translate('sidebarScreen.fabNewChatExplained')}
role={CONST.ROLE.BUTTON}
isActive={isCreateMenuActive}
Expand Down
Loading
Loading