From 01e3b3f2a1258ff7558d35e0d2a9470a0840a34f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Mon, 16 Dec 2024 10:49:10 +0100 Subject: [PATCH] feat: implement freezeOnBlur --- .changeset/purple-baboons-rhyme.md | 6 + apps/example/ios/Podfile.lock | 8 +- apps/example/src/App.tsx | 5 + .../Examples/NativeBottomTabsFreezeOnBlur.tsx | 115 ++++++++++++++++++ .../react-native-bottom-tabs/package.json | 4 +- .../react-native-bottom-tabs/src/Screen.tsx | 26 ++++ .../react-native-bottom-tabs/src/TabView.tsx | 16 ++- .../react-native-bottom-tabs/src/types.ts | 1 + packages/react-navigation/src/types.ts | 9 ++ .../src/views/NativeBottomTabView.tsx | 3 + yarn.lock | 2 + 11 files changed, 188 insertions(+), 7 deletions(-) create mode 100644 .changeset/purple-baboons-rhyme.md create mode 100644 apps/example/src/Examples/NativeBottomTabsFreezeOnBlur.tsx create mode 100644 packages/react-native-bottom-tabs/src/Screen.tsx diff --git a/.changeset/purple-baboons-rhyme.md b/.changeset/purple-baboons-rhyme.md new file mode 100644 index 0000000..344bfb4 --- /dev/null +++ b/.changeset/purple-baboons-rhyme.md @@ -0,0 +1,6 @@ +--- +'react-native-bottom-tabs': patch +'@bottom-tabs/react-navigation': patch +--- + +feat: add freezeOnBlur diff --git a/apps/example/ios/Podfile.lock b/apps/example/ios/Podfile.lock index fd71fc1..879f9d3 100644 --- a/apps/example/ios/Podfile.lock +++ b/apps/example/ios/Podfile.lock @@ -1209,7 +1209,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-bottom-tabs (0.7.3): + - react-native-bottom-tabs (0.7.6): - DoubleConversion - glog - RCT-Folly (= 2024.01.01.00) @@ -1222,7 +1222,7 @@ PODS: - React-graphics - React-ImageManager - React-jsi - - react-native-bottom-tabs/common (= 0.7.3) + - react-native-bottom-tabs/common (= 0.7.6) - React-NativeModulesApple - React-RCTFabric - React-rendererdebug @@ -1234,7 +1234,7 @@ PODS: - SDWebImageSVGCoder (>= 1.7.0) - SwiftUIIntrospect (~> 1.0) - Yoga - - react-native-bottom-tabs/common (0.7.3): + - react-native-bottom-tabs/common (0.7.6): - DoubleConversion - glog - RCT-Folly (= 2024.01.01.00) @@ -1943,7 +1943,7 @@ SPEC CHECKSUMS: React-logger: d79b704bf215af194f5213a6b7deec50ba8e6a9b React-Mapbuffer: b982d5bba94a8bc073bda48f0d27c9b28417fae3 React-microtasksnativemodule: 8fa285fed833a04a754bf575f8ded65fc240b88d - react-native-bottom-tabs: b6b3dc2e971c860a0a6d763701929d1899f666a0 + react-native-bottom-tabs: 084cfd4d4b1e74c03f4196b3f62d39445882f45f react-native-safe-area-context: 73505107f7c673cd550a561aeb6271f152c483b6 React-nativeconfig: 8c83d992b9cc7d75b5abe262069eaeea4349f794 React-NativeModulesApple: b8465afc883f5bf3fe8bac3767e394d581a5f123 diff --git a/apps/example/src/App.tsx b/apps/example/src/App.tsx index b724fb2..8d6e56e 100644 --- a/apps/example/src/App.tsx +++ b/apps/example/src/App.tsx @@ -27,6 +27,7 @@ import TintColorsExample from './Examples/TintColors'; import NativeBottomTabsEmbeddedStacks from './Examples/NativeBottomTabsEmbeddedStacks'; import NativeBottomTabsSVGs from './Examples/NativeBottomTabsSVGs'; import NativeBottomTabsRemoteIcons from './Examples/NativeBottomTabsRemoteIcons'; +import NativeBottomTabsFreezeOnBlur from './Examples/NativeBottomTabsFreezeOnBlur'; const FourTabsIgnoreSafeArea = () => { return ; @@ -102,6 +103,10 @@ const examples = [ name: 'Four Tabs - Transparent scroll edge appearance', platform: 'ios', }, + { + component: NativeBottomTabsFreezeOnBlur, + name: 'Native Bottom Tabs with freezeOnBlur', + }, { component: FourTabsOpaqueScrollEdgeAppearance, name: 'Four Tabs - Opaque scroll edge appearance', diff --git a/apps/example/src/Examples/NativeBottomTabsFreezeOnBlur.tsx b/apps/example/src/Examples/NativeBottomTabsFreezeOnBlur.tsx new file mode 100644 index 0000000..b42439a --- /dev/null +++ b/apps/example/src/Examples/NativeBottomTabsFreezeOnBlur.tsx @@ -0,0 +1,115 @@ +import * as React from 'react'; +import { Platform, StyleSheet, Text, View } from 'react-native'; +import { createNativeBottomTabNavigator } from '@bottom-tabs/react-navigation'; + +const store = new Set(); + +type Dispatch = (value: number) => void; + +function useValue() { + const [value, setValue] = React.useState(0); + + React.useEffect(() => { + const dispatch = (value: number) => { + setValue(value); + }; + store.add(dispatch); + return () => { + store.delete(dispatch); + }; + }, [setValue]); + + return value; +} + +function HomeScreen() { + return ( + + Home! + + ); +} + +function DetailsScreen(props: any) { + const value = useValue(); + const screenName = props?.route?.params?.screenName; + // only 1 'render' should appear at the time + console.log(`${Platform.OS} Details Screen render ${value} ${screenName}`); + return ( + + Details! + + Details Screen {value} {screenName ? screenName : ''}{' '} + + + ); +} +const Tab = createNativeBottomTabNavigator(); + +export default function NativeBottomTabsFreezeOnBlur() { + React.useEffect(() => { + let timer = 0; + const interval = setInterval(() => { + timer = timer + 1; + store.forEach((dispatch) => dispatch(timer)); + }, 3000); + return () => clearInterval(interval); + }, []); + + return ( + + require('../../assets/icons/article_dark.png'), + }} + /> + require('../../assets/icons/grid_dark.png'), + }} + /> + require('../../assets/icons/person_dark.png'), + }} + /> + require('../../assets/icons/chat_dark.png'), + }} + /> + + ); +} + +const styles = StyleSheet.create({ + screenContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, +}); diff --git a/packages/react-native-bottom-tabs/package.json b/packages/react-native-bottom-tabs/package.json index 1824df2..b5db412 100644 --- a/packages/react-native-bottom-tabs/package.json +++ b/packages/react-native-bottom-tabs/package.json @@ -77,11 +77,13 @@ "react": "18.3.1", "react-native": "0.75.4", "react-native-builder-bob": "^0.32.1", + "react-native-screens": "4.3.0", "typescript": "^5.2.2" }, "peerDependencies": { "react": "*", - "react-native": "*" + "react-native": "*", + "react-native-screens": ">=3.29.0" }, "react-native-builder-bob": { "source": "src", diff --git a/packages/react-native-bottom-tabs/src/Screen.tsx b/packages/react-native-bottom-tabs/src/Screen.tsx new file mode 100644 index 0000000..4e13793 --- /dev/null +++ b/packages/react-native-bottom-tabs/src/Screen.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import { View } from 'react-native'; +import type { StyleProp, ViewProps, ViewStyle } from 'react-native'; + +interface Props extends ViewProps { + visible: boolean; + children?: React.ReactNode; + freezeOnBlur?: boolean; + style?: StyleProp; + collapsable?: boolean; +} + +let Screens: typeof import('react-native-screens') | undefined; + +try { + Screens = require('react-native-screens'); +} catch (e) { + // Ignore +} + +export function Screen({ visible, ...rest }: Props) { + if (Screens?.screensEnabled()) { + return ; + } + return ; +} diff --git a/packages/react-native-bottom-tabs/src/TabView.tsx b/packages/react-native-bottom-tabs/src/TabView.tsx index 7f56420..72436d9 100644 --- a/packages/react-native-bottom-tabs/src/TabView.tsx +++ b/packages/react-native-bottom-tabs/src/TabView.tsx @@ -15,6 +15,7 @@ import type { ImageSource } from 'react-native/Libraries/Image/ImageSource'; import TabViewAdapter from './TabViewAdapter'; import useLatestCallback from 'use-latest-callback'; import type { BaseRoute, NavigationState } from './types'; +import { Screen } from './Screen'; const isAppleSymbol = (icon: any): icon is { sfSymbol: string } => icon?.sfSymbol; @@ -116,6 +117,13 @@ interface Props { */ getTestID?: (props: { route: Route }) => string | undefined; + /** + * Get freezeOnBlur for the current screen. Uses false by default. + * Defaults to `true` when `enableFreeze()` is run at the top of the application. + * + */ + getFreezeOnBlur?: (props: { route: Route }) => boolean | undefined; + /** * Background color of the tab bar. */ @@ -160,6 +168,7 @@ const TabView = ({ tabBarInactiveTintColor: inactiveTintColor, getLazy = ({ route }: { route: Route }) => route.lazy, getLabelText = ({ route }: { route: Route }) => route.title, + getFreezeOnBlur = ({ route }: { route: Route }) => route.freezeOnBlur, getIcon = ({ route, focused }: { route: Route; focused: boolean }) => route.unfocusedIcon ? focused @@ -311,11 +320,14 @@ const TabView = ({ const focused = route.key === focusedKey; const opacity = focused ? 1 : 0; const zIndex = focused ? 0 : -1; + const freezeOnBlur = getFreezeOnBlur({ route }); return ( - ({ route, jumpTo, })} - + ); })} diff --git a/packages/react-native-bottom-tabs/src/types.ts b/packages/react-native-bottom-tabs/src/types.ts index 5993a61..744af8d 100644 --- a/packages/react-native-bottom-tabs/src/types.ts +++ b/packages/react-native-bottom-tabs/src/types.ts @@ -15,6 +15,7 @@ export type BaseRoute = { activeTintColor?: string; hidden?: boolean; testID?: string; + freezeOnBlur?: boolean; }; export type NavigationState = { diff --git a/packages/react-navigation/src/types.ts b/packages/react-navigation/src/types.ts index eb1c3eb..3209b44 100644 --- a/packages/react-navigation/src/types.ts +++ b/packages/react-navigation/src/types.ts @@ -91,6 +91,14 @@ export type NativeBottomTabNavigationOptions = { * TestID for the tab. */ tabBarButtonTestID?: string; + + /** + * Whether inactive screens should be suspended from re-rendering. Defaults to `false`. + * Defaults to `true` when `enableFreeze()` is run at the top of the application. + * + * Only supported on iOS and Android. + */ + freezeOnBlur?: boolean; }; export type NativeBottomTabDescriptor = Descriptor< @@ -117,5 +125,6 @@ export type NativeBottomTabNavigationConfig = Partial< | 'onTabLongPress' | 'getActiveTintColor' | 'getTestID' + | 'getFreezeOnBlur' > >; diff --git a/packages/react-navigation/src/views/NativeBottomTabView.tsx b/packages/react-navigation/src/views/NativeBottomTabView.tsx index 8c633f4..8d2acab 100644 --- a/packages/react-navigation/src/views/NativeBottomTabView.tsx +++ b/packages/react-navigation/src/views/NativeBottomTabView.tsx @@ -45,6 +45,9 @@ export default function NativeBottomTabView({ const options = descriptors[route.key]?.options; return options?.tabBarItemHidden === true; }} + getFreezeOnBlur={({ route }) => + descriptors[route.key]?.options.freezeOnBlur + } getTestID={({ route }) => descriptors[route.key]?.options.tabBarButtonTestID } diff --git a/yarn.lock b/yarn.lock index 0e39e01..fb2aeca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15614,12 +15614,14 @@ __metadata: react: 18.3.1 react-native: 0.75.4 react-native-builder-bob: ^0.32.1 + react-native-screens: 4.3.0 sf-symbols-typescript: ^2.0.0 typescript: ^5.2.2 use-latest-callback: ^0.2.1 peerDependencies: react: "*" react-native: "*" + react-native-screens: ">=3.29.0" languageName: unknown linkType: soft