Skip to content

Commit

Permalink
Merge pull request #160 from software-mansion-labs/poc/split-tooltips
Browse files Browse the repository at this point in the history
Fix tooltips for bottom tab bar and sidebar list
  • Loading branch information
adamgrzybowski authored Jan 16, 2025
2 parents 114b059 + 6d1010f commit 2b5299a
Show file tree
Hide file tree
Showing 12 changed files with 154 additions and 63 deletions.
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 @@ -56,6 +56,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 @@ -155,7 +158,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 @@ -537,6 +540,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

0 comments on commit 2b5299a

Please sign in to comment.