Skip to content

Commit

Permalink
Merge pull request #48785 from software-mansion-labs/kicu/search-rout…
Browse files Browse the repository at this point in the history
…er-initial

Add SearchRouter component and context to display it
  • Loading branch information
luacmartins committed Sep 18, 2024
2 parents 2f9df8e + d89f4d2 commit 4af9a6c
Show file tree
Hide file tree
Showing 10 changed files with 244 additions and 20 deletions.
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import OnyxProvider from './components/OnyxProvider';
import PopoverContextProvider from './components/PopoverProvider';
import SafeArea from './components/SafeArea';
import ScrollOffsetContextProvider from './components/ScrollOffsetContextProvider';
import {SearchRouterContextProvider} from './components/Search/SearchRouter/SearchRouterContext';
import ThemeIllustrationsProvider from './components/ThemeIllustrationsProvider';
import ThemeProvider from './components/ThemeProvider';
import ThemeStylesProvider from './components/ThemeStylesProvider';
Expand Down Expand Up @@ -94,6 +95,7 @@ function App({url}: AppProps) {
VolumeContextProvider,
VideoPopoverMenuContextProvider,
KeyboardProvider,
SearchRouterContextProvider,
]}
>
<CustomStatusBarAndBackground />
Expand Down
1 change: 0 additions & 1 deletion src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1055,7 +1055,6 @@ const CONST = {
BOTTOM_DOCKED: 'bottom_docked',
POPOVER: 'popover',
RIGHT_DOCKED: 'right_docked',
ONBOARDING: 'onboarding',
},
ANCHOR_ORIGIN_VERTICAL: {
TOP: 'top',
Expand Down
37 changes: 37 additions & 0 deletions src/components/Search/SearchRouter/SearchButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import {PressableWithoutFeedback} from '@components/Pressable';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import Permissions from '@libs/Permissions';
import {useSearchRouterContext} from './SearchRouterContext';

function SearchButton() {
const styles = useThemeStyles();
const theme = useTheme();
const {openSearchRouter} = useSearchRouterContext();

if (!Permissions.canUseNewSearchRouter()) {
return;
}

return (
<PressableWithoutFeedback
accessibilityLabel=""
style={[styles.flexRow, styles.mr2, styles.touchableButtonImage]}
onPress={() => {
openSearchRouter();
}}
>
<Icon
src={Expensicons.MagnifyingGlass}
fill={theme.icon}
/>
</PressableWithoutFeedback>
);
}

SearchButton.displayName = 'SearchButton';

export default SearchButton;
102 changes: 102 additions & 0 deletions src/components/Search/SearchRouter/SearchRouter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import debounce from 'lodash/debounce';
import React, {useCallback, useState} from 'react';
import {View} from 'react-native';
import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal';
import Modal from '@components/Modal';
import type {SearchQueryJSON} from '@components/Search/types';
import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import * as SearchUtils from '@libs/SearchUtils';
import Navigation from '@navigation/Navigation';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import {useSearchRouterContext} from './SearchRouterContext';
import SearchRouterInput from './SearchRouterInput';

const SEARCH_DEBOUNCE_DELAY = 200;

function SearchRouter() {
const styles = useThemeStyles();

const {isSmallScreenWidth} = useResponsiveLayout();
const {isSearchRouterDisplayed, closeSearchRouter} = useSearchRouterContext();
const [currentQuery, setCurrentQuery] = useState<SearchQueryJSON | undefined>(undefined);

const clearUserQuery = () => {
setCurrentQuery(undefined);
};

const onSearchChange = debounce((userQuery: string) => {
if (!userQuery) {
clearUserQuery();
return;
}

const queryJSON = SearchUtils.buildSearchQueryJSON(userQuery);

if (queryJSON) {
// eslint-disable-next-line
console.log('parsedQuery', queryJSON);

setCurrentQuery(queryJSON);
} else {
// Handle query parsing error
}
}, SEARCH_DEBOUNCE_DELAY);

const onSearchSubmit = useCallback(() => {
closeSearchRouter();

const query = SearchUtils.buildSearchQueryString(currentQuery);
Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query}));
clearUserQuery();
}, [currentQuery, closeSearchRouter]);

useKeyboardShortcut(
CONST.KEYBOARD_SHORTCUTS.ENTER,
() => {
if (!currentQuery) {
return;
}

onSearchSubmit();
},
{
captureOnInputs: true,
shouldBubble: false,
},
);

useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ESCAPE, () => {
closeSearchRouter();
clearUserQuery();
});

const modalType = isSmallScreenWidth ? CONST.MODAL.MODAL_TYPE.CENTERED : CONST.MODAL.MODAL_TYPE.POPOVER;
const isFullWidth = isSmallScreenWidth;

return (
<Modal
type={modalType}
fullscreen
isVisible={isSearchRouterDisplayed}
popoverAnchorPosition={{right: 20, top: 20}}
onClose={closeSearchRouter}
>
<FocusTrapForModal active={isSearchRouterDisplayed}>
<View style={[styles.flex1, styles.p3]}>
<SearchRouterInput
isFullWidth={isFullWidth}
onChange={onSearchChange}
onSubmit={onSearchSubmit}
/>
</View>
</FocusTrapForModal>
</Modal>
);
}

SearchRouter.displayName = 'SearchRouter';

export default SearchRouter;
37 changes: 37 additions & 0 deletions src/components/Search/SearchRouter/SearchRouterContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React, {useContext, useMemo, useState} from 'react';
import type ChildrenProps from '@src/types/utils/ChildrenProps';

const defaultSearchContext = {
isSearchRouterDisplayed: false,
openSearchRouter: () => {},
closeSearchRouter: () => {},
};

type SearchRouterContext = typeof defaultSearchContext;

const Context = React.createContext<SearchRouterContext>(defaultSearchContext);

function SearchRouterContextProvider({children}: ChildrenProps) {
const [isSearchRouterDisplayed, setIsSearchRouterDisplayed] = useState(false);

const routerContext = useMemo(() => {
const openSearchRouter = () => setIsSearchRouterDisplayed(true);
const closeSearchRouter = () => setIsSearchRouterDisplayed(false);

return {
isSearchRouterDisplayed,
openSearchRouter,
closeSearchRouter,
};
}, [isSearchRouterDisplayed, setIsSearchRouterDisplayed]);

return <Context.Provider value={routerContext}>{children}</Context.Provider>;
}

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

SearchRouterContextProvider.displayName = 'SearchRouterContextProvider';

export {SearchRouterContextProvider, useSearchRouterContext};
41 changes: 41 additions & 0 deletions src/components/Search/SearchRouter/SearchRouterInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React, {useState} from 'react';
import BaseTextInput from '@components/TextInput/BaseTextInput';
import useThemeStyles from '@hooks/useThemeStyles';
import variables from '@styles/variables';
import CONST from '@src/CONST';

type SearchRouterInputProps = {
isFullWidth: boolean;
onChange: (searchTerm: string) => void;
onSubmit: () => void;
};

function SearchRouterInput({isFullWidth, onChange, onSubmit}: SearchRouterInputProps) {
const styles = useThemeStyles();

const [value, setValue] = useState('');

const onChangeText = (text: string) => {
setValue(text);
onChange(text);
};

const modalWidth = isFullWidth ? styles.w100 : {width: variables.popoverWidth};

return (
<BaseTextInput
value={value}
onChangeText={onChangeText}
onSubmitEditing={onSubmit}
autoFocus
textInputContainerStyles={[{borderBottomWidth: 0}, modalWidth]}
inputStyle={[styles.searchInputStyle, styles.searchRouterInputStyle, styles.ph2]}
role={CONST.ROLE.PRESENTATION}
autoCapitalize="none"
/>
);
}

SearchRouterInput.displayName = 'SearchRouterInput';

export default SearchRouterInput;
2 changes: 2 additions & 0 deletions src/libs/Navigation/AppNavigator/AuthScreens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {ValueOf} from 'type-fest';
import ComposeProviders from '@components/ComposeProviders';
import OptionsListContextProvider from '@components/OptionListContextProvider';
import {SearchContextProvider} from '@components/Search/SearchContext';
import SearchRouter from '@components/Search/SearchRouter/SearchRouter';
import useActiveWorkspace from '@hooks/useActiveWorkspace';
import usePermissions from '@hooks/usePermissions';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
Expand Down Expand Up @@ -559,6 +560,7 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie
);
})}
</RootStack.Navigator>
<SearchRouter />
</View>
</ComposeProviders>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Breadcrumbs from '@components/Breadcrumbs';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import {PressableWithoutFeedback} from '@components/Pressable';
import SearchButton from '@components/Search/SearchRouter/SearchButton';
import Text from '@components/Text';
import Tooltip from '@components/Tooltip';
import WorkspaceSwitcherButton from '@components/WorkspaceSwitcherButton';
Expand Down Expand Up @@ -73,6 +74,8 @@ function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true,
<Text style={[styles.textBlue]}>{translate('common.cancel')}</Text>
</PressableWithoutFeedback>
)}
{/* This is only temporary for development and will be cleaned up in: https://github.com/Expensify/App/issues/49122 */}
<SearchButton />
{displaySearch && (
<Tooltip text={translate('common.find')}>
<PressableWithoutFeedback
Expand Down
13 changes: 13 additions & 0 deletions src/libs/Permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {OnyxEntry} from 'react-native-onyx';
import CONST from '@src/CONST';
import type {IOUType} from '@src/CONST';
import type Beta from '@src/types/onyx/Beta';
import * as Environment from './Environment/Environment';

function canUseAllBetas(betas: OnyxEntry<Beta[]>): boolean {
return !!betas?.includes(CONST.BETAS.ALL);
Expand Down Expand Up @@ -49,6 +50,17 @@ function canUseCombinedTrackSubmit(betas: OnyxEntry<Beta[]>): boolean {
return !!betas?.includes(CONST.BETAS.COMBINED_TRACK_SUBMIT);
}

/**
* New Search Router is under construction and for now should be displayed only in dev to allow developers to work on it.
* We are not using BETA for this feature, as betas are heavier to cleanup,
* and the development of new router is expected to take 2-3 weeks at most
*
* After everything is implemented this function can be removed, as we will always use SearchRouter in the App.
*/
function canUseNewSearchRouter() {
return Environment.isDevelopment();
}

/**
* Link previews are temporarily disabled.
*/
Expand All @@ -68,4 +80,5 @@ export default {
canUseNewDotCopilot,
canUseWorkspaceRules,
canUseCombinedTrackSubmit,
canUseNewSearchRouter,
};
26 changes: 7 additions & 19 deletions src/styles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3593,31 +3593,19 @@ const styles = (theme: ThemeColors) =>
flex: 1,
},

searchPressable: {
height: variables.componentSizeNormal,
},

searchContainer: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
gap: 8,
paddingHorizontal: 24,
backgroundColor: theme.hoverComponentBG,
borderRadius: variables.componentBorderRadiusRounded,
justifyContent: 'center',
},

searchContainerHovered: {
backgroundColor: theme.border,
},

searchInputStyle: {
color: theme.textSupporting,
fontSize: 13,
lineHeight: 16,
},

searchRouterInputStyle: {
borderRadius: variables.componentBorderRadiusSmall,
borderWidth: 2,
borderColor: theme.borderFocus,
paddingHorizontal: 8,
},

searchTableHeaderActive: {
fontWeight: FontUtils.fontWeight.bold,
},
Expand Down

0 comments on commit 4af9a6c

Please sign in to comment.