Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {buildSubstitutionsMap} from '@components/Search/SearchRouter/buildSubsti
import type {SubstitutionMap} from '@components/Search/SearchRouter/getQueryWithSubstitutions';
import {getQueryWithSubstitutions} from '@components/Search/SearchRouter/getQueryWithSubstitutions';
import {getUpdatedSubstitutionsMap} from '@components/Search/SearchRouter/getUpdatedSubstitutionsMap';
import {useSearchRouterContext} from '@components/Search/SearchRouter/SearchRouterContext';
import {useSearchRouterActions} from '@components/Search/SearchRouter/SearchRouterContext';
import type {SearchQueryJSON, SearchQueryString} from '@components/Search/types';
import type {SearchQueryItem} from '@components/SelectionListWithSections/Search/SearchQueryListItem';
import {isSearchQueryItem} from '@components/SelectionListWithSections/Search/SearchQueryListItem';
Expand Down Expand Up @@ -84,7 +84,7 @@ function SearchPageHeaderInput({queryJSON, searchRouterListVisible, hideSearchRo
const textInputRef = useRef<AnimatedTextInputRef>(null);
const hasMountedRef = useRef(false);
const isFocused = useIsFocused();
const {registerSearchPageInput} = useSearchRouterContext();
const {registerSearchPageInput} = useSearchRouterActions();

useEffect(() => {
hasMountedRef.current = true;
Expand Down
32 changes: 18 additions & 14 deletions src/components/Search/SearchRouter/SearchButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {startSpan} from '@libs/telemetry/activeSpans';
import {callFunctionIfActionIsAllowed} from '@userActions/Session';
import Timing from '@userActions/Timing';
import CONST from '@src/CONST';
import {useSearchRouterContext} from './SearchRouterContext';
import {useSearchRouterActions} from './SearchRouterContext';

type SearchButtonProps = {
style?: StyleProp<ViewStyle>;
Expand All @@ -23,10 +23,25 @@ function SearchButton({style, shouldUseAutoHitSlop = false}: SearchButtonProps)
const styles = useThemeStyles();
const theme = useTheme();
const {translate} = useLocalize();
const {openSearchRouter} = useSearchRouterContext();
const {openSearchRouter} = useSearchRouterActions();
const pressableRef = useRef<View>(null);
const expensifyIcons = useMemoizedLazyExpensifyIcons(['MagnifyingGlass']);

const onPress = () => {
callFunctionIfActionIsAllowed(() => {
pressableRef.current?.blur();

Timing.start(CONST.TIMING.OPEN_SEARCH);
Performance.markStart(CONST.TIMING.OPEN_SEARCH);
startSpan(CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER, {
name: CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER,
op: CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER,
});

openSearchRouter();
})();
};

return (
<Tooltip text={translate('common.search')}>
<PressableWithoutFeedback
Expand All @@ -36,18 +51,7 @@ function SearchButton({style, shouldUseAutoHitSlop = false}: SearchButtonProps)
style={[styles.flexRow, styles.touchableButtonImage, style]}
shouldUseAutoHitSlop={shouldUseAutoHitSlop}
sentryLabel={CONST.SENTRY_LABEL.SEARCH.SEARCH_BUTTON}
onPress={callFunctionIfActionIsAllowed(() => {
pressableRef?.current?.blur();

Timing.start(CONST.TIMING.OPEN_SEARCH);
Performance.markStart(CONST.TIMING.OPEN_SEARCH);
startSpan(CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER, {
name: CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER,
op: CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER,
});

openSearchRouter();
})}
onPress={onPress}
>
<Icon
src={expensifyIcons.MagnifyingGlass}
Expand Down
175 changes: 95 additions & 80 deletions src/components/Search/SearchRouter/SearchRouterContext.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useContext, useEffect, useMemo, useRef, useState} from 'react';
import React, {useContext, useEffect, useRef, useState} from 'react';
import type {AnimatedTextInputRef} from '@components/RNTextInput';
import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute';
import {navigationRef} from '@libs/Navigation/Navigation';
Expand All @@ -9,8 +9,11 @@ import NAVIGATORS from '@src/NAVIGATORS';
import SCREENS from '@src/SCREENS';
import type ChildrenProps from '@src/types/utils/ChildrenProps';

type SearchRouterContext = {
type SearchRouterStateContextType = {
isSearchRouterDisplayed: boolean;
};

type SearchRouterActionsContextType = {
openSearchRouter: () => void;
closeSearchRouter: () => void;
toggleSearch: () => void;
Expand All @@ -22,16 +25,17 @@ type HistoryState = {
isSearchModalOpen?: boolean;
};

const defaultSearchContext: SearchRouterContext = {
isSearchRouterDisplayed: false,
const defaultSearchRouterActionsContext: SearchRouterActionsContextType = {
openSearchRouter: () => {},
closeSearchRouter: () => {},
toggleSearch: () => {},
registerSearchPageInput: () => {},
unregisterSearchPageInput: () => {},
};

const Context = React.createContext<SearchRouterContext>(defaultSearchContext);
const SearchRouterStateContext = React.createContext<SearchRouterStateContextType>({isSearchRouterDisplayed: false});

const SearchRouterActionsContext = React.createContext<SearchRouterActionsContextType>(defaultSearchRouterActionsContext);

const isBrowserWithHistory = typeof window !== 'undefined' && typeof window.history !== 'undefined';
const canListenPopState = typeof window !== 'undefined' && typeof window.addEventListener === 'function';
Expand Down Expand Up @@ -69,87 +73,98 @@ function SearchRouterContextProvider({children}: ChildrenProps) {
return () => window.removeEventListener('popstate', handlePopState);
}, []);

const routerContext = useMemo(() => {
const openSearchRouter = () => {
if (isBrowserWithHistory) {
window.history.pushState({isSearchModalOpen: true} satisfies HistoryState, '');
}
close(
() => {
setIsSearchRouterDisplayed(true);
searchRouterDisplayedRef.current = true;
},
false,
true,
);
};
const closeSearchRouter = () => {
setIsSearchRouterDisplayed(false);
searchRouterDisplayedRef.current = false;
if (isBrowserWithHistory) {
const state = window.history.state as HistoryState | null;
if (state?.isSearchModalOpen) {
window.history.replaceState({isSearchModalOpen: false} satisfies HistoryState, '');
}
const openSearchRouter = () => {
if (isBrowserWithHistory) {
window.history.pushState({isSearchModalOpen: true} satisfies HistoryState, '');
}
close(
() => {
setIsSearchRouterDisplayed(true);
searchRouterDisplayedRef.current = true;
},
false,
true,
);
};
const closeSearchRouter = () => {
setIsSearchRouterDisplayed(false);
searchRouterDisplayedRef.current = false;
if (isBrowserWithHistory) {
const state = window.history.state as HistoryState | null;
if (state?.isSearchModalOpen) {
window.history.replaceState({isSearchModalOpen: false} satisfies HistoryState, '');
}
};

const startSearchRouterOpenSpan = () => {
startSpan(CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER, {
name: CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER,
op: CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER,
attributes: {
trigger: 'keyboard',
},
});
};

// There are callbacks that live outside of React render-loop and interact with SearchRouter
// So we need a function that is based on ref to correctly open/close it
// When user is on `/search` page we focus the Input instead of showing router
const toggleSearch = () => {
const searchFullScreenRoutes = navigationRef.getRootState()?.routes.findLast((route) => route.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR);
const lastRoute = searchFullScreenRoutes?.state?.routes?.at(-1);
const isUserOnSearchPage = isSearchTopmostFullScreenRoute() && lastRoute?.name === SCREENS.SEARCH.ROOT;

if (isUserOnSearchPage && searchPageInputRef.current) {
if (searchPageInputRef.current.isFocused()) {
searchPageInputRef.current.blur();
} else {
startSearchRouterOpenSpan();
searchPageInputRef.current.focus();
}
} else if (searchRouterDisplayedRef.current) {
closeSearchRouter();
}
};

const startSearchRouterOpenSpan = () => {
startSpan(CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER, {
name: CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER,
op: CONST.TELEMETRY.SPAN_OPEN_SEARCH_ROUTER,
attributes: {
trigger: 'keyboard',
},
});
};

// There are callbacks that live outside of React render-loop and interact with SearchRouter
// So we need a function that is based on ref to correctly open/close it
// When user is on `/search` page we focus the Input instead of showing router
const toggleSearch = () => {
const searchFullScreenRoutes = navigationRef.getRootState()?.routes.findLast((route) => route.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR);
const lastRoute = searchFullScreenRoutes?.state?.routes?.at(-1);
const isUserOnSearchPage = isSearchTopmostFullScreenRoute() && lastRoute?.name === SCREENS.SEARCH.ROOT;

if (isUserOnSearchPage && searchPageInputRef.current) {
if (searchPageInputRef.current.isFocused()) {
searchPageInputRef.current.blur();
} else {
startSearchRouterOpenSpan();
openSearchRouter();
searchPageInputRef.current.focus();
}
};

const registerSearchPageInput = (ref: AnimatedTextInputRef) => {
searchPageInputRef.current = ref;
};

const unregisterSearchPageInput = () => {
searchPageInputRef.current = undefined;
};

return {
isSearchRouterDisplayed,
openSearchRouter,
closeSearchRouter,
toggleSearch,
registerSearchPageInput,
unregisterSearchPageInput,
};
}, [isSearchRouterDisplayed, setIsSearchRouterDisplayed]);
} else if (searchRouterDisplayedRef.current) {
closeSearchRouter();
} else {
startSearchRouterOpenSpan();
openSearchRouter();
}
};

const registerSearchPageInput = (ref: AnimatedTextInputRef) => {
searchPageInputRef.current = ref;
};

const unregisterSearchPageInput = () => {
searchPageInputRef.current = undefined;
};

// Because of the React Compiler we don't need to memoize it manually
// eslint-disable-next-line react/jsx-no-constructed-context-values
Comment on lines +141 to +142
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it cause lint error without this comment?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it does

const actionsContextValue = {
openSearchRouter,
closeSearchRouter,
toggleSearch,
registerSearchPageInput,
unregisterSearchPageInput,
};

// Because of the React Compiler we don't need to memoize it manually
// eslint-disable-next-line react/jsx-no-constructed-context-values
Comment on lines +151 to +152
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, there is a lint error

const stateContextValue = {isSearchRouterDisplayed};

return (
<SearchRouterActionsContext.Provider value={actionsContextValue}>
<SearchRouterStateContext.Provider value={stateContextValue}>{children}</SearchRouterStateContext.Provider>
</SearchRouterActionsContext.Provider>
);
}

return <Context.Provider value={routerContext}>{children}</Context.Provider>;
function useSearchRouterState() {
return useContext(SearchRouterStateContext);
}

function useSearchRouterContext() {
return useContext(Context);
function useSearchRouterActions() {
return useContext(SearchRouterActionsContext);
}

export {SearchRouterContextProvider, useSearchRouterContext};
export {SearchRouterContextProvider, useSearchRouterState, useSearchRouterActions};
5 changes: 3 additions & 2 deletions src/components/Search/SearchRouter/SearchRouterModal.tsx
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Run npm run prettier

Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ import useViewportOffsetTop from '@hooks/useViewportOffsetTop';
import {isMobileIOS} from '@libs/Browser';
import CONST from '@src/CONST';
import SearchRouter from './SearchRouter';
import {useSearchRouterContext} from './SearchRouterContext';
import {useSearchRouterActions, useSearchRouterState} from './SearchRouterContext';

const isMobileWebIOS = isMobileIOS();

function SearchRouterModal() {
const {shouldUseNarrowLayout} = useResponsiveLayout();
const {isSearchRouterDisplayed, closeSearchRouter} = useSearchRouterContext();
const {isSearchRouterDisplayed} = useSearchRouterState();
const {closeSearchRouter} = useSearchRouterActions();
const viewportOffsetTop = useViewportOffsetTop();

// On mWeb Safari, the input caret stuck for a moment while the modal is animating. So, we hide the caret until the animation is done.
Expand Down
4 changes: 2 additions & 2 deletions src/libs/Navigation/AppNavigator/AuthScreens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import OpenAppFailureModal from '@components/OpenAppFailureModal';
import OptionsListContextProvider from '@components/OptionListContextProvider';
import PriorityModeController from '@components/PriorityModeController';
import {SearchContextProvider} from '@components/Search/SearchContext';
import {useSearchRouterContext} from '@components/Search/SearchRouter/SearchRouterContext';
import {useSearchRouterActions} from '@components/Search/SearchRouter/SearchRouterContext';
import SearchRouterModal from '@components/Search/SearchRouter/SearchRouterModal';
import SupportalPermissionDeniedModalProvider from '@components/SupportalPermissionDeniedModalProvider';
import {WideRHPContext} from '@components/WideRHPContextProvider';
Expand Down Expand Up @@ -143,7 +143,7 @@ function AuthScreens() {
const {shouldUseNarrowLayout} = useResponsiveLayout();
const rootNavigatorScreenOptions = useRootNavigatorScreenOptions();
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const {toggleSearch} = useSearchRouterContext();
const {toggleSearch} = useSearchRouterActions();
const currentUrl = getCurrentUrl();
const delegatorEmail = getSearchParamFromUrl(currentUrl, 'delegatorEmail');
const [credentials] = useOnyx(ONYXKEYS.CREDENTIALS, {canBeMissing: true});
Expand Down
Loading