Skip to content
Draft
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
4 changes: 4 additions & 0 deletions assets/images/sidebar-left.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions assets/images/sidebar-right.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9367,6 +9367,7 @@ const CONST = {
TRANSACTION_LIST_ITEM_CHECKBOX: 'Search-TransactionListItemCheckbox',
EXPANDED_TRANSACTION_ROW: 'Search-ExpandedTransactionRow',
EXPANDED_TRANSACTION_ROW_CHECKBOX: 'Search-ExpandedTransactionRowCheckbox',
SIDEBAR_TOGGLE: 'Search-SidebarToggle',
TYPE_MENU_ITEM: 'Search-TypeMenuItem',
SAVED_SEARCH_MENU_ITEM: 'Search-SavedSearchMenuItem',
ADVANCED_FILTER_ITEM: 'Search-AdvancedFilterItem',
Expand Down
76 changes: 65 additions & 11 deletions src/components/Navigation/SearchSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,32 @@
import type {ParamListBase} from '@react-navigation/native';
import React, {useEffect} from 'react';
import {View} from 'react-native';
import Animated from 'react-native-reanimated';
import SidebarLeftIcon from '@assets/images/sidebar-left.svg';
import SidebarRightIcon from '@assets/images/sidebar-right.svg';
import Hoverable from '@components/Hoverable';
import Icon from '@components/Icon';
import {PressableWithoutFeedback} from '@components/Pressable';
import {useSearchQueryContext, useSearchResultsActions, useSearchResultsContext} from '@components/Search/SearchContext';
import Tooltip from '@components/Tooltip';
import useLoadingBarVisibility from '@hooks/useLoadingBarVisibility';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import type {PlatformStackNavigationState} from '@libs/Navigation/PlatformStackNavigation/types';
import SearchTypeMenuWide from '@pages/Search/SearchTypeMenuWide';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import SCREENS from '@src/SCREENS';
import {
useSearchSidebarCollapse,
useSearchSidebarCollapseFadeStyle,
useSearchSidebarLayoutWidthStyle,
useSearchSidebarToggleButtonStyle,
useSearchSidebarVisualWidthStyle,
} from './SearchSidebarCollapseStore';
import TopBar from './TopBar';

type SearchSidebarProps = {
Expand All @@ -19,9 +36,15 @@ type SearchSidebarProps = {
function SearchSidebar({state}: SearchSidebarProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const theme = useTheme();
const {isOffline} = useNetwork();
const shouldShowLoadingBarForReports = useLoadingBarVisibility();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const {isCollapsed, toggleSidebar, startPeek, endPeek} = useSearchSidebarCollapse();
const layoutSpacerStyle = useSearchSidebarLayoutWidthStyle();
const visualSidebarWidthStyle = useSearchSidebarVisualWidthStyle();
const breadcrumbAnimatedStyle = useSearchSidebarCollapseFadeStyle();
const toggleButtonAnimatedStyle = useSearchSidebarToggleButtonStyle();

const route = state.routes.at(-1);
const {lastSearchType, currentSearchResults} = useSearchResultsContext();
Expand All @@ -45,18 +68,49 @@ function SearchSidebar({state}: SearchSidebarProps) {
return null;
}

const toggleButtonLabel = translate(isCollapsed ? 'reportActionCompose.expand' : 'reportActionCompose.collapse');
const toggleButton = (
<Tooltip text={toggleButtonLabel}>
<Animated.View style={toggleButtonAnimatedStyle}>
<PressableWithoutFeedback
accessibilityLabel={toggleButtonLabel}
onPress={toggleSidebar}
sentryLabel={CONST.SENTRY_LABEL.SEARCH.SIDEBAR_TOGGLE}
style={[styles.p2, styles.br2]}
>
<Icon
src={isCollapsed ? SidebarRightIcon : SidebarLeftIcon}
width={variables.iconSizeNormal}
height={variables.iconSizeNormal}
fill={theme.icon}
/>
</PressableWithoutFeedback>
</Animated.View>
</Tooltip>
);

return (
<View style={styles.searchSidebar}>
<View style={styles.flex1}>
<TopBar
shouldShowLoadingBar={shouldShowLoadingState || shouldShowLoadingBarForReports}
breadcrumbLabel={translate('common.spend')}
shouldDisplaySearch={false}
shouldDisplayHelpButton={false}
/>
<SearchTypeMenuWide queryJSON={currentSearchQueryJSON} />
</View>
</View>
<Animated.View style={[{height: '100%'}, layoutSpacerStyle]}>
<Hoverable
onHoverIn={startPeek}
onHoverOut={endPeek}
>
<Animated.View style={[styles.searchSidebar, {position: 'absolute', top: 0, bottom: 0, left: 0, zIndex: 1}, visualSidebarWidthStyle]}>
<View style={styles.flex1}>
<TopBar
shouldShowLoadingBar={shouldShowLoadingState || shouldShowLoadingBarForReports}
breadcrumbLabel={translate('common.spend')}
breadcrumbAnimatedStyle={breadcrumbAnimatedStyle}
shouldDisplaySearch={false}
shouldDisplayHelpButton={false}
>
{toggleButton}
</TopBar>
<SearchTypeMenuWide queryJSON={currentSearchQueryJSON} />
</View>
</Animated.View>
</Hoverable>
</Animated.View>
);
}

Expand Down
136 changes: 136 additions & 0 deletions src/components/Navigation/SearchSidebarCollapseStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import {useMemo, useSyncExternalStore} from 'react';
import {Platform} from 'react-native';
import type {ViewStyle} from 'react-native';
import variables from '@styles/variables';

const SEARCH_SIDEBAR_COLLAPSE_ANIMATION_DURATION_MS = 220;
const SEARCH_SIDEBAR_COLLAPSE_TRANSLATE_X = -8;
const TOGGLE_BUTTON_COLLAPSED_TRANSLATE_X = -10;

const layoutTransitionStyle: ViewStyle =
Platform.OS === 'web' ? {transition: `width ${SEARCH_SIDEBAR_COLLAPSE_ANIMATION_DURATION_MS}ms ease, margin-left ${SEARCH_SIDEBAR_COLLAPSE_ANIMATION_DURATION_MS}ms ease`} : {};
const fadeTransitionStyle: ViewStyle =
Platform.OS === 'web' ? {transition: `opacity ${SEARCH_SIDEBAR_COLLAPSE_ANIMATION_DURATION_MS}ms ease, transform ${SEARCH_SIDEBAR_COLLAPSE_ANIMATION_DURATION_MS}ms ease`} : {};

let isCollapsed = false;
let isPeeking = false;

const listeners = new Set<() => void>();

function subscribe(listener: () => void) {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
}

function notify() {
for (const listener of listeners) {
listener();
}
}

function getCollapseSnapshot() {
return isCollapsed;
}

function getPeekSnapshot() {
return isPeeking;
}

function getSearchSidebarWidth(progress: number) {
return variables.searchSidebarExpandedWidth + (variables.searchSidebarCollapsedWidth - variables.searchSidebarExpandedWidth) * progress;
}

function toggleSidebar() {
isCollapsed = !isCollapsed;
isPeeking = false;
notify();
}

function startPeek() {
if (!isCollapsed || isPeeking) {
return;
}

isPeeking = true;
notify();
}

function endPeek() {
if (!isPeeking) {
return;
}

isPeeking = false;
notify();
}

function useSearchSidebarCollapse() {
const collapsed = useSyncExternalStore(subscribe, getCollapseSnapshot, getCollapseSnapshot);
const peeking = useSyncExternalStore(subscribe, getPeekSnapshot, getPeekSnapshot);

return {
isCollapsed: collapsed,
isPeeking: peeking,
isVisuallyCollapsed: collapsed && !peeking,
toggleSidebar,
startPeek,
endPeek,
};
}

function useSearchSidebarLayoutWidthStyle() {
const {isCollapsed: collapsed} = useSearchSidebarCollapse();

return useMemo<ViewStyle>(() => ({...layoutTransitionStyle, width: getSearchSidebarWidth(collapsed ? 1 : 0)}), [collapsed]);
}

function useSearchSidebarVisualWidthStyle() {
const {isCollapsed: collapsed, isPeeking: peeking} = useSearchSidebarCollapse();

return useMemo<ViewStyle>(() => ({...layoutTransitionStyle, width: getSearchSidebarWidth(collapsed && !peeking ? 1 : 0)}), [collapsed, peeking]);
}

function useSearchSidebarContentOffsetStyle() {
const {isCollapsed: collapsed} = useSearchSidebarCollapse();

return useMemo<ViewStyle>(() => ({...layoutTransitionStyle, marginLeft: getSearchSidebarWidth(collapsed ? 1 : 0)}), [collapsed]);
}

function useSearchSidebarCollapseFadeStyle() {
const {isVisuallyCollapsed} = useSearchSidebarCollapse();

return useMemo<ViewStyle>(
() => ({
...fadeTransitionStyle,
opacity: isVisuallyCollapsed ? 0 : 1,
transform: [{translateX: isVisuallyCollapsed ? SEARCH_SIDEBAR_COLLAPSE_TRANSLATE_X : 0}],
}),
[isVisuallyCollapsed],
);
}

function useSearchSidebarToggleButtonStyle() {
const {isVisuallyCollapsed} = useSearchSidebarCollapse();

return useMemo<ViewStyle>(
() => ({
...fadeTransitionStyle,
transform: [{translateX: isVisuallyCollapsed ? TOGGLE_BUTTON_COLLAPSED_TRANSLATE_X : 0}],
}),
[isVisuallyCollapsed],
);
}

export {
toggleSidebar,
startPeek,
endPeek,
useSearchSidebarCollapse,
useSearchSidebarLayoutWidthStyle,
useSearchSidebarVisualWidthStyle,
useSearchSidebarContentOffsetStyle,
useSearchSidebarCollapseFadeStyle,
useSearchSidebarToggleButtonStyle,
};
10 changes: 7 additions & 3 deletions src/components/Navigation/TopBar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import React from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
import {Keyboard, View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import Animated from 'react-native-reanimated';
import type {AnimatedStyle} from 'react-native-reanimated';
import LoadingBar from '@components/LoadingBar';
import {PressableWithoutFeedback} from '@components/Pressable';
import SearchButton from '@components/Search/SearchRouter/SearchButton';
Expand All @@ -24,11 +27,12 @@ type TopBarProps = {
shouldShowLoadingBar?: boolean;
cancelSearch?: () => void;
children?: React.ReactNode;
breadcrumbAnimatedStyle?: StyleProp<AnimatedStyle<ViewStyle>>;
};

const authTokenTypeSelector = (session: OnyxEntry<Session>) => session && {authTokenType: session.authTokenType};

function TopBar({breadcrumbLabel, shouldDisplaySearch = true, shouldDisplayHelpButton = false, cancelSearch, shouldShowLoadingBar, children}: TopBarProps) {
function TopBar({breadcrumbLabel, shouldDisplaySearch = true, shouldDisplayHelpButton = false, cancelSearch, shouldShowLoadingBar, children, breadcrumbAnimatedStyle}: TopBarProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const [session] = useOnyx(ONYXKEYS.SESSION, {selector: authTokenTypeSelector});
Expand All @@ -49,15 +53,15 @@ function TopBar({breadcrumbLabel, shouldDisplaySearch = true, shouldDisplayHelpB
onTouchStart={isInLandscapeMode ? () => Keyboard.dismiss() : undefined}
>
<View style={[styles.flex1, styles.flexRow, styles.alignItemsCenter, styles.pr2]}>
<View style={[styles.flex1, styles.flexRow, styles.alignItemsCenter]}>
<Animated.View style={[styles.flex1, styles.flexRow, styles.alignItemsCenter, breadcrumbAnimatedStyle]}>
<Text
numberOfLines={1}
style={[styles.flexShrink1, styles.topBarLabel]}
accessibilityRole={CONST.ROLE.HEADER}
>
{breadcrumbLabel}
</Text>
</View>
</Animated.View>
</View>
{children}
{displaySignIn && <SignInButton />}
Expand Down
9 changes: 6 additions & 3 deletions src/pages/Search/SearchPageWide.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import React, {useCallback, useContext, useMemo, useRef} from 'react';
import type {NativeScrollEvent, NativeSyntheticEvent} from 'react-native';
import {StyleSheet, View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import Animated from 'react-native-reanimated';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import {useSearchSidebarContentOffsetStyle} from '@components/Navigation/SearchSidebarCollapseStore';
import ReceiptScanDropZone from '@components/ReceiptScanDropZone';
import ScreenWrapper from '@components/ScreenWrapper';
import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider';
Expand Down Expand Up @@ -88,11 +90,12 @@ function SearchPageWide({
}, [shouldShowFooter, styles]);

const handleOnBackButtonPress = () => Navigation.goBack(ROUTES.SEARCH_ROOT.getRoute({query: buildCannedSearchQuery()}));
const splitContainerAnimatedStyle = useSearchSidebarContentOffsetStyle();

return (
<View
<Animated.View
ref={receiptDropTargetRef}
style={styles.searchSplitContainer}
style={[styles.searchSplitContainer, splitContainerAnimatedStyle]}
>
<ScreenWrapper
testID="Search"
Expand Down Expand Up @@ -158,7 +161,7 @@ function SearchPageWide({
</FullPageNotFoundView>
</ScreenWrapper>
{!!queryJSON && <ReceiptScanDropZone targetRef={receiptDropTargetRef} />}
</View>
</Animated.View>
);
}

Expand Down
Loading
Loading