diff --git a/__mocks__/@react-navigation/native.ts b/__mocks__/@react-navigation/native.ts index 023a3a2cd76..fbea3ccf875 100644 --- a/__mocks__/@react-navigation/native.ts +++ b/__mocks__/@react-navigation/native.ts @@ -40,14 +40,14 @@ export const navigation = { setParams, useLinkProps, } -export const useNavigation = () => navigation +export const useNavigation = jest.fn().mockReturnValue(navigation) export const createNavigationContainerRef = () => ({ ...navigation, isReady: () => true, current: navigation, }) -export const useIsFocused = jest.fn() +export const useIsFocused = jest.fn().mockReturnValue(true) export const useRoute = jest.fn().mockReturnValue({ params: {} }) export const useFocusEffect = useEffect export const NavigationContainer = jest.fn() diff --git a/src/features/bookings/components/EndedBookingItem.native.test.tsx b/src/features/bookings/components/EndedBookingItem.native.test.tsx index a6d3da1c687..220451fb33f 100644 --- a/src/features/bookings/components/EndedBookingItem.native.test.tsx +++ b/src/features/bookings/components/EndedBookingItem.native.test.tsx @@ -1,3 +1,4 @@ +import * as reactNavigationNative from '@react-navigation/native' import React, { ComponentProps } from 'react' import { Share } from 'react-native' @@ -23,10 +24,10 @@ jest.mock('libs/firebase/remoteConfig/remoteConfig.services', () => ({ jest.mock('libs/network/NetInfoWrapper') const mockNavigate = jest.fn() -jest.mock('@react-navigation/native', () => ({ - ...jest.requireActual('@react-navigation/native'), - useNavigation: () => ({ navigate: mockNavigate, push: jest.fn() }), -})) +jest.spyOn(reactNavigationNative, 'useNavigation').mockReturnValue({ + navigate: mockNavigate, + push: jest.fn(), +}) jest.mock('libs/subcategories/useSubcategory') diff --git a/src/features/navigation/TabBar/TabBar.tsx b/src/features/navigation/TabBar/TabBar.tsx index 0290b6e1617..62f6e842d7c 100644 --- a/src/features/navigation/TabBar/TabBar.tsx +++ b/src/features/navigation/TabBar/TabBar.tsx @@ -75,7 +75,6 @@ export const TabBar: React.FC = ({ navigation, state }) => { return ( const netInfo = useNetInfoContext() return ( - - - {children} - - + {children} ) @@ -23,7 +19,8 @@ export const TabBarContainer = ({ children }: { children: React.ReactNode }) => const RowContainer = styled.View({ flexDirection: 'row', - width: '100%', + justifyContent: 'space-evenly', + paddingHorizontal: getSpacing(4), }) const SafeAreaPlaceholder = styled.View<{ safeHeight: number }>(({ safeHeight }) => ({ @@ -31,12 +28,11 @@ const SafeAreaPlaceholder = styled.View<{ safeHeight: number }>(({ safeHeight }) })) const MainContainer = styled.View(({ theme }) => ({ - alignItems: 'center', borderTopStyle: 'solid', borderTopWidth: getSpacing(1 / 4), borderTopColor: theme.colors.greyMedium, - backgroundColor: theme.uniqueColors.tabBar, width: '100%', + backgroundColor: theme.uniqueColors.tabBar, position: 'absolute', bottom: 0, ...getShadow({ diff --git a/src/features/offer/components/OfferContent/ChronicleSection/ChronicleSection.native.test.tsx b/src/features/offer/components/OfferContent/ChronicleSection/ChronicleSection.native.test.tsx index 483c6be7a63..c0aeb4c1fdc 100644 --- a/src/features/offer/components/OfferContent/ChronicleSection/ChronicleSection.native.test.tsx +++ b/src/features/offer/components/OfferContent/ChronicleSection/ChronicleSection.native.test.tsx @@ -1,3 +1,4 @@ +import * as reactNavigationNative from '@react-navigation/native' import React from 'react' import { chroniclesSnap } from 'features/chronicle/fixtures/chroniclesSnap' @@ -6,10 +7,10 @@ import { render, screen, userEvent } from 'tests/utils' import { ChronicleSection } from './ChronicleSection' const mockNavigate = jest.fn() -jest.mock('@react-navigation/native', () => ({ - ...jest.requireActual('@react-navigation/native'), - useNavigation: () => ({ navigate: mockNavigate, push: jest.fn() }), -})) +jest.spyOn(reactNavigationNative, 'useNavigation').mockReturnValue({ + navigate: mockNavigate, + push: jest.fn(), +}) describe('ChroniclesSection', () => { const user = userEvent.setup() diff --git a/src/features/offer/components/OfferContent/ChronicleSection/ChronicleSection.web.test.tsx b/src/features/offer/components/OfferContent/ChronicleSection/ChronicleSection.web.test.tsx index 8f38bfbd52b..afd2ddd4062 100644 --- a/src/features/offer/components/OfferContent/ChronicleSection/ChronicleSection.web.test.tsx +++ b/src/features/offer/components/OfferContent/ChronicleSection/ChronicleSection.web.test.tsx @@ -1,3 +1,4 @@ +import * as reactNavigationNative from '@react-navigation/native' import React from 'react' import { chroniclesSnap } from 'features/chronicle/fixtures/chroniclesSnap' @@ -6,12 +7,12 @@ import { fireEvent, render, screen } from 'tests/utils/web' import { ChronicleSection } from './ChronicleSection' const mockNavigate = jest.fn() -jest.mock('@react-navigation/native', () => ({ - ...jest.requireActual('@react-navigation/native'), - useNavigation: () => ({ navigate: mockNavigate, push: jest.fn() }), -})) +jest.spyOn(reactNavigationNative, 'useNavigation').mockReturnValue({ + navigate: mockNavigate, + push: jest.fn(), +}) -describe('ChroniclesSection', () => { +describe('ChronicleSection', () => { it('should render correctly in mobile', () => { render( ', () => { { state: State.END, absoluteY: -30 }, ]) - expect(navigate).toHaveBeenCalledWith('Venue', { id: venuesFixture[0].venueId }) + expect(mockNavigate).toHaveBeenCalledWith('Venue', { id: venuesFixture[0].venueId }) }) it('should deactivate navigation to Venue page when bottom sheet is open, pressing venue button, wipIsOpenToPublic feature flag is true and venue is not open to public', async () => { diff --git a/src/ui/components/touchableLink/InternalTouchableLink.native.test.tsx b/src/ui/components/touchableLink/InternalTouchableLink.native.test.tsx index ad7476a37bc..df7403f7a8e 100644 --- a/src/ui/components/touchableLink/InternalTouchableLink.native.test.tsx +++ b/src/ui/components/touchableLink/InternalTouchableLink.native.test.tsx @@ -1,13 +1,20 @@ +import * as reactNavigationNative from '@react-navigation/native' import React from 'react' import { Text } from 'react-native' -import { navigate, push } from '__mocks__/@react-navigation/native' import { navigateFromRef, pushFromRef } from 'features/navigation/navigationRef' import { render, screen, userEvent } from 'tests/utils' import { InternalTouchableLink } from 'ui/components/touchableLink/InternalTouchableLink' jest.mock('features/navigation/navigationRef') +const mockPush = jest.fn() +const mockNavigate = jest.fn() +jest.spyOn(reactNavigationNative, 'useNavigation').mockReturnValue({ + navigate: mockNavigate, + push: mockPush, +}) + const navigateToItineraryMock = jest.fn() const useItinerary = () => ({ navigateTo: navigateToItineraryMock, @@ -39,7 +46,7 @@ describe('', () => { await user.press(screen.getByText(linkText)) - expect(navigate).toHaveBeenCalledWith('TabNavigator', { screen: 'Home', params: undefined }) + expect(mockNavigate).toHaveBeenCalledWith('TabNavigator', { screen: 'Home', params: undefined }) }) it('should push right screen with expected params if withPush={true}', async () => { @@ -52,13 +59,13 @@ describe('', () => { await user.press(screen.getByText(linkText)) - expect(push).toHaveBeenCalledWith('TabNavigator', { + expect(mockPush).toHaveBeenCalledWith('TabNavigator', { screen: 'Home', params: undefined, }) }) - it('should push screen only once in case of press spamming when withPush={true}', async () => { + it('should push screen only once in case of press spamming', async () => { render( @@ -70,7 +77,7 @@ describe('', () => { await user.press(screen.getByText(linkText)) await user.press(screen.getByText(linkText)) - expect(push).toHaveBeenNthCalledWith(1, 'TabNavigator', { + expect(mockPush).toHaveBeenNthCalledWith(1, 'TabNavigator', { screen: 'Home', params: undefined, }) @@ -129,7 +136,7 @@ describe('', () => { await user.press(screen.getByText(linkText)) - expect(navigate).not.toHaveBeenCalledWith('TabNavigator', { + expect(mockNavigate).not.toHaveBeenCalledWith('TabNavigator', { screen: 'Home', params: undefined, }) diff --git a/src/ui/components/touchableLink/InternalTouchableLink.tsx b/src/ui/components/touchableLink/InternalTouchableLink.tsx index 5e7c6934a1b..b24ff42addb 100644 --- a/src/ui/components/touchableLink/InternalTouchableLink.tsx +++ b/src/ui/components/touchableLink/InternalTouchableLink.tsx @@ -1,4 +1,4 @@ -import { useLinkProps, useNavigation } from '@react-navigation/native' +import { useIsFocused, useLinkProps, useNavigation } from '@react-navigation/native' import React, { FunctionComponent, useCallback } from 'react' import { pushFromRef, navigateFromRef, resetFromRef } from 'features/navigation/navigationRef' @@ -6,38 +6,38 @@ import { UseNavigationType } from 'features/navigation/RootNavigator/types' import { TouchableLink } from 'ui/components/touchableLink/TouchableLink' import { InternalTouchableLinkProps } from 'ui/components/touchableLink/types' -const PUSH_LINK_COOLDOWN = 1500 - export const InternalTouchableLink: FunctionComponent = ({ navigateTo, enableNavigate = true, + onBeforeNavigate, + onAfterNavigate, ...rest }) => { // We use nullish operator here because TabBar uses InternalTouchableLink but navigateTo is undefined during launch const internalLinkProps = useLinkProps({ to: navigateTo ?? '' }) + const isFocused = useIsFocused() const { navigate, push, reset } = useNavigation() const { screen, params, fromRef, withPush, withReset } = navigateTo + // To avoid double tap issue, we define navigation functions only when component is focused const handleNavigation = useCallback(() => { - if (enableNavigate) { - if (withReset) { - fromRef - ? resetFromRef(screen, params) - : reset({ index: 0, routes: [{ name: screen, params }] }) - } else if (withPush) { - fromRef ? pushFromRef(screen, params) : push(screen, params) - } else { - fromRef ? navigateFromRef(screen, params) : navigate(screen, params) - } + if (withReset) { + fromRef + ? resetFromRef(screen, params) + : reset({ index: 0, routes: [{ name: screen, params }] }) + } else if (withPush) { + fromRef ? pushFromRef(screen, params) : push(screen, params) + } else { + fromRef ? navigateFromRef(screen, params) : navigate(screen, params) } - }, [enableNavigate, navigate, push, reset, fromRef, withPush, withReset, params, screen]) - // For link in push mode, we want to avoid double tap. So we put a pretty large cooldown wait value + }, [navigate, push, reset, fromRef, withPush, withReset, params, screen]) return ( ) } diff --git a/src/ui/components/touchableLink/TouchableLink.native.test.tsx b/src/ui/components/touchableLink/TouchableLink.native.test.tsx index 5fcdf75d599..4b83a9409ba 100644 --- a/src/ui/components/touchableLink/TouchableLink.native.test.tsx +++ b/src/ui/components/touchableLink/TouchableLink.native.test.tsx @@ -2,7 +2,7 @@ import React from 'react' import { Text } from 'react-native' import { analytics } from 'libs/analytics/provider' -import { act, render, screen, userEvent } from 'tests/utils' +import { render, screen, userEvent } from 'tests/utils' import { ButtonPrimary } from 'ui/components/buttons/ButtonPrimary' import { TouchableLink } from './TouchableLink' @@ -73,57 +73,6 @@ describe('', () => { expect(link.props.style).toEqual(expectedStyle) }) - it('should trigger handleNavigation only once in case of press spamming with correct cooldown delay', async () => { - render( - - - - ) - - await user.press(screen.getByText(linkText)) - await user.press(screen.getByText(linkText)) - await user.press(screen.getByText(linkText)) - - expect(handleNavigationMock).toHaveBeenCalledTimes(1) - }) - - it('should trigger handleNavigation multiple time if cooldown delay is respected', async () => { - render( - - - - ) - - await act(async () => { - await user.press(screen.getByText(linkText)) - - jest.advanceTimersByTime(300) - - await user.press(screen.getByText(linkText)) - - jest.advanceTimersByTime(300) - - await user.press(screen.getByText(linkText)) - }) - - expect(handleNavigationMock).toHaveBeenCalledTimes(3) - }) - - it('should trigger handleNavigation with multiple press when no cooldown delay is set', async () => { - jest.useFakeTimers() - render( - - - - ) - - await user.press(screen.getByText(linkText)) - await user.press(screen.getByText(linkText)) - await user.press(screen.getByText(linkText)) - - expect(handleNavigationMock).toHaveBeenCalledTimes(3) - }) - it('should not trigger handleNavigation when Link is disabled', async () => { render( diff --git a/src/ui/components/touchableLink/TouchableLink.tsx b/src/ui/components/touchableLink/TouchableLink.tsx index 67f020ed3f5..1d60da393f2 100644 --- a/src/ui/components/touchableLink/TouchableLink.tsx +++ b/src/ui/components/touchableLink/TouchableLink.tsx @@ -1,4 +1,3 @@ -import { throttle } from 'lodash' import React, { createRef, ElementType, useCallback, useEffect, useMemo } from 'react' import { NativeSyntheticEvent, Platform, TargetedEvent } from 'react-native' import styled from 'styled-components/native' @@ -28,7 +27,6 @@ export function TouchableLink({ hoverUnderlineColor, accessibilityLabel, testID, - pressCooldownDelay = 0, ...rest }: TouchableLinkProps) { const TouchableComponent = ( @@ -73,7 +71,7 @@ export function TouchableLink({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []) - const pressFn = useMemo( + const handlePress = useMemo( () => handleNavigationWrapper({ onBeforeNavigate, @@ -83,11 +81,6 @@ export function TouchableLink({ [onBeforeNavigate, onAfterNavigate, handleNavigation] ) - const throttledPressFn = useMemo( - () => throttle(pressFn, pressCooldownDelay, { leading: true, trailing: false }), - [pressCooldownDelay, pressFn] - ) - return ( {children} diff --git a/src/ui/components/touchableLink/handleNavigationWrapper.ts b/src/ui/components/touchableLink/handleNavigationWrapper.ts index 959ff9efcd7..805f74196fe 100644 --- a/src/ui/components/touchableLink/handleNavigationWrapper.ts +++ b/src/ui/components/touchableLink/handleNavigationWrapper.ts @@ -9,7 +9,7 @@ export const handleNavigationWrapper = ({ }: HandleNavigationWrapperProps) => { const onClick = async (event: GestureResponderEvent) => { if (onBeforeNavigate) await onBeforeNavigate(event) - handleNavigation() + handleNavigation?.() if (onAfterNavigate) await onAfterNavigate(event) } diff --git a/src/ui/components/touchableLink/handleNavigationWrapper.web.ts b/src/ui/components/touchableLink/handleNavigationWrapper.web.ts index f8eac491bf1..53207540885 100644 --- a/src/ui/components/touchableLink/handleNavigationWrapper.web.ts +++ b/src/ui/components/touchableLink/handleNavigationWrapper.web.ts @@ -11,7 +11,7 @@ export const handleNavigationWrapper = ({ event.preventDefault() if (onBeforeNavigate) await onBeforeNavigate(event) - handleNavigation() + handleNavigation?.() if (onAfterNavigate) await onAfterNavigate(event) } diff --git a/src/ui/components/touchableLink/types.ts b/src/ui/components/touchableLink/types.ts index 4ce0eddcfc1..5521d0eacd6 100644 --- a/src/ui/components/touchableLink/types.ts +++ b/src/ui/components/touchableLink/types.ts @@ -38,11 +38,10 @@ type AsProps = { } & Record type TouchableLinkGenericProps = { - onBeforeNavigate?: (event: GestureResponderEvent | MouseEvent) => void - onAfterNavigate?: (event: GestureResponderEvent | MouseEvent) => void + onBeforeNavigate?: (event: GestureResponderEvent | MouseEvent) => void | Promise + onAfterNavigate?: (event: GestureResponderEvent | MouseEvent) => void | Promise highlight?: boolean // If true, uses TouchableHighlight instead of TouchableOpacity to render component hoverUnderlineColor?: ColorsEnum // Color to be used for underline effect on hover. Black if not specified - pressCooldownDelay?: number } & Omit & AsProps @@ -51,7 +50,7 @@ export type ExternalTouchableLinkProps = TouchableLinkGenericProps & ExternalNav export type InternalTouchableLinkProps = TouchableLinkGenericProps & InternalNavigationProps export type TouchableLinkProps = TouchableLinkGenericProps & { - handleNavigation: () => void + handleNavigation?: () => void linkProps?: | { href: string