diff --git a/CHANGELOG.md b/CHANGELOG.md index 65e6db67d46..64adcd0b79d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased - changed: New dynamic menu tabs that responds to scene scroll +- changed: New dynamic wallet list search drawer above tabs - fixed: Remove `minWidth` style from stake option card ## 3.23.0 diff --git a/src/__tests__/components/WalletListHeader.test.tsx b/src/__tests__/components/WalletListHeader.test.tsx index 5f7a341ad27..4e7d1c35287 100644 --- a/src/__tests__/components/WalletListHeader.test.tsx +++ b/src/__tests__/components/WalletListHeader.test.tsx @@ -15,7 +15,6 @@ describe('WalletListHeader', () => { navigation={fakeNavigation} sorting searching - searchText="string" openSortModal={() => undefined} onChangeSearchText={() => undefined} onChangeSearchingState={searching => undefined} diff --git a/src/__tests__/components/__snapshots__/MenuTabs.test.tsx.snap b/src/__tests__/components/__snapshots__/MenuTabs.test.tsx.snap index 6ebdd8cf129..8482c9e423a 100644 --- a/src/__tests__/components/__snapshots__/MenuTabs.test.tsx.snap +++ b/src/__tests__/components/__snapshots__/MenuTabs.test.tsx.snap @@ -2,7 +2,6 @@ exports[`MenuTabs should render with loading props 1`] = ` - - - - - - - Done - - - - -`; +exports[`WalletListHeader should render with loading props 1`] = ``; diff --git a/src/__tests__/scenes/__snapshots__/CreateWalletAccountSetupScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CreateWalletAccountSetupScene.test.tsx.snap index c62d3538afe..b34645bdd95 100644 --- a/src/__tests__/scenes/__snapshots__/CreateWalletAccountSetupScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CreateWalletAccountSetupScene.test.tsx.snap @@ -523,5 +523,32 @@ exports[`CreateWalletAccountSelect renders 1`] = ` } } /> + `; diff --git a/src/__tests__/scenes/__snapshots__/CreateWalletImportScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CreateWalletImportScene.test.tsx.snap index 9dd4a0f4a9c..10f0a1251df 100644 --- a/src/__tests__/scenes/__snapshots__/CreateWalletImportScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CreateWalletImportScene.test.tsx.snap @@ -541,5 +541,32 @@ exports[`CreateWalletImportScene should render with loading props 1`] = ` } } /> + `; diff --git a/src/__tests__/scenes/__snapshots__/CreateWalletSelectCryptoScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CreateWalletSelectCryptoScene.test.tsx.snap index 8afd933c7ba..51c5420f3d7 100644 --- a/src/__tests__/scenes/__snapshots__/CreateWalletSelectCryptoScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CreateWalletSelectCryptoScene.test.tsx.snap @@ -912,5 +912,32 @@ exports[`CreateWalletSelectCrypto should render with loading props 1`] = ` } } /> + `; diff --git a/src/__tests__/scenes/__snapshots__/CreateWalletSelectFiatScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CreateWalletSelectFiatScene.test.tsx.snap index 51d40aec06c..b047e2a82b4 100644 --- a/src/__tests__/scenes/__snapshots__/CreateWalletSelectFiatScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CreateWalletSelectFiatScene.test.tsx.snap @@ -839,5 +839,32 @@ exports[`CreateWalletSelectFiatComponent should render with loading props 1`] = } } /> + `; diff --git a/src/__tests__/scenes/__snapshots__/CryptoExchangeQuoteScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CryptoExchangeQuoteScene.test.tsx.snap index 1f3afe846b9..c76ff4a22d6 100644 --- a/src/__tests__/scenes/__snapshots__/CryptoExchangeQuoteScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CryptoExchangeQuoteScene.test.tsx.snap @@ -1405,5 +1405,32 @@ exports[`CryptoExchangeQuoteScreenComponent should render with loading props 1`] } } /> + `; diff --git a/src/__tests__/scenes/__snapshots__/CurrencyNotificationScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CurrencyNotificationScene.test.tsx.snap index 76ab6946abf..5863c3ff5c1 100644 --- a/src/__tests__/scenes/__snapshots__/CurrencyNotificationScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CurrencyNotificationScene.test.tsx.snap @@ -294,5 +294,32 @@ exports[`CurrencyNotificationComponent should render with loading props 1`] = ` } } /> + `; diff --git a/src/__tests__/scenes/__snapshots__/CurrencySettings.ui.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CurrencySettings.ui.test.tsx.snap index 548fb4b4213..25885d6b13c 100644 --- a/src/__tests__/scenes/__snapshots__/CurrencySettings.ui.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CurrencySettings.ui.test.tsx.snap @@ -500,5 +500,32 @@ exports[`CurrencySettings should render 1`] = ` } } /> + `; diff --git a/src/__tests__/scenes/__snapshots__/EdgeLoginScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/EdgeLoginScene.test.tsx.snap index d52870485bc..dcccd484a23 100644 --- a/src/__tests__/scenes/__snapshots__/EdgeLoginScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/EdgeLoginScene.test.tsx.snap @@ -466,5 +466,32 @@ exports[`EdgeLoginScene should render with loading props 1`] = ` } } /> + `; diff --git a/src/__tests__/scenes/__snapshots__/SendScene2.ui.test.tsx.snap b/src/__tests__/scenes/__snapshots__/SendScene2.ui.test.tsx.snap index 4b403f28768..c4713a11fa3 100644 --- a/src/__tests__/scenes/__snapshots__/SendScene2.ui.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/SendScene2.ui.test.tsx.snap @@ -799,6 +799,33 @@ exports[`SendScene2 1 spendTarget 1`] = ` } } /> + `; @@ -1811,6 +1838,33 @@ exports[`SendScene2 1 spendTarget with info tiles 1`] = ` } } /> + `; @@ -2745,6 +2799,33 @@ exports[`SendScene2 2 spendTargets 1`] = ` } } /> + `; @@ -3549,6 +3630,33 @@ exports[`SendScene2 2 spendTargets hide tiles 1`] = ` } } /> + `; @@ -4331,6 +4439,33 @@ exports[`SendScene2 2 spendTargets hide tiles 2`] = ` } } /> + `; @@ -4983,6 +5118,33 @@ exports[`SendScene2 2 spendTargets hide tiles 3`] = ` } } /> + `; @@ -5890,6 +6052,33 @@ exports[`SendScene2 2 spendTargets lock tiles 1`] = ` } } /> + `; @@ -6770,6 +6959,33 @@ exports[`SendScene2 2 spendTargets lock tiles 2`] = ` } } /> + `; @@ -7623,6 +7839,33 @@ exports[`SendScene2 2 spendTargets lock tiles 3`] = ` } } /> + `; @@ -8416,5 +8659,32 @@ exports[`SendScene2 Render SendScene 1`] = ` } } /> + `; diff --git a/src/__tests__/scenes/__snapshots__/SettingsScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/SettingsScene.test.tsx.snap index 6f08eb2467a..25d53d3b551 100644 --- a/src/__tests__/scenes/__snapshots__/SettingsScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/SettingsScene.test.tsx.snap @@ -2838,6 +2838,33 @@ exports[`MyComponent should render Locked SettingsOverview 1`] = ` } } /> + `; @@ -5679,5 +5706,32 @@ exports[`MyComponent should render UnLocked SettingsOverview 1`] = ` } } /> + `; diff --git a/src/__tests__/scenes/__snapshots__/TransactionDetailsScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/TransactionDetailsScene.test.tsx.snap index dfa084e1f35..ecfdb8abdec 100644 --- a/src/__tests__/scenes/__snapshots__/TransactionDetailsScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/TransactionDetailsScene.test.tsx.snap @@ -1056,6 +1056,33 @@ exports[`TransactionDetailsScene should render 1`] = ` } } /> + `; @@ -2115,5 +2142,32 @@ exports[`TransactionDetailsScene should render with negative nativeAmount and fi } } /> + `; diff --git a/src/components/common/SceneWrapper.tsx b/src/components/common/SceneWrapper.tsx index a0d80908908..0a7747c4592 100644 --- a/src/components/common/SceneWrapper.tsx +++ b/src/components/common/SceneWrapper.tsx @@ -4,10 +4,11 @@ import { Animated, ScrollView, StyleSheet, View } from 'react-native' import LinearGradient from 'react-native-linear-gradient' import { EdgeInsets, useSafeAreaFrame, useSafeAreaInsets } from 'react-native-safe-area-context' -import { useDrawerOpenRatio } from '../../state/SceneDrawerState' +import { useSceneDrawerState } from '../../state/SceneDrawerState' import { useSelector } from '../../types/reactRedux' import { NotificationView } from '../notification/NotificationView' import { useTheme } from '../services/ThemeContext' +import { SceneDrawer } from '../themed/SceneDrawer' import { KeyboardTracker } from './KeyboardTracker' type BackgroundOptions = @@ -39,6 +40,9 @@ interface SceneWrapperProps { // Padding to add inside the scene border: padding?: number + // Render function to render component for the tab drawer + renderDrawer?: () => React.ReactNode + // True to make the scene scrolling (if avoidKeyboard is false): scroll?: boolean } @@ -54,6 +58,7 @@ export function SceneWrapper(props: SceneWrapperProps): JSX.Element { avoidKeyboard = false, background = 'theme', children, + renderDrawer, hasHeader = true, hasTabs = false, keyboardShouldPersistTaps, @@ -65,12 +70,11 @@ export function SceneWrapper(props: SceneWrapperProps): JSX.Element { const activeUsername = useSelector(state => state.core.account.username) const isLightAccount = accountId != null && activeUsername == null - const { drawerHeight } = useDrawerOpenRatio() + const { tabDrawerHeight = 0 } = useSceneDrawerState() const theme = useTheme() const notificationHeight = isLightAccount ? theme.rem(4) : 0 - - const scrollContainerPaddingBottom = notificationHeight + drawerHeight.value + const scrollContainerPaddingBottom = notificationHeight + tabDrawerHeight // Subscribe to the window size: const frame = useSafeAreaFrame() @@ -103,12 +107,14 @@ export function SceneWrapper(props: SceneWrapperProps): JSX.Element { <> {scene} + {renderDrawer == null ? null : renderDrawer()} ) return ( {scene} + {renderDrawer == null ? null : renderDrawer()} ) } diff --git a/src/components/scenes/WalletListScene.tsx b/src/components/scenes/WalletListScene.tsx index 98630fd1118..8c9fc029bdc 100644 --- a/src/components/scenes/WalletListScene.tsx +++ b/src/components/scenes/WalletListScene.tsx @@ -17,6 +17,7 @@ import { cacheStyles, Theme, useTheme } from '../services/ThemeContext' import { EdgeText } from '../themed/EdgeText' import { WalletListFooter } from '../themed/WalletListFooter' import { WalletListHeader } from '../themed/WalletListHeader' +import { WalletListSearch } from '../themed/WalletListSearch' import { WalletListSortable } from '../themed/WalletListSortable' import { WalletListSwipeable } from '../themed/WalletListSwipeable' import { WiredProgressBar } from '../themed/WiredProgressBar' @@ -30,7 +31,7 @@ export function WalletListScene(props: Props) { const dispatch = useDispatch() const [sorting, setSorting] = React.useState(false) - const [searching, setSearching] = React.useState(false) + const [isSearching, setIsSearching] = React.useState(false) const [searchText, setSearchText] = React.useState('') const needsPasswordCheck = useSelector(state => state.ui.passwordReminder.needsPasswordCheck) @@ -47,13 +48,25 @@ export function WalletListScene(props: Props) { }) const handleRefresh = useHandler(() => { - setSearching(true) + setIsSearching(true) }) - // Turn off searching mode when a wallet is selected - const handlReset = useHandler(() => { + const handleReset = useHandler(() => { setSearchText('') - setSearching(false) + setIsSearching(false) + }) + + const handleStartSearching = useHandler(() => { + setIsSearching(true) + }) + + const handleDoneSearching = useHandler(() => { + setSearchText('') + setIsSearching(false) + }) + + const handleChangeText = useHandler((value: string) => { + setSearchText(value) }) // Show the password reminder on mount if required: @@ -78,19 +91,30 @@ export function WalletListScene(props: Props) { {}} + onChangeSearchingState={() => {}} /> ) - }, [handleSort, navigation, searchText, searching, sorting]) + }, [handleSort, navigation, isSearching, sorting]) const handlePressDone = useHandler(() => setSorting(false)) + const renderDrawer = () => { + return ( + + ) + } + return ( - + {(gap, notificationHeight) => ( <> @@ -107,13 +131,13 @@ export function WalletListScene(props: Props) { diff --git a/src/components/themed/MenuTabs.tsx b/src/components/themed/MenuTabs.tsx index ef1185b1d9b..a5bae2d4053 100644 --- a/src/components/themed/MenuTabs.tsx +++ b/src/components/themed/MenuTabs.tsx @@ -13,7 +13,7 @@ import { Fontello } from '../../assets/vector/index' import { useHandler } from '../../hooks/useHandler' import { LocaleStringKey } from '../../locales/en_US' import { lstrings } from '../../locales/strings' -import { useDrawerOpenRatio } from '../../state/SceneDrawerState' +import { useDrawerOpenRatio, useLayoutHeightInTabBar } from '../../state/SceneDrawerState' import { config } from '../../theme/appConfig' import { styled } from '../hoc/styled' import { useTheme } from '../services/ThemeContext' @@ -22,6 +22,9 @@ import { VectorIcon } from './VectorIcon' const extraTabString: LocaleStringKey = config.extraTab?.tabTitleKey ?? 'title_map' +export const MAX_TAB_BAR_HEIGHT = 58 +export const MIN_TAB_BAR_HEIGHT = 40 + const title: { readonly [key: string]: string } = { marketsTab: lstrings.title_markets, walletsTab: lstrings.title_wallets, @@ -52,15 +55,15 @@ export const MenuTabs = (props: BottomTabBarProps) => { [state.routes] ) - const insets = useSafeAreaInsets() - const activeTabRoute = state.routes[activeTabFullIndex] const activeTabIndex = routes.findIndex(route => route.name === activeTabRoute.name) - const { drawerOpenRatio, handleDrawerLayout, isRatioDisabled = false, resetDrawerRatio } = useDrawerOpenRatio() + const { drawerOpenRatio, resetDrawerRatio } = useDrawerOpenRatio() + + const handleLayout = useLayoutHeightInTabBar() return ( - + @@ -73,7 +76,6 @@ export const MenuTabs = (props: BottomTabBarProps) => { isActive={activeTabIndex === index} drawerOpenRatio={drawerOpenRatio} resetDrawerRatio={resetDrawerRatio} - isRatioDisabled={isRatioDisabled} /> ))} @@ -82,7 +84,7 @@ export const MenuTabs = (props: BottomTabBarProps) => { ) } -const Container = styled(View)<{ bottom: number; height?: number }>({ +const Container = styled(View)({ position: 'absolute', left: 0, right: 0, @@ -103,15 +105,13 @@ const Tab = ({ drawerOpenRatio, resetDrawerRatio, currentName, - isRatioDisabled, navigation }: { isActive: boolean currentName: string route: BottomTabBarProps['state']['routes'][number] - drawerOpenRatio: SharedValue | undefined + drawerOpenRatio: SharedValue resetDrawerRatio: () => void - isRatioDisabled: boolean navigation: NavigationHelpers }) => { const theme = useTheme() @@ -149,34 +149,25 @@ const Tab = ({ return ( {icon[route.name]} - ) } -const TabContainer = styled(TouchableOpacity)<{ insetBottom: number }>(theme => props => ({ +const TabContainer = styled(TouchableOpacity)<{ insetBottom: number }>(theme => ({ insetBottom }) => ({ flex: 1, paddingTop: theme.rem(0.75), - paddingBottom: Math.max(theme.rem(0.75), props.insetBottom), + paddingBottom: Math.max(theme.rem(0.75), insetBottom), justifyContent: 'center', alignItems: 'center' })) const Label = styled(Animated.Text)<{ isActive: boolean - isRatioDisabled: boolean - openRatio: SharedValue | undefined -}>(theme => ({ isActive, isRatioDisabled, openRatio }) => { + openRatio: SharedValue +}>(theme => ({ isActive, openRatio }) => { const rem = theme.rem(1) return [ { @@ -190,9 +181,10 @@ const Label = styled(Animated.Text)<{ }, useAnimatedStyle(() => { 'worklet' + if (openRatio == null) return {} return { - height: isRatioDisabled ? undefined : openRatio == null ? undefined : rem * openRatio.value, - opacity: isRatioDisabled ? undefined : openRatio == null ? undefined : openRatio.value + height: rem * openRatio.value, + opacity: openRatio.value } }) ] diff --git a/src/components/themed/SceneDrawer.tsx b/src/components/themed/SceneDrawer.tsx new file mode 100644 index 00000000000..34c1300d31d --- /dev/null +++ b/src/components/themed/SceneDrawer.tsx @@ -0,0 +1,54 @@ +import React from 'react' +import Animated, { interpolate, SharedValue, useAnimatedStyle } from 'react-native-reanimated' +import { useSafeAreaInsets } from 'react-native-safe-area-context' + +import { useDrawerOpenRatio, useLayoutHeightInTabBar } from '../../state/SceneDrawerState' +import { styled } from '../hoc/styled' +import { MAX_TAB_BAR_HEIGHT, MIN_TAB_BAR_HEIGHT } from './MenuTabs' + +export interface SceneDrawerProps { + children: React.ReactNode + keyboardHeight: number +} + +export const SceneDrawer = (props: SceneDrawerProps) => { + const { children, keyboardHeight } = props + + const insets = useSafeAreaInsets() + + const { drawerOpenRatio } = useDrawerOpenRatio() + + const handleLayout = useLayoutHeightInTabBar() + + return ( + + {children} + + ) +} + +const Drawer = styled(Animated.View)<{ + drawerOpenRatio: SharedValue + insetBottom: number + keyboardHeight: number +}>(theme => ({ drawerOpenRatio, insetBottom, keyboardHeight }) => { + const rem = theme.rem(1) + return [ + { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + flexDirection: 'column', + justifyContent: 'flex-start', + alignItems: 'stretch', + overflow: 'hidden' + }, + useAnimatedStyle(() => { + const insetHeight = Math.max(rem * 0.75, insetBottom) + return { + bottom: keyboardHeight > 0 ? keyboardHeight : interpolate(drawerOpenRatio.value, [0, 1], [MIN_TAB_BAR_HEIGHT, MAX_TAB_BAR_HEIGHT]) + insetHeight + } + }) + ] +}) diff --git a/src/components/themed/WalletListHeader.tsx b/src/components/themed/WalletListHeader.tsx index 2c9bfadfb80..2a847505715 100644 --- a/src/components/themed/WalletListHeader.tsx +++ b/src/components/themed/WalletListHeader.tsx @@ -9,13 +9,12 @@ import { PromoCard } from '../cards/PromoCard' import { cacheStyles, Theme, ThemeProps, withTheme } from '../services/ThemeContext' import { EdgeText } from '../themed/EdgeText' import { WiredBalanceBox } from '../themed/WiredBalanceBox' -import { OutlinedTextInput, OutlinedTextInputRef } from './OutlinedTextInput' +import { OutlinedTextInputRef } from './OutlinedTextInput' interface OwnProps { navigation: NavigationBase sorting: boolean searching: boolean - searchText: string openSortModal: () => void onChangeSearchText: (search: string) => void onChangeSearchingState: (searching: boolean) => void @@ -46,30 +45,11 @@ export class WalletListHeaderComponent extends React.PureComponent { } render() { - const { navigation, sorting, searching, searchText, theme } = this.props + const { navigation, sorting, searching, theme } = this.props const styles = getStyles(theme) return ( <> - - - - - {searching && ( - - {lstrings.string_done_cap} - - )} - {!searching && } {!sorting && !searching && ( @@ -112,18 +92,6 @@ const getStyles = cacheStyles((theme: Theme) => ({ }, addButton: { marginRight: theme.rem(0.5) - }, - - searchContainer: { - flexDirection: 'row', - alignItems: 'center', - marginTop: theme.rem(0.5), - marginHorizontal: theme.rem(1) - }, - searchDoneButton: { - justifyContent: 'center', - paddingLeft: theme.rem(0.75), - paddingBottom: theme.rem(1) } })) diff --git a/src/components/themed/WalletListSearch.tsx b/src/components/themed/WalletListSearch.tsx new file mode 100644 index 00000000000..7cb6ade7bc0 --- /dev/null +++ b/src/components/themed/WalletListSearch.tsx @@ -0,0 +1,105 @@ +import React, { useEffect, useState } from 'react' +import { LayoutChangeEvent } from 'react-native' +import LinearGradient from 'react-native-linear-gradient' +import Animated, { SharedValue, useAnimatedStyle, useDerivedValue } from 'react-native-reanimated' + +import { useHandler } from '../../hooks/useHandler' +import { lstrings } from '../../locales/strings' +import { useDrawerOpenRatio } from '../../state/SceneDrawerState' +import { styled } from '../hoc/styled' +import { SearchIconAnimated } from '../icons/ThemedIcons' +import { Space } from '../layout/Space' +import { useTheme } from '../services/ThemeContext' +import { SimpleTextInput, SimpleTextInputRef } from './SimpleTextInput' + +interface WalletListSearchProps { + isSearching: boolean + searchText: string + + onChangeText: (value: string) => void + onDoneSearching: () => void + onStartSearching: () => void +} + +export const WalletListSearch = (props: WalletListSearchProps) => { + const { isSearching, searchText, onChangeText, onDoneSearching, onStartSearching } = props + const theme = useTheme() + + const textInputRef = React.useRef(null) + + const { drawerOpenRatio, setKeepOpen } = useDrawerOpenRatio() + const [containerHeight, setContainerHeight] = useState(undefined) + + const inputScale = useDerivedValue(() => drawerOpenRatio.value) + + const handleLayout = useHandler((event: LayoutChangeEvent) => { + if (containerHeight != null) return + setContainerHeight(event.nativeEvent.layout.height) + }) + + const handleSearchChangeText = useHandler((text: string) => { + onChangeText(text) + }) + + const handleSearchBlur = useHandler(() => { + if (searchText === '') { + onDoneSearching() + } + }) + + const handleSearchClear = useHandler(() => { + if (!textInputRef.current?.isFocused()) { + onDoneSearching() + } + }) + + const handleSearchFocus = useHandler(() => { + onStartSearching() + }) + + useEffect(() => { + if (setKeepOpen != null) setKeepOpen(isSearching) + if (isSearching && textInputRef.current) { + textInputRef.current.focus() + } + if (!isSearching && textInputRef.current) { + textInputRef.current.blur() + } + }, [isSearching, setKeepOpen]) + + return ( + + + + + + + + ) +} + +const ContainerAnimatedView = styled(Animated.View)<{ + containerHeight?: number + drawerOpenRatio: SharedValue +}>(() => ({ containerHeight, drawerOpenRatio }) => [ + { + overflow: 'hidden' + }, + useAnimatedStyle(() => { + if (containerHeight == null) return {} + return { + height: containerHeight * drawerOpenRatio.value + } + }) +]) diff --git a/src/state/SceneDrawerState.tsx b/src/state/SceneDrawerState.tsx index b6515cc69af..0f6436abe71 100644 --- a/src/state/SceneDrawerState.tsx +++ b/src/state/SceneDrawerState.tsx @@ -1,4 +1,5 @@ -import { LayoutChangeEvent, Platform } from 'react-native' +import { useEffect } from 'react' +import { Dimensions, LayoutChangeEvent, Platform } from 'react-native' import { runOnJS, useAnimatedReaction, useSharedValue, withTiming } from 'react-native-reanimated' import { withContextProvider } from '../components/hoc/withContextProvider' @@ -8,14 +9,18 @@ import { useState } from '../types/reactHooks' import { makeUseContextValue } from '../util/makeUseContextValue' import { useSceneScrollContext } from './SceneScrollState' +const SCROLL_DISTANCE = Dimensions.get('window').height / 10 + export const [SceneDrawerProvider, SceneDrawerContext] = withContextProvider(() => { - const [isRatioDisabled, setIsRatioDisabled] = useState(false) + const [keepOpen, setKeepOpen] = useState(false) + const [tabDrawerHeight, setTabDrawerHeight] = useState(undefined) return { - drawerHeight: useSharedValue(0), drawerOpenRatio: useSharedValue(1), drawerOpenRatioStart: useSharedValue(1), - isRatioDisabled, - setIsRatioDisabled + keepOpen, + setKeepOpen, + tabDrawerHeight, + setTabDrawerHeight } }) export const useSceneDrawerState = makeUseContextValue(SceneDrawerContext) @@ -25,7 +30,7 @@ export const useDrawerOpenRatio = () => { const scrollYStart = useSharedValue(undefined) const snapTo = useSharedValue(undefined) - const { drawerHeight, drawerOpenRatio, drawerOpenRatioStart, isRatioDisabled, setIsRatioDisabled } = useSceneDrawerState() + const { drawerOpenRatio, drawerOpenRatioStart, keepOpen, setKeepOpen } = useSceneDrawerState() function resetDrawerRatio() { snapTo.value = 1 @@ -77,14 +82,14 @@ export const useDrawerOpenRatio = () => { useAnimatedReaction( () => { - // Drawer height is not ready - if (drawerHeight.value === 0) return drawerOpenRatio.value + // Keep it open when disabled + if (keepOpen) return 1 // Scrolling hasn't started yet if (scrollYStart.value == null) return const scrollYDelta = scrollY.value - scrollYStart.value - const ratioDelta = scrollYDelta / drawerHeight.value / 2 // Constant is to lower jumpy-ness + const ratioDelta = scrollYDelta / SCROLL_DISTANCE // Constant is to lower jumpy-ness return Math.min(1, Math.max(0, drawerOpenRatioStart.value - ratioDelta)) }, @@ -109,31 +114,55 @@ export const useDrawerOpenRatio = () => { snapTo.value = undefined drawerOpenRatio.value = currentValue }, - [] + [keepOpen] ) useAnimatedReaction( - () => snapTo.value, + () => { + // Keep it open when disabled + if (keepOpen) return 1 + + return snapTo.value + }, (currentValue, previousValue) => { if (currentValue === previousValue) return if (currentValue == null) return drawerOpenRatio.value = withTiming(currentValue, { duration: 300 }) - } + }, + [keepOpen] ) - const handleDrawerLayout = useHandler((event: LayoutChangeEvent) => { - // Only handle the initial layout (re-layout is not yet supported): - if (drawerHeight.value !== 0) return - drawerHeight.value = event.nativeEvent.layout.height - }) - return { - drawerHeight, drawerOpenRatio, - isRatioDisabled, - setIsRatioDisabled, - handleDrawerLayout, - resetDrawerRatio + resetDrawerRatio, + + keepOpen, + setKeepOpen } } + +export const useLayoutHeightInTabBar = (): ((event: LayoutChangeEvent) => void) => { + const { setTabDrawerHeight } = useSceneDrawerState() + + const [layoutHeight, setLayoutHeight] = useState(undefined) + + // One-time layout measurement handler: + const handleLayout = useHandler((event: LayoutChangeEvent) => { + if (layoutHeight == null) { + const layoutHeight = event.nativeEvent.layout.height + setLayoutHeight((prev = 0) => prev + layoutHeight) + } + }) + + // Add/subtract container height to the tab-bar height when mounted/unmounted + useEffect(() => { + if (layoutHeight == null) return + setTabDrawerHeight((prev = 0) => prev + layoutHeight) + return () => { + setTabDrawerHeight((prev = 0) => prev - layoutHeight) + } + }, [layoutHeight, setTabDrawerHeight]) + + return handleLayout +}